[feat](trx-frontend-http): add exclusive flag to scheduler entries
When a schedule entry has `exclusive: true`, the scheduler stays on that entry's bookmark for the entire time window without interleaving with other overlapping entries. Useful for WEFAX and satellite passes where switching away mid-reception would lose data. Backend: first exclusive active entry wins outright in timespan_active_entry. Frontend: "Excl." checkbox in inline edit disables interleave input; interleave status shows exclusive entry as sole active entry. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -206,6 +206,11 @@
|
|||||||
if (active.length === 1) {
|
if (active.length === 1) {
|
||||||
return { activeEntries: active, currentIndex: 0, remainingSec: 0, cycleMin: 0 };
|
return { activeEntries: active, currentIndex: 0, remainingSec: 0, cycleMin: 0 };
|
||||||
}
|
}
|
||||||
|
// Exclusive entry wins outright — no interleaving.
|
||||||
|
var exclIdx = active.findIndex(function (e) { return e.exclusive; });
|
||||||
|
if (exclIdx >= 0) {
|
||||||
|
return { activeEntries: [active[exclIdx]], currentIndex: 0, remainingSec: 0, cycleMin: 0 };
|
||||||
|
}
|
||||||
const defaultInterleave = Number(config.interleave_min);
|
const defaultInterleave = Number(config.interleave_min);
|
||||||
const durations = active.map(function (entry) {
|
const durations = active.map(function (entry) {
|
||||||
const own = Number(entry && entry.interleave_min);
|
const own = Number(entry && entry.interleave_min);
|
||||||
@@ -820,7 +825,8 @@
|
|||||||
'<button class="sch-write sch-inline-extra-add" type="button" style="padding:0 0.7rem;">+</button>' +
|
'<button class="sch-write sch-inline-extra-add" type="button" style="padding:0 0.7rem;">+</button>' +
|
||||||
'</div></td>' +
|
'</div></td>' +
|
||||||
'<td><input type="text" class="status-input sch-inline-input" value="' + escHtml(entry.label || '') + '" data-field="label" /></td>' +
|
'<td><input type="text" class="status-input sch-inline-input" value="' + escHtml(entry.label || '') + '" data-field="label" /></td>' +
|
||||||
'<td><input type="number" class="status-input sch-inline-input" value="' + (entry.interleave_min || '') + '" min="1" max="60" placeholder="\u2014" data-field="interleave" style="width:4rem;" /></td>' +
|
'<td><input type="number" class="status-input sch-inline-input" value="' + (entry.interleave_min || '') + '" min="1" max="60" placeholder="\u2014" data-field="interleave" style="width:4rem;" ' + (entry.exclusive ? 'disabled' : '') + ' />' +
|
||||||
|
'<label style="display:block;font-size:0.85em;margin-top:0.2rem;"><input type="checkbox" data-field="exclusive" ' + (entry.exclusive ? 'checked' : '') + ' /> Excl.</label></td>' +
|
||||||
'<td><input type="checkbox" ' + (entry.record ? 'checked' : '') + ' data-field="record" /></td>' +
|
'<td><input type="checkbox" ' + (entry.record ? 'checked' : '') + ' data-field="record" /></td>' +
|
||||||
'<td><button class="sch-write sch-inline-save" type="button">Save</button><button class="sch-write sch-inline-cancel" type="button">Cancel</button></td>';
|
'<td><button class="sch-write sch-inline-save" type="button">Save</button><button class="sch-write sch-inline-cancel" type="button">Cancel</button></td>';
|
||||||
|
|
||||||
@@ -862,6 +868,16 @@
|
|||||||
extraPick.value = '';
|
extraPick.value = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Wire exclusive checkbox to disable interleave input.
|
||||||
|
var exclEl = tr.querySelector('[data-field="exclusive"]');
|
||||||
|
var ilInput = tr.querySelector('[data-field="interleave"]');
|
||||||
|
if (exclEl && ilInput) {
|
||||||
|
exclEl.addEventListener('change', function () {
|
||||||
|
ilInput.disabled = exclEl.checked;
|
||||||
|
if (exclEl.checked) ilInput.value = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
tr.querySelector('.sch-inline-save').addEventListener('click', function () {
|
tr.querySelector('.sch-inline-save').addEventListener('click', function () {
|
||||||
var startEl = tr.querySelector('[data-field="start"]');
|
var startEl = tr.querySelector('[data-field="start"]');
|
||||||
var endEl = tr.querySelector('[data-field="end"]');
|
var endEl = tr.querySelector('[data-field="end"]');
|
||||||
@@ -869,6 +885,7 @@
|
|||||||
var labelEl = tr.querySelector('[data-field="label"]');
|
var labelEl = tr.querySelector('[data-field="label"]');
|
||||||
var ilEl = tr.querySelector('[data-field="interleave"]');
|
var ilEl = tr.querySelector('[data-field="interleave"]');
|
||||||
var recEl = tr.querySelector('[data-field="record"]');
|
var recEl = tr.querySelector('[data-field="record"]');
|
||||||
|
var exEl = tr.querySelector('[data-field="exclusive"]');
|
||||||
|
|
||||||
if (bmEl && !bmEl.value) { alert('Please select a bookmark.'); return; }
|
if (bmEl && !bmEl.value) { alert('Please select a bookmark.'); return; }
|
||||||
|
|
||||||
@@ -876,8 +893,9 @@
|
|||||||
entry.end_min = hhmmToMin(endEl.value);
|
entry.end_min = hhmmToMin(endEl.value);
|
||||||
entry.bookmark_id = bmEl.value;
|
entry.bookmark_id = bmEl.value;
|
||||||
entry.label = labelEl.value.trim() || null;
|
entry.label = labelEl.value.trim() || null;
|
||||||
|
entry.exclusive = exEl ? exEl.checked : false;
|
||||||
var ilVal = parseInt(ilEl.value, 10);
|
var ilVal = parseInt(ilEl.value, 10);
|
||||||
entry.interleave_min = (!isNaN(ilVal) && ilVal > 0) ? ilVal : null;
|
entry.interleave_min = entry.exclusive ? null : ((!isNaN(ilVal) && ilVal > 0) ? ilVal : null);
|
||||||
entry.bookmark_ids = inlineExtraIds.slice();
|
entry.bookmark_ids = inlineExtraIds.slice();
|
||||||
entry.record = recEl.checked;
|
entry.record = recEl.checked;
|
||||||
|
|
||||||
@@ -908,7 +926,7 @@
|
|||||||
entry.id && String(entry.id) === String(currentSchedulerStatus.last_entry_id)) {
|
entry.id && String(entry.id) === String(currentSchedulerStatus.last_entry_id)) {
|
||||||
tr.classList.add("sch-active");
|
tr.classList.add("sch-active");
|
||||||
}
|
}
|
||||||
const il = entry.interleave_min ? String(entry.interleave_min) + " min" : "—";
|
const il = entry.exclusive ? "Exclusive" : entry.interleave_min ? String(entry.interleave_min) + " min" : "—";
|
||||||
const allDay = entry.start_min === entry.end_min;
|
const allDay = entry.start_min === entry.end_min;
|
||||||
const centerCell = entry.center_hz ? formatFreq(entry.center_hz) : "—";
|
const centerCell = entry.center_hz ? formatFreq(entry.center_hz) : "—";
|
||||||
const extraIds = Array.isArray(entry.bookmark_ids) ? entry.bookmark_ids : [];
|
const extraIds = Array.isArray(entry.bookmark_ids) ? entry.bookmark_ids : [];
|
||||||
|
|||||||
@@ -89,6 +89,12 @@ pub struct ScheduleEntry {
|
|||||||
/// Whether to auto-record audio when this entry is active.
|
/// Whether to auto-record audio when this entry is active.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub record: bool,
|
pub record: bool,
|
||||||
|
/// When `true`, this entry is never interleaved with other overlapping
|
||||||
|
/// entries. While this entry's time window is active the scheduler stays
|
||||||
|
/// on its bookmark until the window ends. Useful for WEFAX and satellite
|
||||||
|
/// passes where switching away mid-reception would lose data.
|
||||||
|
#[serde(default)]
|
||||||
|
pub exclusive: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -490,6 +496,12 @@ fn timespan_active_entry(
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If any active entry is exclusive, it wins outright (first exclusive
|
||||||
|
// entry in schedule order takes priority).
|
||||||
|
if let Some(excl) = active.iter().find(|e| e.exclusive) {
|
||||||
|
return Some(excl);
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(idx) = timespan_cycle_slot(&active, now_min, default_interleave) {
|
if let Some(idx) = timespan_cycle_slot(&active, now_min, default_interleave) {
|
||||||
return Some(active[idx]);
|
return Some(active[idx]);
|
||||||
}
|
}
|
||||||
@@ -1500,6 +1512,7 @@ mod tests {
|
|||||||
center_hz,
|
center_hz,
|
||||||
bookmark_ids: Vec::new(),
|
bookmark_ids: Vec::new(),
|
||||||
record: false,
|
record: false,
|
||||||
|
exclusive: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1553,4 +1566,20 @@ mod tests {
|
|||||||
let active = timespan_active_entry(&entries, 10.0, None).expect("active entry");
|
let active = timespan_active_entry(&entries, 10.0, None).expect("active entry");
|
||||||
assert_eq!(active.id, "slot-b");
|
assert_eq!(active.id, "slot-b");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exclusive_entry_wins_over_interleaved() {
|
||||||
|
let mut entries = vec![
|
||||||
|
entry("ft8", 0, 0, "bm-ft8", None, Some(10)),
|
||||||
|
entry("wefax", 0, 0, "bm-wefax", None, Some(10)),
|
||||||
|
];
|
||||||
|
// Without exclusive, interleaving picks slot-b at minute 15.
|
||||||
|
let active = timespan_active_entry(&entries, 15.0, None).expect("active entry");
|
||||||
|
assert_eq!(active.id, "wefax");
|
||||||
|
|
||||||
|
// Mark wefax as exclusive — it should always win regardless of cycle.
|
||||||
|
entries[1].exclusive = true;
|
||||||
|
let active = timespan_active_entry(&entries, 5.0, None).expect("active entry");
|
||||||
|
assert_eq!(active.id, "wefax", "exclusive entry should win at any time");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user