From 41a53b33762744fc1634b1413b7b0ad6406f033a Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Sun, 29 Mar 2026 22:55:12 +0200 Subject: [PATCH] [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 Signed-off-by: Stan Grams --- .../trx-frontend-http/assets/web/index.html | 130 ++++++------ .../assets/web/plugins/background-decode.js | 106 +++++----- .../assets/web/plugins/scheduler.js | 138 +++++++++++-- .../trx-frontend-http/assets/web/style.css | 191 +++++++++++++++++- 4 files changed, 433 insertions(+), 132 deletions(-) diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html index baf8a2b..ebe42a2 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html @@ -344,7 +344,17 @@
Scheduler is controlling the rig.
-
Interleaving: --
+ @@ -986,6 +996,10 @@
+ +
+
No activity yet.
+
+ + + + + + +
+
+ + +
+ +
+
+ Entry details + + + + + +
StartEndCenter freqPrimary bookmarkExtra channelsLabelInterleave (min)
+
@@ -1111,59 +1165,21 @@ - -
- -
-
Last Activity
-
No activity yet.
-
'; }); html += ""; @@ -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"); 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 fe56d43..a564d5e 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 @@ -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 el = document.getElementById("scheduler-cycle-status"); - if (!el) return; - el.textContent = schedulerInterleaveSummary(currentConfig); + 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"; + } + + // 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 = ''; + + // Background bar + svg += ''; + + // 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 += ''; + } else if (start < end) { + var x = (start / 1440) * W; + var w = ((end - start) / 1440) * W; + svg += ''; + } else { + // Wrap-around: two segments + var x1 = (start / 1440) * W; + var w1 = W - x1; + svg += ''; + var w2 = (end / 1440) * W; + svg += ''; + } + }); + + // Tick marks every 3 hours + for (var h = 0; h <= 24; h += 3) { + var tx = (h / 24) * W; + svg += ''; + if (h < 24) { + svg += '' + String(h).padStart(2, "0") + ''; + } + } + + // Current time needle + svg += '' + timelineNeedleSvg() + ''; + + 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 '' + + ''; + } + + 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) { diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css index e6b7ad1..e676c7b 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css @@ -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); }