[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 -->
|
<!-- Time Span section -->
|
||||||
<div id="scheduler-timespan-section" class="sch-section" style="display:none;">
|
<div id="scheduler-timespan-section" class="sch-section" style="display:none;">
|
||||||
<div class="sch-section-title">Time Span Entries (UTC)</div>
|
<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">
|
<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></th></tr>
|
||||||
|
|||||||
@@ -195,6 +195,13 @@
|
|||||||
renderBookmarkSelect("scheduler-gl-night", null);
|
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
|
// TimeSpan entries
|
||||||
renderTimespanEntries();
|
renderTimespanEntries();
|
||||||
|
|
||||||
@@ -374,6 +381,8 @@
|
|||||||
} else if (mode === "time_span") {
|
} else if (mode === "time_span") {
|
||||||
config.entries =
|
config.entries =
|
||||||
currentConfig && currentConfig.entries ? currentConfig.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");
|
const btn = document.getElementById("scheduler-save-btn");
|
||||||
|
|||||||
@@ -78,6 +78,11 @@ pub struct SchedulerConfig {
|
|||||||
pub grayline: Option<GraylineConfig>,
|
pub grayline: Option<GraylineConfig>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub entries: Vec<ScheduleEntry>,
|
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> {
|
fn entry_is_active(entry: &ScheduleEntry, now_min: f64) -> bool {
|
||||||
for entry in entries {
|
let start = entry.start_min as f64;
|
||||||
let start = entry.start_min as f64;
|
let end = entry.end_min as f64;
|
||||||
let end = entry.end_min as f64;
|
if start <= end {
|
||||||
let in_window = if start <= end {
|
now_min >= start && now_min < end
|
||||||
now_min >= start && now_min < end
|
} else {
|
||||||
} else {
|
// Spans midnight.
|
||||||
// Spans midnight.
|
now_min >= start || now_min < end
|
||||||
now_min >= start || now_min < end
|
}
|
||||||
};
|
}
|
||||||
if in_window {
|
|
||||||
return Some(entry.bookmark_id.clone());
|
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.
|
/// Current UTC time as minutes since midnight.
|
||||||
@@ -346,7 +371,7 @@ pub fn spawn_scheduler_task(
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|gl| grayline_bookmark_id(gl, now_min)),
|
.and_then(|gl| grayline_bookmark_id(gl, now_min)),
|
||||||
SchedulerMode::TimeSpan => {
|
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,
|
mode: SchedulerMode::Disabled,
|
||||||
grayline: None,
|
grayline: None,
|
||||||
entries: vec![],
|
entries: vec![],
|
||||||
|
interleave_min: None,
|
||||||
});
|
});
|
||||||
HttpResponse::Ok().json(config)
|
HttpResponse::Ok().json(config)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user