[feat](trx-frontend-http): implement Phase 1 UX/UI quick wins from Settings analysis

- IX-2: Add confirm() dialogs before all destructive actions (10 history
  clear buttons, scheduler reset, background decode reset)
- IX-6: Add Select All / Deselect All buttons for background decode
  bookmark checklist
- IX-1: Add dirty-state indicator (pulsing dot) on Save buttons when
  unsaved changes exist in scheduler and background decode panels
- A-4: Add role="alert" and aria-live="polite" to toast notification
  elements for screen reader accessibility
- A-3: Add Unicode symbol prefixes to background decode state labels
  (checkmark/triangle/cross) so state is distinguishable without color

https://claude.ai/code/session_01ShfPMW9hPLD3czp9YovkbJ
Signed-off-by: Claude <noreply@anthropic.com>
This commit is contained in:
Claude
2026-03-30 07:33:45 +00:00
committed by Stan Grams
parent 8ea7bf3b84
commit c85a9c9bc4
14 changed files with 155 additions and 13 deletions
@@ -1074,7 +1074,7 @@
</div>
<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>
<div class="sch-toast" id="scheduler-toast" role="alert" aria-live="polite" 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>
@@ -1254,7 +1254,7 @@
</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>
<div class="sch-toast" id="background-decode-toast" role="alert" aria-live="polite" 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>
@@ -1273,6 +1273,10 @@
<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 class="bgd-select-actions">
<button type="button" id="bgd-select-all-btn" class="bgd-select-btn" aria-label="Select all bookmarks">Select All</button>
<button type="button" id="bgd-deselect-all-btn" class="bgd-select-btn" aria-label="Deselect all bookmarks">Deselect All</button>
</div>
<div id="bgd-bookmark-checklist" class="bgd-checklist"></div>
</div>
</div>
@@ -378,6 +378,7 @@ window.pruneAisHistoryView = function() {
};
document.getElementById("settings-clear-ais-history")?.addEventListener("click", async () => {
if (!confirm("Clear all AIS decode history? This cannot be undone.")) return;
try {
await postPath("/clear_ais_decode");
window.resetAisHistoryView();
@@ -438,6 +438,7 @@ window.restoreAprsHistory = function(packets) {
};
document.getElementById("settings-clear-aprs-history")?.addEventListener("click", async () => {
if (!confirm("Clear all APRS decode history? This cannot be undone.")) return;
try {
await postPath("/clear_aprs_decode");
window.resetAprsHistoryView();
@@ -12,6 +12,7 @@
let currentConfig = null;
let bookmarkList = [];
let statusInterval = null;
let bgdDirty = false;
function initBackgroundDecode(rigId, role) {
backgroundDecodeRole = role;
@@ -77,6 +78,7 @@
currentConfig = config || { remote: rigId, enabled: false, bookmark_ids: [] };
bookmarkList = Array.isArray(bookmarks) ? bookmarks : [];
renderBackgroundDecode();
clearBgdDirty();
pollBackgroundDecodeStatus();
})
.catch(function (err) {
@@ -175,6 +177,7 @@
} else if (!checked) {
currentConfig.bookmark_ids = currentConfig.bookmark_ids.filter(function (id) { return id !== bookmarkId; });
}
markBgdDirty();
}
function saveBackgroundDecode() {
@@ -191,6 +194,7 @@
.then(function (saved) {
currentConfig = saved;
renderBackgroundDecode();
clearBgdDirty();
pollBackgroundDecodeStatus();
showToast("Background decode saved.");
})
@@ -205,10 +209,12 @@
function resetBackgroundDecode() {
const rigId = currentRigId;
if (!rigId) return;
if (!confirm("Reset background decode configuration? This cannot be undone.")) return;
apiResetConfig(rigId)
.then(function (saved) {
currentConfig = saved;
renderBackgroundDecode();
clearBgdDirty();
pollBackgroundDecodeStatus();
showToast("Background decode reset.");
})
@@ -272,17 +278,17 @@
function prettyState(state) {
switch (state) {
case "active": return "Active";
case "out_of_span": return "Out of span";
case "waiting_for_spectrum": return "Waiting";
case "waiting_for_user": return "No user";
case "missing_bookmark": return "Missing";
case "no_supported_decoders": return "Unsupported";
case "disabled": return "Disabled";
case "handled_by_scheduler": return "Scheduler";
case "scheduler_has_control": return "Scheduler";
case "handled_by_virtual_channel": return "VChan";
default: return "Inactive";
case "active": return "\u2713 Active";
case "out_of_span": return "\u25B3 Out of span";
case "waiting_for_spectrum": return "\u25B3 Waiting";
case "waiting_for_user": return "\u25B3 No user";
case "missing_bookmark": return "\u2717 Missing";
case "no_supported_decoders": return "\u2717 Unsupported";
case "disabled": return "\u25B3 Disabled";
case "handled_by_scheduler": return "\u25B3 Scheduler";
case "scheduler_has_control": return "\u25B3 Scheduler";
case "handled_by_virtual_channel": return "\u25B3 VChan";
default: return "\u25B3 Inactive";
}
}
@@ -306,6 +312,19 @@
.replace(/"/g, "&quot;");
}
function markBgdDirty() {
if (bgdDirty) return;
bgdDirty = true;
var btn = document.getElementById("background-decode-save-btn");
if (btn) btn.classList.add("sch-dirty");
}
function clearBgdDirty() {
bgdDirty = false;
var btn = document.getElementById("background-decode-save-btn");
if (btn) btn.classList.remove("sch-dirty");
}
function showToast(msg, isError) {
const el = document.getElementById("background-decode-toast");
if (!el) return;
@@ -317,6 +336,25 @@
}, 3000);
}
function selectAllBookmarks() {
if (!currentConfig) {
currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] };
}
var ids = supportedBookmarks().map(function (bm) { return bm.id; });
currentConfig.bookmark_ids = ids;
renderBookmarkChecklist(document.getElementById("bgd-bookmark-filter")?.value);
markBgdDirty();
}
function deselectAllBookmarks() {
if (!currentConfig) {
currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] };
}
currentConfig.bookmark_ids = [];
renderBookmarkChecklist(document.getElementById("bgd-bookmark-filter")?.value);
markBgdDirty();
}
function wireBackgroundDecodeEvents() {
const filterInput = document.getElementById("bgd-bookmark-filter");
if (filterInput && !filterInput._wired) {
@@ -326,6 +364,24 @@
});
}
const enabledCb = document.getElementById("background-decode-enabled");
if (enabledCb && !enabledCb._wired) {
enabledCb._wired = true;
enabledCb.addEventListener("change", function () { markBgdDirty(); });
}
const selectAllBtn = document.getElementById("bgd-select-all-btn");
if (selectAllBtn && !selectAllBtn._wired) {
selectAllBtn._wired = true;
selectAllBtn.addEventListener("click", selectAllBookmarks);
}
const deselectAllBtn = document.getElementById("bgd-deselect-all-btn");
if (deselectAllBtn && !deselectAllBtn._wired) {
deselectAllBtn._wired = true;
deselectAllBtn.addEventListener("click", deselectAllBookmarks);
}
const saveBtn = document.getElementById("background-decode-save-btn");
if (saveBtn && !saveBtn._wired) {
saveBtn._wired = true;
@@ -360,6 +360,7 @@ window.resetCwHistoryView = function() {
};
document.getElementById("settings-clear-cw-history")?.addEventListener("click", async () => {
if (!confirm("Clear all CW decode history? This cannot be undone.")) return;
try {
await postPath("/clear_cw_decode");
window.resetCwHistoryView();
@@ -190,6 +190,7 @@ ft2DecodeToggleBtn?.addEventListener("click", async () => {
});
document.getElementById("settings-clear-ft2-history")?.addEventListener("click", async () => {
if (!confirm("Clear all FT2 decode history? This cannot be undone.")) return;
try {
await postPath("/clear_ft2_decode");
window.resetFt2HistoryView();
@@ -190,6 +190,7 @@ ft4DecodeToggleBtn?.addEventListener("click", async () => {
});
document.getElementById("settings-clear-ft4-history")?.addEventListener("click", async () => {
if (!confirm("Clear all FT4 decode history? This cannot be undone.")) return;
try {
await postPath("/clear_ft4_decode");
window.resetFt4HistoryView();
@@ -458,6 +458,7 @@ ft8DecodeToggleBtn?.addEventListener("click", async () => {
});
document.getElementById("settings-clear-ft8-history")?.addEventListener("click", async () => {
if (!confirm("Clear all FT8 decode history? This cannot be undone.")) return;
try {
await postPath("/clear_ft8_decode");
window.resetFt8HistoryView();
@@ -384,6 +384,7 @@ hfAprsDecodeToggleBtn?.addEventListener("click", async () => {
});
document.getElementById("settings-clear-hf-aprs-history")?.addEventListener("click", async () => {
if (!confirm("Clear all HF APRS decode history? This cannot be undone.")) return;
try {
await postPath("/clear_hf_aprs_decode");
window.resetHfAprsHistoryView();
@@ -278,6 +278,7 @@ satDom.typeFilter?.addEventListener("change", () => renderSatHistoryTable());
document
.getElementById("settings-clear-sat-history")
?.addEventListener("click", async () => {
if (!confirm("Clear all satellite decode history? This cannot be undone.")) return;
try {
await postPath("/clear_lrpt_decode");
window.resetSatHistoryView();
@@ -19,6 +19,7 @@
let interleaveTicker = null;
let schedulerStepPending = false;
let schEntryEditIdx = null; // null = adding, number = editing that index
let schedulerDirty = false; // true when unsaved changes exist
// Satellite entry editing state moved to sat-scheduler.js
// -------------------------------------------------------------------------
@@ -127,6 +128,7 @@
bookmarkList = Array.isArray(bms) ? bms : [];
populateTsBookmarkSelect();
renderScheduler();
clearSchedulerDirty();
renderSchedulerInterleaveStatus();
})
.catch(function (e) {
@@ -569,6 +571,7 @@
schCloseEntryForm();
renderTimespanEntries();
markSchedulerDirty();
}
// -------------------------------------------------------------------------
@@ -781,6 +784,7 @@
if (!currentConfig || !currentConfig.entries) return;
currentConfig.entries.splice(idx, 1);
renderTimespanEntries();
markSchedulerDirty();
}
// -------------------------------------------------------------------------
@@ -830,6 +834,7 @@
.then(function (saved) {
currentConfig = saved;
renderScheduler();
clearSchedulerDirty();
showSchedulerToast("Scheduler saved.");
})
.catch(function (e) {
@@ -859,6 +864,7 @@
entries: [],
};
renderScheduler();
clearSchedulerDirty();
showSchedulerToast("Scheduler reset.");
})
.catch(function (e) {
@@ -866,6 +872,22 @@
});
}
// -------------------------------------------------------------------------
// Dirty-state tracking
// -------------------------------------------------------------------------
function markSchedulerDirty() {
if (schedulerDirty) return;
schedulerDirty = true;
var btn = document.getElementById("scheduler-save-btn");
if (btn) btn.classList.add("sch-dirty");
}
function clearSchedulerDirty() {
schedulerDirty = false;
var btn = document.getElementById("scheduler-save-btn");
if (btn) btn.classList.remove("sch-dirty");
}
// -------------------------------------------------------------------------
// Toast helper
// -------------------------------------------------------------------------
@@ -918,6 +940,21 @@
schedulerSelectRelativeEntry(1);
});
// Dirty-state: mark dirty on any user input/change within the scheduler panel
var schPanel = document.getElementById("scheduler-panel");
if (schPanel && !schPanel._dirtyWired) {
schPanel._dirtyWired = true;
schPanel.addEventListener("input", function (e) {
// Ignore the entry-form inputs (they don't affect saved config until submitted)
if (e.target.closest("#sch-entry-form") || e.target.closest("#sch-sat-form")) return;
markSchedulerDirty();
});
schPanel.addEventListener("change", function (e) {
if (e.target.closest("#sch-entry-form") || e.target.closest("#sch-sat-form")) return;
markSchedulerDirty();
});
}
wireExtraBmAdd();
wireSatelliteEvents();
}
@@ -313,6 +313,7 @@ window.restoreVdesHistory = function(messages) {
};
document.getElementById("settings-clear-vdes-history")?.addEventListener("click", async () => {
if (!confirm("Clear all VDES decode history? This cannot be undone.")) return;
try {
await postPath("/clear_vdes_decode");
window.resetVdesHistoryView();
@@ -266,6 +266,7 @@ wsprDecodeToggleBtn?.addEventListener("click", async () => {
});
document.getElementById("settings-clear-wspr-history")?.addEventListener("click", async () => {
if (!confirm("Clear all WSPR decode history? This cannot be undone.")) return;
try {
await postPath("/clear_wspr_decode");
window.resetWsprHistoryView();
@@ -4502,6 +4502,21 @@ button:focus-visible, input:focus-visible, select:focus-visible {
border-color: var(--accent-green);
font-weight: 700;
}
.sch-save-btn.sch-dirty::after {
content: "";
display: inline-block;
width: 7px;
height: 7px;
border-radius: 50%;
background: #fff;
margin-left: 0.45rem;
vertical-align: middle;
animation: sch-dirty-pulse 1.5s ease-in-out infinite;
}
@keyframes sch-dirty-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.sch-status-card {
font-size: 0.9rem;
color: var(--text-muted);
@@ -4829,6 +4844,26 @@ button:focus-visible, input:focus-visible, select:focus-visible {
color: var(--text-muted);
font-size: 0.85rem;
}
/* ── Select All / Deselect All buttons ────────────────────────────── */
.bgd-select-actions {
display: flex;
gap: 0.5rem;
}
.bgd-select-btn {
background: var(--bg-secondary);
border: 1px solid var(--border-light);
border-radius: 0.3rem;
color: var(--text-muted);
font-size: 0.78rem;
font-weight: 600;
padding: 0.25rem 0.65rem;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.bgd-select-btn:hover {
background: var(--card-bg);
color: var(--text);
}
/* ── SVG State Dot Badges ─────────────────────────────────────────── */
.bgd-state-dot {
width: 8px;