diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/scheduler.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/scheduler.js index 44af2e6..e586f32 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/scheduler.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/scheduler.js @@ -206,6 +206,11 @@ if (active.length === 1) { 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 durations = active.map(function (entry) { const own = Number(entry && entry.interleave_min); @@ -820,7 +825,8 @@ '' + '' + '' + - '' + + '' + + '' + '' + ''; @@ -862,6 +868,16 @@ 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 () { var startEl = tr.querySelector('[data-field="start"]'); var endEl = tr.querySelector('[data-field="end"]'); @@ -869,6 +885,7 @@ var labelEl = tr.querySelector('[data-field="label"]'); var ilEl = tr.querySelector('[data-field="interleave"]'); var recEl = tr.querySelector('[data-field="record"]'); + var exEl = tr.querySelector('[data-field="exclusive"]'); if (bmEl && !bmEl.value) { alert('Please select a bookmark.'); return; } @@ -876,8 +893,9 @@ entry.end_min = hhmmToMin(endEl.value); entry.bookmark_id = bmEl.value; entry.label = labelEl.value.trim() || null; + entry.exclusive = exEl ? exEl.checked : false; 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.record = recEl.checked; @@ -908,7 +926,7 @@ entry.id && String(entry.id) === String(currentSchedulerStatus.last_entry_id)) { 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 centerCell = entry.center_hz ? formatFreq(entry.center_hz) : "—"; const extraIds = Array.isArray(entry.bookmark_ids) ? entry.bookmark_ids : []; diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/scheduler.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/scheduler.rs index 237f861..c7a8941 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/scheduler.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/scheduler.rs @@ -89,6 +89,12 @@ pub struct ScheduleEntry { /// Whether to auto-record audio when this entry is active. #[serde(default)] 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)] @@ -490,6 +496,12 @@ fn timespan_active_entry( 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) { return Some(active[idx]); } @@ -1500,6 +1512,7 @@ mod tests { center_hz, bookmark_ids: Vec::new(), record: false, + exclusive: false, } } @@ -1553,4 +1566,20 @@ mod tests { let active = timespan_active_entry(&entries, 10.0, None).expect("active entry"); 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"); + } }