[feat](trx-frontend-http): add interleave time for overlapping TimeSpan entries
When multiple time-span entries are active simultaneously, the scheduler now cycles through them by slot: slot = floor(utc_min / interleave_min) % count. The interleave_min field is optional (null disables, first match wins). UI: "Interleave time (min)" number input in the TimeSpan section with a hint explaining the behaviour. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -716,6 +716,12 @@
|
||||
<!-- Time Span section -->
|
||||
<div id="scheduler-timespan-section" class="sch-section" style="display:none;">
|
||||
<div class="sch-section-title">Time Span Entries (UTC)</div>
|
||||
<div class="sch-row" style="margin-bottom:0.75rem;">
|
||||
<label class="sch-label">Interleave time (min)
|
||||
<input type="number" id="scheduler-ts-interleave" class="status-input" min="1" max="60" placeholder="off" style="width:7rem;" />
|
||||
</label>
|
||||
<small style="color:var(--text-muted);align-self:flex-end;padding-bottom:0.35rem;">When multiple entries overlap, spend this many minutes at each before cycling. Leave blank to disable.</small>
|
||||
</div>
|
||||
<table class="sch-ts-table">
|
||||
<thead>
|
||||
<tr><th>Start</th><th>End</th><th>Bookmark</th><th>Label</th><th></th></tr>
|
||||
|
||||
@@ -195,6 +195,13 @@
|
||||
renderBookmarkSelect("scheduler-gl-night", null);
|
||||
}
|
||||
|
||||
// Interleave input
|
||||
const ilEl = document.getElementById("scheduler-ts-interleave");
|
||||
if (ilEl) {
|
||||
const il = currentConfig && currentConfig.interleave_min;
|
||||
ilEl.value = il ? il : "";
|
||||
}
|
||||
|
||||
// TimeSpan entries
|
||||
renderTimespanEntries();
|
||||
|
||||
@@ -374,6 +381,8 @@
|
||||
} else if (mode === "time_span") {
|
||||
config.entries =
|
||||
currentConfig && currentConfig.entries ? currentConfig.entries : [];
|
||||
const ilVal = parseInt(document.getElementById("scheduler-ts-interleave").value, 10);
|
||||
config.interleave_min = isNaN(ilVal) || ilVal <= 0 ? null : ilVal;
|
||||
}
|
||||
|
||||
const btn = document.getElementById("scheduler-save-btn");
|
||||
|
||||
@@ -78,6 +78,11 @@ pub struct SchedulerConfig {
|
||||
pub grayline: Option<GraylineConfig>,
|
||||
#[serde(default)]
|
||||
pub entries: Vec<ScheduleEntry>,
|
||||
/// When multiple entries are active simultaneously, cycle through them,
|
||||
/// spending this many minutes at each before advancing to the next.
|
||||
/// `None` (or 0) disables interleaving — the first matching entry wins.
|
||||
#[serde(default)]
|
||||
pub interleave_min: Option<u32>,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -265,21 +270,41 @@ fn grayline_bookmark_id(gl: &GraylineConfig, now_min: f64) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
fn timespan_bookmark_id(entries: &[ScheduleEntry], now_min: f64) -> Option<String> {
|
||||
for entry in entries {
|
||||
let start = entry.start_min as f64;
|
||||
let end = entry.end_min as f64;
|
||||
let in_window = if start <= end {
|
||||
now_min >= start && now_min < end
|
||||
} else {
|
||||
// Spans midnight.
|
||||
now_min >= start || now_min < end
|
||||
};
|
||||
if in_window {
|
||||
return Some(entry.bookmark_id.clone());
|
||||
fn entry_is_active(entry: &ScheduleEntry, now_min: f64) -> bool {
|
||||
let start = entry.start_min as f64;
|
||||
let end = entry.end_min as f64;
|
||||
if start <= end {
|
||||
now_min >= start && now_min < end
|
||||
} else {
|
||||
// Spans midnight.
|
||||
now_min >= start || now_min < end
|
||||
}
|
||||
}
|
||||
|
||||
fn timespan_bookmark_id(
|
||||
entries: &[ScheduleEntry],
|
||||
now_min: f64,
|
||||
interleave_min: Option<u32>,
|
||||
) -> Option<String> {
|
||||
let active: Vec<&ScheduleEntry> = entries
|
||||
.iter()
|
||||
.filter(|e| entry_is_active(e, now_min))
|
||||
.collect();
|
||||
|
||||
if active.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// With interleaving and more than one active entry, pick by time slot.
|
||||
if active.len() > 1 {
|
||||
if let Some(step) = interleave_min.filter(|&s| s > 0) {
|
||||
let slot = (now_min as u64 / step as u64) as usize % active.len();
|
||||
return Some(active[slot].bookmark_id.clone());
|
||||
}
|
||||
}
|
||||
None
|
||||
|
||||
// Default: first matching entry wins.
|
||||
Some(active[0].bookmark_id.clone())
|
||||
}
|
||||
|
||||
/// Current UTC time as minutes since midnight.
|
||||
@@ -346,7 +371,7 @@ pub fn spawn_scheduler_task(
|
||||
.as_ref()
|
||||
.and_then(|gl| grayline_bookmark_id(gl, now_min)),
|
||||
SchedulerMode::TimeSpan => {
|
||||
timespan_bookmark_id(&config.entries, now_min)
|
||||
timespan_bookmark_id(&config.entries, now_min, config.interleave_min)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -459,6 +484,7 @@ pub async fn get_scheduler(
|
||||
mode: SchedulerMode::Disabled,
|
||||
grayline: None,
|
||||
entries: vec![],
|
||||
interleave_min: None,
|
||||
});
|
||||
HttpResponse::Ok().json(config)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user