-
+
No background decode bookmarks configured.
@@ -1273,6 +1273,10 @@
+
+
+
+
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ais.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ais.js
index ec6f03b..994a494 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ais.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ais.js
@@ -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();
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/aprs.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/aprs.js
index b360114..e5bb738 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/aprs.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/aprs.js
@@ -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();
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/background-decode.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/background-decode.js
index 12b3ebc..4833160 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/background-decode.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/background-decode.js
@@ -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;
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/cw.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/cw.js
index 7ab89ec..877ce6d 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/cw.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/cw.js
@@ -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();
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ft2.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ft2.js
index 60af55c..2c65b7a 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ft2.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ft2.js
@@ -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();
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ft4.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ft4.js
index 4e15164..06009ac 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ft4.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ft4.js
@@ -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();
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ft8.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ft8.js
index f5a9de2..d3efaa9 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ft8.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/ft8.js
@@ -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();
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/hf-aprs.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/hf-aprs.js
index bc947b7..f2ba1f7 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/hf-aprs.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/hf-aprs.js
@@ -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();
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/sat.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/sat.js
index 2a5441e..eb53f7a 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/sat.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/sat.js
@@ -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();
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 a564d5e..e38fbf9 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
@@ -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();
}
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/vdes.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/vdes.js
index 9842f84..2b20acd 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/vdes.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/vdes.js
@@ -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();
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/wspr.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/wspr.js
index 1674be9..15e3d67 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/wspr.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/wspr.js
@@ -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();
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 9af5762..540124f 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
@@ -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;