[feat](trx-frontend-http): per-entry interleave time in TimeSpan scheduler

Each ScheduleEntry can now carry its own interleave_min, overriding the
config-level default for that slot in the cycle.  The cycle length is the
sum of all active entries' effective durations (weighted), so entries with
longer individual interleave times occupy proportionally more time.

UI: "Interleave (min, optional)" input in the add-entry form; value shown
in the entries table (displays "—" when using the config default).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-10 23:35:52 +01:00
parent 998c6ad0e6
commit e4cfd35282
3 changed files with 35 additions and 7 deletions
@@ -724,7 +724,7 @@
</div> </div>
<table class="sch-ts-table"> <table class="sch-ts-table">
<thead> <thead>
<tr><th>Start</th><th>End</th><th>Bookmark</th><th>Label</th><th></th></tr> <tr><th>Start</th><th>End</th><th>Bookmark</th><th>Label</th><th>Interleave (min)</th><th></th></tr>
</thead> </thead>
<tbody id="scheduler-ts-tbody"></tbody> <tbody id="scheduler-ts-tbody"></tbody>
</table> </table>
@@ -741,6 +741,9 @@
<label class="sch-label">Label (optional) <label class="sch-label">Label (optional)
<input type="text" id="scheduler-ts-label" class="status-input" placeholder="e.g. 40m FT8" /> <input type="text" id="scheduler-ts-label" class="status-input" placeholder="e.g. 40m FT8" />
</label> </label>
<label class="sch-label">Interleave (min, optional)
<input type="number" id="scheduler-ts-entry-interleave" class="status-input" min="1" max="60" placeholder="default" style="width:6rem;" />
</label>
<button id="scheduler-ts-add-btn" class="sch-write" type="button">+ Add</button> <button id="scheduler-ts-add-btn" class="sch-write" type="button">+ Add</button>
</div> </div>
</div> </div>
@@ -277,11 +277,13 @@
: []; : [];
entries.forEach(function (entry, idx) { entries.forEach(function (entry, idx) {
const tr = document.createElement("tr"); const tr = document.createElement("tr");
const il = entry.interleave_min ? String(entry.interleave_min) + " min" : "—";
tr.innerHTML = tr.innerHTML =
'<td>' + minToHHMM(entry.start_min) + '</td>' + '<td>' + minToHHMM(entry.start_min) + '</td>' +
'<td>' + minToHHMM(entry.end_min) + '</td>' + '<td>' + minToHHMM(entry.end_min) + '</td>' +
'<td>' + bmName(entry.bookmark_id) + '</td>' + '<td>' + bmName(entry.bookmark_id) + '</td>' +
'<td>' + escHtml(entry.label || "") + '</td>' + '<td>' + escHtml(entry.label || "") + '</td>' +
'<td>' + il + '</td>' +
'<td><button class="sch-write sch-remove-btn" data-idx="' + idx + '" type="button">Remove</button></td>'; '<td><button class="sch-write sch-remove-btn" data-idx="' + idx + '" type="button">Remove</button></td>';
tbody.appendChild(tr); tbody.appendChild(tr);
}); });
@@ -330,12 +332,15 @@
const endEl = document.getElementById("scheduler-ts-end"); const endEl = document.getElementById("scheduler-ts-end");
const bmEl = document.getElementById("scheduler-ts-bookmark"); const bmEl = document.getElementById("scheduler-ts-bookmark");
const labelEl = document.getElementById("scheduler-ts-label"); const labelEl = document.getElementById("scheduler-ts-label");
const ilEl = document.getElementById("scheduler-ts-entry-interleave");
if (!startEl || !endEl || !bmEl) return; if (!startEl || !endEl || !bmEl) return;
const startMin = hhmmToMin(startEl.value); const startMin = hhmmToMin(startEl.value);
const endMin = hhmmToMin(endEl.value); const endMin = hhmmToMin(endEl.value);
const bmId = bmEl.value; const bmId = bmEl.value;
const label = labelEl ? labelEl.value.trim() : ""; const label = labelEl ? labelEl.value.trim() : "";
const ilVal = ilEl ? parseInt(ilEl.value, 10) : NaN;
const entryInterleave = !isNaN(ilVal) && ilVal > 0 ? ilVal : null;
if (!bmId) { if (!bmId) {
alert("Please select a bookmark."); alert("Please select a bookmark.");
@@ -354,12 +359,14 @@
end_min: endMin, end_min: endMin,
bookmark_id: bmId, bookmark_id: bmId,
label: label || null, label: label || null,
interleave_min: entryInterleave,
}); });
startEl.value = ""; startEl.value = "";
endEl.value = ""; endEl.value = "";
bmEl.value = ""; // reset select to first option bmEl.value = "";
if (labelEl) labelEl.value = ""; if (labelEl) labelEl.value = "";
if (ilEl) ilEl.value = "";
renderTimespanEntries(); renderTimespanEntries();
} }
@@ -68,6 +68,11 @@ pub struct ScheduleEntry {
pub bookmark_id: String, pub bookmark_id: String,
#[serde(default)] #[serde(default)]
pub label: Option<String>, pub label: Option<String>,
/// Per-entry interleave duration in minutes. Overrides the config-level
/// `interleave_min` when set. Allows each entry to occupy a differently
/// sized slice of the interleave cycle.
#[serde(default)]
pub interleave_min: Option<u32>,
} }
#[derive(Debug, Clone, Serialize, Deserialize, Default)] #[derive(Debug, Clone, Serialize, Deserialize, Default)]
@@ -284,7 +289,7 @@ fn entry_is_active(entry: &ScheduleEntry, now_min: f64) -> bool {
fn timespan_bookmark_id( fn timespan_bookmark_id(
entries: &[ScheduleEntry], entries: &[ScheduleEntry],
now_min: f64, now_min: f64,
interleave_min: Option<u32>, default_interleave: Option<u32>,
) -> Option<String> { ) -> Option<String> {
let active: Vec<&ScheduleEntry> = entries let active: Vec<&ScheduleEntry> = entries
.iter() .iter()
@@ -295,11 +300,24 @@ fn timespan_bookmark_id(
return None; return None;
} }
// With interleaving and more than one active entry, pick by time slot. // With interleaving and more than one active entry, use a weighted cycle.
// Each entry's effective duration is its own interleave_min, falling back
// to the config-level default. The cycle length is the sum of all durations.
if active.len() > 1 { if active.len() > 1 {
if let Some(step) = interleave_min.filter(|&s| s > 0) { let durations: Vec<u32> = active
let slot = (now_min as u64 / step as u64) as usize % active.len(); .iter()
return Some(active[slot].bookmark_id.clone()); .map(|e| e.interleave_min.or(default_interleave).unwrap_or(0))
.collect();
let cycle: u32 = durations.iter().sum();
if cycle > 0 {
let pos = (now_min as u64) % (cycle as u64);
let mut cum = 0u64;
for (entry, &dur) in active.iter().zip(durations.iter()) {
cum += dur as u64;
if pos < cum {
return Some(entry.bookmark_id.clone());
}
}
} }
} }