[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:
@@ -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();
|
||||
|
||||
+67
-11
@@ -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, """);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user