[feat](trx-client): redesign scheduler and background decode UX

Visual 24h timeline bar, inline entry editor, interleave progress ring, filterable checkbox list for bookmarks, status cards moved to top, SVG dot state badges.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-29 22:55:12 +02:00
parent c041ac83f3
commit 41a53b3376
4 changed files with 433 additions and 132 deletions
@@ -344,7 +344,17 @@
<button id="scheduler-next-btn" type="button">Next Entry</button>
</div>
<div id="scheduler-release-status" class="scheduler-release-status">Scheduler is controlling the rig.</div>
<div id="scheduler-cycle-status" class="scheduler-cycle-status">Interleaving: --</div>
<div id="scheduler-cycle-status" class="interleave-ring-wrap" style="display:none;">
<svg class="interleave-ring" viewBox="0 0 36 36">
<circle class="interleave-ring-bg" cx="18" cy="18" r="15.915" />
<circle class="interleave-ring-fill" id="interleave-ring-fill" cx="18" cy="18" r="15.915"
stroke-dasharray="100" stroke-dashoffset="100" />
</svg>
<div class="interleave-ring-text">
<div class="interleave-ring-label" id="interleave-active-name">--</div>
<div class="interleave-ring-sub" id="interleave-countdown">--</div>
</div>
</div>
</div>
</div>
</div>
@@ -986,6 +996,10 @@
<div id="subtab-settings-scheduler" class="sub-tab-panel">
<div id="scheduler-panel" class="sch-panel">
<div class="sch-toast" id="scheduler-toast" style="display:none;"></div>
<!-- Now Playing status card (moved to top) -->
<div class="now-playing-card">
<div id="scheduler-status-card" class="sch-status-card">No activity yet.</div>
</div>
<div class="sch-row">
<label class="sch-label">Mode
<select id="scheduler-mode-select" class="status-input" aria-label="Scheduler mode">
@@ -1035,13 +1049,53 @@
</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>
<div id="scheduler-ts-timeline" class="sch-timeline-wrap"></div>
<button id="scheduler-ts-add-btn" class="sch-write" type="button" style="margin-bottom:0.75rem;">+ Add Entry</button>
<div id="sch-entry-form-wrap" style="display:none;">
<form id="sch-entry-form" class="bm-form">
<div class="bm-form-title" id="sch-entry-form-title">Add Entry</div>
<div class="bm-form-grid">
<label class="bm-label">Start (UTC)
<input type="time" id="scheduler-ts-start" class="status-input" title="Set both to 00:00 for all-day" />
</label>
<label class="bm-label">End (UTC)
<input type="time" id="scheduler-ts-end" class="status-input" title="Set both to 00:00 for all-day" />
</label>
<label class="bm-label" id="scheduler-ts-center-hz-wrap" title="SDR only — sets center frequency before tuning">Center freq (Hz, SDR)
<input type="number" id="scheduler-ts-center-hz" class="status-input" min="0" placeholder="optional" />
</label>
<label class="bm-label bm-label-wide">Primary bookmark
<select id="scheduler-ts-bookmark" class="status-input" aria-label="Entry bookmark"></select>
</label>
<label class="bm-label">Extra channels (virtual)
<div id="scheduler-ts-extra-bm-list" class="sch-extra-bm-list"></div>
<div style="display:flex;gap:0.4rem;margin-top:0.3rem;">
<select id="scheduler-ts-extra-bm-pick" class="status-input" aria-label="Extra bookmark"></select>
<button id="scheduler-ts-extra-bm-add" type="button" class="sch-write" style="padding:0 0.7rem;">+</button>
</div>
</label>
<label class="bm-label">Label (optional)
<input type="text" id="scheduler-ts-label" class="status-input" placeholder="e.g. 40m FT8" />
</label>
<label class="bm-label">Interleave (min, optional)
<input type="number" id="scheduler-ts-entry-interleave" class="status-input" min="1" max="60" placeholder="default" />
</label>
</div>
<div class="bm-form-actions">
<button type="submit" class="bm-save-btn">Save</button>
<button type="button" id="sch-entry-form-cancel">Cancel</button>
</div>
</form>
</div>
<details class="sch-ts-details">
<summary>Entry details</summary>
<table class="sch-ts-table">
<thead>
<tr><th>Start</th><th>End</th><th>Center freq</th><th>Primary bookmark</th><th>Extra channels</th><th>Label</th><th>Interleave (min)</th><th></th></tr>
</thead>
<tbody id="scheduler-ts-tbody"></tbody>
</table>
</details>
</div>
<!-- Satellite Overlay section -->
@@ -1111,59 +1165,21 @@
</form>
</div>
<div id="sch-entry-form-wrap" style="display:none;">
<form id="sch-entry-form" class="bm-form">
<div class="bm-form-title" id="sch-entry-form-title">Add Entry</div>
<div class="bm-form-grid">
<label class="bm-label">Start (UTC)
<input type="time" id="scheduler-ts-start" class="status-input" title="Set both to 00:00 for all-day" />
</label>
<label class="bm-label">End (UTC)
<input type="time" id="scheduler-ts-end" class="status-input" title="Set both to 00:00 for all-day" />
</label>
<label class="bm-label" id="scheduler-ts-center-hz-wrap" title="SDR only — sets center frequency before tuning">Center freq (Hz, SDR)
<input type="number" id="scheduler-ts-center-hz" class="status-input" min="0" placeholder="optional" />
</label>
<label class="bm-label bm-label-wide">Primary bookmark
<select id="scheduler-ts-bookmark" class="status-input" aria-label="Entry bookmark"></select>
</label>
<label class="bm-label">Extra channels (virtual)
<div id="scheduler-ts-extra-bm-list" class="sch-extra-bm-list"></div>
<div style="display:flex;gap:0.4rem;margin-top:0.3rem;">
<select id="scheduler-ts-extra-bm-pick" class="status-input" aria-label="Extra bookmark"></select>
<button id="scheduler-ts-extra-bm-add" type="button" class="sch-write" style="padding:0 0.7rem;">+</button>
</div>
</label>
<label class="bm-label">Label (optional)
<input type="text" id="scheduler-ts-label" class="status-input" placeholder="e.g. 40m FT8" />
</label>
<label class="bm-label">Interleave (min, optional)
<input type="number" id="scheduler-ts-entry-interleave" class="status-input" min="1" max="60" placeholder="default" />
</label>
</div>
<div class="bm-form-actions">
<button type="submit" class="bm-save-btn">Save</button>
<button type="button" id="sch-entry-form-cancel">Cancel</button>
</div>
</form>
</div>
<!-- Actions -->
<div class="sch-actions">
<button id="scheduler-save-btn" class="sch-write sch-save-btn" type="button" style="display:none;">Save</button>
<button id="scheduler-reset-btn" class="sch-write sch-reset-btn" type="button" style="display:none;">Reset to Disabled</button>
</div>
<!-- Status -->
<div class="sch-section">
<div class="sch-section-title">Last Activity</div>
<div id="scheduler-status-card" class="sch-status-card">No activity yet.</div>
</div>
</div>
</div>
<div id="subtab-settings-background-decode" class="sub-tab-panel" style="display:none;">
<div id="background-decode-panel" class="sch-panel">
<div class="sch-toast" id="background-decode-toast" style="display:none;"></div>
<!-- Now Playing status card (moved to top) -->
<div class="now-playing-card">
<div id="background-decode-status-card" class="sch-status-card">No background decode bookmarks configured.</div>
</div>
<div class="sch-section">
<div class="sch-section-title">Configuration</div>
<div class="sch-row">
@@ -1174,26 +1190,18 @@
</span>
</label>
</div>
<div class="sch-row">
<label class="sch-label bgd-bookmark-pick">Bookmark
<div class="bgd-add-row">
<select id="background-decode-bookmark-pick" class="status-input" aria-label="Background decode bookmark"></select>
<button id="background-decode-bookmark-add" type="button" class="sch-write">+ Add</button>
</div>
<div class="sch-row" style="flex-direction:column;gap:0.5rem;">
<label class="sch-label" style="min-width:100%;">Bookmarks
<input type="text" id="bgd-bookmark-filter" class="bgd-checklist-filter" placeholder="Filter bookmarks..." />
</label>
<div id="bgd-bookmark-checklist" class="bgd-checklist"></div>
</div>
<div id="background-decode-bookmark-list" class="sch-extra-bm-list bgd-bookmark-list"></div>
</div>
<div class="sch-actions">
<button id="background-decode-save-btn" class="sch-write sch-save-btn" type="button" style="display:none;">Save</button>
<button id="background-decode-reset-btn" class="sch-write sch-reset-btn" type="button" style="display:none;">Reset</button>
</div>
<div class="sch-section">
<div class="sch-section-title">Runtime Status</div>
<div id="background-decode-status-card" class="sch-status-card">No background decode bookmarks configured.</div>
</div>
</div>
</div>
<div id="subtab-settings-history" class="sub-tab-panel" style="display:none;">
@@ -76,7 +76,6 @@
.then(function ([config, bookmarks]) {
currentConfig = config || { remote: rigId, enabled: false, bookmark_ids: [] };
bookmarkList = Array.isArray(bookmarks) ? bookmarks : [];
renderBookmarkPick();
renderBackgroundDecode();
pollBackgroundDecodeStatus();
})
@@ -105,26 +104,12 @@
return supported;
}
function renderBookmarkPick() {
const sel = document.getElementById("background-decode-bookmark-pick");
if (!sel) return;
const selectedIds = new Set(currentConfig && Array.isArray(currentConfig.bookmark_ids) ? currentConfig.bookmark_ids : []);
sel.innerHTML = '<option value="">- select bookmark -</option>';
supportedBookmarks().forEach(function (bookmark) {
if (selectedIds.has(bookmark.id)) return;
const opt = document.createElement("option");
opt.value = bookmark.id;
opt.textContent = bookmark.name + " (" + formatFreq(bookmark.freq_hz) + " " + bookmark.mode + ")";
sel.appendChild(opt);
});
}
function renderBackgroundDecode() {
if (!currentConfig) {
currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] };
}
setCheckbox("background-decode-enabled", !!currentConfig.enabled);
renderBookmarkList();
renderBookmarkChecklist();
const isControl = backgroundDecodeRole === "control" || (typeof authEnabled !== "undefined" && !authEnabled);
const panel = document.getElementById("background-decode-panel");
@@ -139,52 +124,57 @@
if (resetBtn) resetBtn.style.display = isControl ? "" : "none";
}
function renderBookmarkList() {
const container = document.getElementById("background-decode-bookmark-list");
function renderBookmarkChecklist(filterText) {
const container = document.getElementById("bgd-bookmark-checklist");
if (!container) return;
container.innerHTML = "";
const ids = currentConfig && Array.isArray(currentConfig.bookmark_ids) ? currentConfig.bookmark_ids : [];
if (!ids.length) {
container.textContent = "No background decode bookmarks selected.";
const selectedIds = new Set(
currentConfig && Array.isArray(currentConfig.bookmark_ids) ? currentConfig.bookmark_ids : []
);
const all = supportedBookmarks();
const filter = (filterText || "").trim().toLowerCase();
const filtered = filter
? all.filter(function (bm) {
var text = (bm.name + " " + formatFreq(bm.freq_hz) + " " + bm.mode).toLowerCase();
return text.indexOf(filter) >= 0;
})
: all;
if (filtered.length === 0) {
container.innerHTML = '<div class="bgd-checklist-empty">' +
(all.length === 0 ? "No supported bookmarks available." : "No bookmarks match filter.") +
'</div>';
return;
}
ids.forEach(function (id) {
const bookmark = bookmarkList.find(function (item) { return item.id === id; });
const chip = document.createElement("div");
chip.className = "sch-extra-bm-tag bgd-bookmark-tag";
const decoders = bookmarkDecoderKinds(bookmark);
chip.innerHTML =
'<span>' + escHtml(bookmark ? bookmark.name : id) + '</span>' +
'<span class="bgd-bookmark-meta">' + escHtml(bookmark ? (formatFreq(bookmark.freq_hz) + " " + bookmark.mode + " · " + decoders.join("/").toUpperCase()) : "Missing bookmark") + '</span>';
const btn = document.createElement("span");
btn.className = "sch-extra-bm-rm";
btn.textContent = "×";
btn.addEventListener("click", function () {
removeBookmark(id);
filtered.forEach(function (bookmark) {
var row = document.createElement("label");
row.className = "bgd-checklist-row";
var decoders = bookmarkDecoderKinds(bookmark);
var checked = selectedIds.has(bookmark.id) ? " checked" : "";
row.innerHTML =
'<input type="checkbox"' + checked + ' data-bm-id="' + escHtml(bookmark.id) + '" />' +
'<span class="bgd-checklist-name">' + escHtml(bookmark.name) + '</span>' +
'<span class="bgd-checklist-meta">' + escHtml(formatFreq(bookmark.freq_hz) + " " + bookmark.mode + " · " + decoders.join("/").toUpperCase()) + '</span>';
row.querySelector("input").addEventListener("change", function (e) {
onChecklistToggle(bookmark.id, e.target.checked);
});
chip.appendChild(btn);
container.appendChild(chip);
container.appendChild(row);
});
}
function removeBookmark(id) {
if (!currentConfig || !Array.isArray(currentConfig.bookmark_ids)) return;
currentConfig.bookmark_ids = currentConfig.bookmark_ids.filter(function (item) { return item !== id; });
renderBookmarkPick();
renderBackgroundDecode();
}
function addBookmark() {
const sel = document.getElementById("background-decode-bookmark-pick");
if (!sel || !sel.value) return;
function onChecklistToggle(bookmarkId, checked) {
if (!currentConfig) {
currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] };
}
if (!Array.isArray(currentConfig.bookmark_ids)) currentConfig.bookmark_ids = [];
if (!currentConfig.bookmark_ids.includes(sel.value)) currentConfig.bookmark_ids.push(sel.value);
sel.value = "";
renderBookmarkPick();
renderBackgroundDecode();
if (checked && !currentConfig.bookmark_ids.includes(bookmarkId)) {
currentConfig.bookmark_ids.push(bookmarkId);
} else if (!checked) {
currentConfig.bookmark_ids = currentConfig.bookmark_ids.filter(function (id) { return id !== bookmarkId; });
}
}
function saveBackgroundDecode() {
@@ -200,7 +190,6 @@
apiPutConfig(rigId, payload)
.then(function (saved) {
currentConfig = saved;
renderBookmarkPick();
renderBackgroundDecode();
pollBackgroundDecodeStatus();
showToast("Background decode saved.");
@@ -219,7 +208,6 @@
apiResetConfig(rigId)
.then(function (saved) {
currentConfig = saved;
renderBookmarkPick();
renderBackgroundDecode();
pollBackgroundDecodeStatus();
showToast("Background decode reset.");
@@ -273,7 +261,9 @@
'<div class="bgd-status-name">' + escHtml(name) + '</div>' +
'<div class="bgd-status-meta">' + escHtml(parts.join(" · ")) + '</div>' +
'</div>' +
'<div class="bgd-status-state" data-state="' + escHtml(entry.state || "inactive") + '">' + escHtml(prettyState(entry.state)) + '</div>' +
'<div class="bgd-status-state" data-state="' + escHtml(entry.state || "inactive") + '">' +
'<svg class="bgd-state-dot" viewBox="0 0 8 8"><circle cx="4" cy="4" r="3.5"/></svg>' +
escHtml(prettyState(entry.state)) + '</div>' +
'</div>';
});
html += "</div>";
@@ -328,10 +318,12 @@
}
function wireBackgroundDecodeEvents() {
const addBtn = document.getElementById("background-decode-bookmark-add");
if (addBtn && !addBtn._wired) {
addBtn._wired = true;
addBtn.addEventListener("click", addBookmark);
const filterInput = document.getElementById("bgd-bookmark-filter");
if (filterInput && !filterInput._wired) {
filterInput._wired = true;
filterInput.addEventListener("input", function () {
renderBookmarkChecklist(filterInput.value);
});
}
const saveBtn = document.getElementById("background-decode-save-btn");
@@ -275,17 +275,36 @@
};
}
function schedulerInterleaveSummary(config) {
const state = schedulerInterleaveState(config);
if (state.activeEntries.length <= 1 || !(state.cycleMin > 0)) return "Interleaving: off";
const activeName = schedulerEntryDisplayName(state.activeEntries[state.currentIndex]);
return "Interleaving: " + activeName + " · next switch in " + state.remainingSec + "s (" + state.cycleMin + " min cycle)";
function renderSchedulerInterleaveStatus() {
const wrap = document.getElementById("scheduler-cycle-status");
if (!wrap) return;
const state = schedulerInterleaveState(currentConfig);
const isActive = state.activeEntries.length > 1 && state.cycleMin > 0;
wrap.style.display = isActive ? "" : "none";
if (isActive) {
var activeName = schedulerEntryDisplayName(state.activeEntries[state.currentIndex]);
var totalSlotSec = state.cycleMin > 0
? (state.cycleMin * 60) / state.activeEntries.length
: 0;
var elapsedPct = totalSlotSec > 0
? Math.min(100, Math.max(0, ((totalSlotSec - state.remainingSec) / totalSlotSec) * 100))
: 0;
var ringFill = document.getElementById("interleave-ring-fill");
if (ringFill) ringFill.setAttribute("stroke-dashoffset", String(100 - elapsedPct));
var nameEl = document.getElementById("interleave-active-name");
if (nameEl) nameEl.textContent = activeName;
var countdownEl = document.getElementById("interleave-countdown");
if (countdownEl) countdownEl.textContent = "next in " + state.remainingSec + "s · " + state.cycleMin + "m cycle";
}
function renderSchedulerInterleaveStatus() {
const el = document.getElementById("scheduler-cycle-status");
if (!el) return;
el.textContent = schedulerInterleaveSummary(currentConfig);
// Also update the timeline needle if visible
renderTimelineNeedle();
renderSchedulerStepControls();
}
@@ -459,7 +478,7 @@
}
// -------------------------------------------------------------------------
// Entry form (modal — mirrors bookmark add/edit modal)
// Entry form (inline card below Add Entry button)
// -------------------------------------------------------------------------
function schOpenEntryForm(entry, idx) {
schEntryEditIdx = (idx != null) ? idx : null;
@@ -486,7 +505,7 @@
const wrap = document.getElementById("sch-entry-form-wrap");
if (wrap) {
wrap.style.display = "flex";
wrap.style.display = "block";
if (startEl) startEl.focus();
}
}
@@ -552,6 +571,100 @@
renderTimespanEntries();
}
// -------------------------------------------------------------------------
// 24h Timeline Bar
// -------------------------------------------------------------------------
var TIMELINE_COLORS = ["#38bdf8", "#f59e0b", "#a78bfa", "#34d399", "#fb7185", "#60a5fa"];
function renderTimeline() {
var container = document.getElementById("scheduler-ts-timeline");
if (!container) return;
var entries = currentConfig && Array.isArray(currentConfig.entries) ? currentConfig.entries : [];
if (entries.length === 0) {
container.innerHTML = "";
return;
}
var W = 1000;
var H = 62;
var BAR_Y = 6;
var BAR_H = 30;
var TICK_Y = BAR_Y + BAR_H + 2;
var svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' + W + ' ' + H + '" preserveAspectRatio="none">';
// Background bar
svg += '<rect x="0" y="' + BAR_Y + '" width="' + W + '" height="' + BAR_H + '" rx="3" fill="var(--btn-bg)" />';
// Entry segments
entries.forEach(function (entry, idx) {
var start = Number(entry.start_min);
var end = Number(entry.end_min);
if (!Number.isFinite(start) || !Number.isFinite(end)) return;
var color = TIMELINE_COLORS[idx % TIMELINE_COLORS.length];
if (start === end) {
// All-day entry
svg += '<rect class="sch-timeline-seg" x="0" y="' + BAR_Y + '" width="' + W + '" height="' + BAR_H +
'" rx="3" fill="' + color + '" data-idx="' + idx + '" />';
} else if (start < end) {
var x = (start / 1440) * W;
var w = ((end - start) / 1440) * W;
svg += '<rect class="sch-timeline-seg" x="' + x.toFixed(1) + '" y="' + BAR_Y + '" width="' + w.toFixed(1) +
'" height="' + BAR_H + '" fill="' + color + '" data-idx="' + idx + '" />';
} else {
// Wrap-around: two segments
var x1 = (start / 1440) * W;
var w1 = W - x1;
svg += '<rect class="sch-timeline-seg" x="' + x1.toFixed(1) + '" y="' + BAR_Y + '" width="' + w1.toFixed(1) +
'" height="' + BAR_H + '" fill="' + color + '" data-idx="' + idx + '" />';
var w2 = (end / 1440) * W;
svg += '<rect class="sch-timeline-seg" x="0" y="' + BAR_Y + '" width="' + w2.toFixed(1) +
'" height="' + BAR_H + '" fill="' + color + '" data-idx="' + idx + '" />';
}
});
// Tick marks every 3 hours
for (var h = 0; h <= 24; h += 3) {
var tx = (h / 24) * W;
svg += '<line x1="' + tx.toFixed(1) + '" y1="' + TICK_Y + '" x2="' + tx.toFixed(1) + '" y2="' + (TICK_Y + 5) +
'" stroke="var(--border-light)" stroke-width="1" />';
if (h < 24) {
svg += '<text class="sch-timeline-tick-label" x="' + (tx + 3).toFixed(1) + '" y="' + (TICK_Y + 16) +
'">' + String(h).padStart(2, "0") + '</text>';
}
}
// Current time needle
svg += '<g id="sch-timeline-needle-g">' + timelineNeedleSvg() + '</g>';
svg += '</svg>';
container.innerHTML = svg;
// Wire click events on segments
container.querySelectorAll(".sch-timeline-seg").forEach(function (seg) {
seg.addEventListener("click", function () {
var i = parseInt(seg.getAttribute("data-idx"), 10);
var entry = currentConfig && currentConfig.entries ? currentConfig.entries[i] : null;
if (entry) schOpenEntryForm(entry, i);
});
});
}
function timelineNeedleSvg() {
var info = schedulerUtcMinuteInfo();
var nowMin = info.minuteOfDay + (info.secondOfMinute / 60);
var x = (nowMin / 1440) * 1000;
return '<line class="sch-timeline-needle" x1="' + x.toFixed(1) + '" y1="2" x2="' + x.toFixed(1) + '" y2="38" />' +
'<polygon class="sch-timeline-needle-head" points="' +
(x - 3).toFixed(1) + ',2 ' + (x + 3).toFixed(1) + ',2 ' + x.toFixed(1) + ',6" />';
}
function renderTimelineNeedle() {
var g = document.getElementById("sch-timeline-needle-g");
if (g) g.innerHTML = timelineNeedleSvg();
}
// -------------------------------------------------------------------------
// TimeSpan entries table
// -------------------------------------------------------------------------
@@ -598,6 +711,7 @@
removeEntry(parseInt(btn.dataset.idx, 10));
});
});
renderTimeline();
}
function bmName(id) {
@@ -3814,8 +3814,7 @@ button:focus-visible, input:focus-visible, select:focus-visible {
#sch-entry-form .bm-label {
gap: 0.35rem;
}
#bm-form-wrap,
#sch-entry-form-wrap {
#bm-form-wrap {
position: fixed;
inset: 0;
z-index: 120;
@@ -4539,6 +4538,187 @@ button:focus-visible, input:focus-visible, select:focus-visible {
.bgd-status-state[data-state="no_supported_decoders"] {
color: var(--accent-red);
}
/* ── "Now Playing" status card (top of scheduler & bgd panels) ──── */
.now-playing-card {
border-left: 3px solid var(--accent-green);
padding: 0.75rem 1rem;
margin-bottom: 1.25rem;
background: var(--bg-secondary);
border-radius: 0 0.5rem 0.5rem 0;
font-size: 0.9rem;
}
/* ── 24h Timeline Bar ─────────────────────────────────────────────── */
.sch-timeline-wrap {
margin-bottom: 1rem;
overflow: hidden;
border-radius: 0.4rem;
border: 1px solid var(--border-light);
background: var(--bg-secondary);
}
.sch-timeline-wrap svg {
display: block;
width: 100%;
height: 62px;
}
.sch-timeline-seg {
cursor: pointer;
opacity: 0.82;
transition: opacity 0.12s;
}
.sch-timeline-seg:hover {
opacity: 1;
}
.sch-timeline-tick-label {
fill: var(--text-muted);
font-size: 9px;
font-family: inherit;
}
.sch-timeline-needle {
stroke: var(--accent-red);
stroke-width: 1.5;
}
.sch-timeline-needle-head {
fill: var(--accent-red);
}
/* Details toggle for entry table */
.sch-ts-details {
margin-top: 0.5rem;
}
.sch-ts-details summary {
cursor: pointer;
font-size: 0.82rem;
color: var(--text-muted);
font-weight: 600;
padding: 0.3rem 0;
user-select: none;
}
/* ── Inline Entry Editor (replaces modal overlay) ─────────────────── */
#sch-entry-form-wrap {
position: static;
z-index: auto;
display: none;
align-items: stretch;
justify-content: stretch;
background: none;
margin-bottom: 1rem;
}
#sch-entry-form-wrap .bm-form {
max-width: 100%;
width: 100%;
border: 1px solid var(--border-light);
border-radius: 0.5rem;
background: var(--bg-secondary);
}
/* ── Interleave Progress Ring ─────────────────────────────────────── */
.interleave-ring-wrap {
display: flex;
align-items: center;
gap: 0.5rem;
}
.interleave-ring {
width: 32px;
height: 32px;
transform: rotate(-90deg);
flex-shrink: 0;
}
.interleave-ring-bg {
fill: none;
stroke: var(--border-light);
stroke-width: 3;
}
.interleave-ring-fill {
fill: none;
stroke: var(--accent-green);
stroke-width: 3;
stroke-linecap: round;
transition: stroke-dashoffset 0.9s linear;
}
.interleave-ring-text {
display: flex;
flex-direction: column;
gap: 0;
min-width: 0;
}
.interleave-ring-label {
font-size: 0.78rem;
font-weight: 600;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 14rem;
}
.interleave-ring-sub {
font-size: 0.7rem;
color: var(--text-muted);
opacity: 0.88;
}
/* ── Background Decode Checkbox List ──────────────────────────────── */
.bgd-checklist-filter {
width: 100%;
margin-bottom: 0.5rem;
padding: 0.4rem 0.6rem;
font-size: 0.85rem;
background: var(--input-bg);
border: 1px solid var(--border);
border-radius: var(--radius, 4px);
color: var(--text);
}
.bgd-checklist-filter::placeholder {
color: var(--text-muted);
opacity: 0.7;
}
.bgd-checklist {
max-height: 16rem;
overflow-y: auto;
border: 1px solid var(--border-light);
border-radius: 0.4rem;
background: var(--bg-secondary);
}
.bgd-checklist-row {
display: flex;
align-items: center;
gap: 0.55rem;
padding: 0.45rem 0.65rem;
border-bottom: 1px solid var(--border-light);
cursor: pointer;
font-size: 0.85rem;
transition: background 0.1s;
}
.bgd-checklist-row:last-child {
border-bottom: none;
}
.bgd-checklist-row:hover {
background: color-mix(in srgb, var(--card-bg) 60%, transparent);
}
.bgd-checklist-row input[type="checkbox"] {
flex-shrink: 0;
}
.bgd-checklist-name {
font-weight: 600;
color: var(--text);
}
.bgd-checklist-meta {
color: var(--text-muted);
font-size: 0.78rem;
margin-left: auto;
white-space: nowrap;
}
.bgd-checklist-empty {
padding: 0.75rem;
text-align: center;
color: var(--text-muted);
font-size: 0.85rem;
}
/* ── SVG State Dot Badges ─────────────────────────────────────────── */
.bgd-state-dot {
width: 8px;
height: 8px;
display: inline-block;
vertical-align: middle;
margin-right: 4px;
fill: currentColor;
}
@media (max-width: 600px) {
.channel-scheduler-controls {
flex-direction: column;
@@ -4587,6 +4767,13 @@ button:focus-visible, input:focus-visible, select:focus-visible {
.bgd-add-row button {
width: 100%;
}
.interleave-ring-label {
max-width: 8rem;
}
.bgd-checklist-meta {
margin-left: 0;
font-size: 0.72rem;
}
}
/* ── SAT panel ──────────────────────────────────────────────────────── */
.sat-view-bar { display: flex; gap: 0; margin-bottom: 0.75rem; border-bottom: 1px solid var(--border); }