diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js
index 8f7f673..fd330c2 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js
@@ -254,7 +254,8 @@ function applyAuthRestrictions() {
"settings-clear-ft2-history",
"settings-clear-wspr-history",
"settings-clear-sat-history",
- "header-rec-btn"
+ "recorder-start-btn",
+ "recorder-stop-btn"
];
pluginToggleBtns.forEach(id => {
const btn = document.getElementById(id);
@@ -4300,12 +4301,13 @@ if (spectrumBwSweetBtn) {
}
// --- Tab navigation ---
-const TAB_ORDER = ["main", "bookmarks", "digital-modes", "map", "statistics", "settings", "about"];
+const TAB_ORDER = ["main", "bookmarks", "digital-modes", "map", "statistics", "recorder", "settings", "about"];
const TAB_PATHS = {
main: "/",
bookmarks: "/bookmarks",
"digital-modes": "/digital-modes",
map: "/map",
+ recorder: "/recorder",
settings: "/settings",
about: "/about",
};
@@ -4353,6 +4355,9 @@ function navigateToTab(name, options = {}) {
if (name === "statistics") {
scheduleStatsRender();
}
+ if (name === "recorder") {
+ refreshRecorderStatus();
+ }
}
document.querySelector(".tab-bar").addEventListener("click", (e) => {
@@ -8857,31 +8862,100 @@ if (headerAudioToggle) {
headerAudioToggle.addEventListener("click", startRxAudio);
}
-// ── Recorder button ────────────────────────────────────────────────────────
-const headerRecBtn = document.getElementById("header-rec-btn");
+// ── Recorder page ──────────────────────────────────────────────────────────
let recorderActive = false;
-function syncRecorderBtn() {
- if (!headerRecBtn) return;
- headerRecBtn.classList.toggle("rec-active", recorderActive);
+const recorderStartBtn = document.getElementById("recorder-start-btn");
+const recorderStopBtn = document.getElementById("recorder-stop-btn");
+const recorderStatusInd = document.getElementById("recorder-status-indicator");
+
+function syncRecorderUi() {
+ if (recorderStartBtn) recorderStartBtn.disabled = recorderActive;
+ if (recorderStopBtn) recorderStopBtn.disabled = !recorderActive;
+ if (recorderStatusInd) {
+ recorderStatusInd.textContent = recorderActive ? "Recording" : "";
+ recorderStatusInd.classList.toggle("rec-active", recorderActive);
+ }
+ // Sync the tab icon indicator.
+ const tabBtn = document.querySelector('.tab[data-tab="recorder"]');
+ if (tabBtn) tabBtn.classList.toggle("rec-active", recorderActive);
}
-if (headerRecBtn) {
- headerRecBtn.addEventListener("click", async () => {
+
+if (recorderStartBtn) {
+ recorderStartBtn.addEventListener("click", async () => {
try {
- if (recorderActive) {
- await postPath("/api/recorder/stop");
- } else {
- await postPath("/api/recorder/start");
- }
+ await postPath("/api/recorder/start");
} catch (e) {
- console.error("Recorder toggle failed", e);
+ console.error("Recorder start failed", e);
}
});
}
+if (recorderStopBtn) {
+ recorderStopBtn.addEventListener("click", async () => {
+ try {
+ await postPath("/api/recorder/stop");
+ } catch (e) {
+ console.error("Recorder stop failed", e);
+ }
+ });
+}
+
window._syncRecorderState = function (enabled) {
recorderActive = enabled;
- syncRecorderBtn();
+ syncRecorderUi();
};
+async function refreshRecorderStatus() {
+ try {
+ const [statusResp, filesResp] = await Promise.all([
+ fetch("/api/recorder/status"),
+ fetch("/api/recorder/files"),
+ ]);
+ if (statusResp.ok) {
+ const active = await statusResp.json();
+ renderRecorderActive(active);
+ }
+ if (filesResp.ok) {
+ const files = await filesResp.json();
+ renderRecorderFiles(files);
+ }
+ } catch (e) {
+ console.error("Recorder status fetch failed", e);
+ }
+}
+
+function renderRecorderActive(list) {
+ const el = document.getElementById("recorder-active-list");
+ if (!el) return;
+ if (!list.length) {
+ el.innerHTML = '
No active recordings.
';
+ return;
+ }
+ let html = 'Rig VChan File Started ';
+ for (const r of list) {
+ const started = new Date(r.started_at * 1000).toLocaleTimeString();
+ const fname = r.path.split("/").pop();
+ html += `${escapeMapHtml(r.rig_id)} ${r.vchan_id ? escapeMapHtml(r.vchan_id) : "-"} ${escapeMapHtml(fname)} ${started} `;
+ }
+ html += "
";
+ el.innerHTML = html;
+}
+
+function renderRecorderFiles(list) {
+ const el = document.getElementById("recorder-files-list");
+ if (!el) return;
+ if (!list.length) {
+ el.innerHTML = 'No recorded files.
';
+ return;
+ }
+ let html = 'File Size ';
+ for (const f of list) {
+ const size = f.size < 1048576 ? (f.size / 1024).toFixed(1) + " KB" : (f.size / 1048576).toFixed(1) + " MB";
+ html += `${escapeMapHtml(f.name)} ${size} `;
+ }
+ html += "
";
+ el.innerHTML = html;
+}
+
const rxVolPct = document.getElementById("rx-vol-pct");
const txVolPct = document.getElementById("tx-vol-pct");
@@ -10513,14 +10587,14 @@ function createBookmarkChip(bm, colorMap, options = {}) {
`` +
" " +
` ` +
- `${esc(freqStr)} ` +
+ `${escapeMapHtml(freqStr)} ` +
`` +
- `${esc(bm.name)} `
+ `${escapeMapHtml(bm.name)} `
)
: (
"" +
" " +
- " \u00a0" + esc(bm.name) + " "
+ "\u00a0" + escapeMapHtml(bm.name) + " "
);
span.innerHTML =
labelHtml;
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 57b273a..0692a19 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
@@ -51,6 +51,9 @@
Statistics
+
+
+ Recorder
Settings
@@ -65,7 +68,6 @@
-
@@ -1059,6 +1061,26 @@
+
+
Recorder
+
+ Start recording
+ Stop recording
+
+
+
+ Active recordings
+
+
No active recordings.
+
+
+
+
Scheduler
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 26cb5c4..44f4bdb 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
@@ -1142,31 +1142,68 @@ small { color: var(--text-muted); }
color: #00d17f;
border-color: #00d17f;
}
-.header-bar-btn.header-rec-btn {
- height: 2rem;
- min-height: 0;
- padding: 0 0.5rem;
- font-size: 0.7rem;
- font-weight: 700;
- letter-spacing: 0.04em;
+/* ── Recorder page ──────────────────────────────────────────────────────── */
+.recorder-controls-bar {
+ display: flex;
+ gap: 0.75rem;
+ align-items: center;
+ margin-bottom: 1.25rem;
+}
+.recorder-action-btn {
+ padding: 0.45rem 1rem;
border: 1px solid var(--border-light);
border-radius: 6px;
background: var(--input-bg);
- color: var(--text-muted);
+ color: var(--text-primary);
+ font-size: 0.85rem;
cursor: pointer;
- flex-shrink: 0;
- transition: color 0.15s, border-color 0.15s, background 0.15s;
+ transition: border-color 0.15s, color 0.15s, background 0.15s;
}
-.header-rec-btn.rec-active {
+.recorder-action-btn:hover:not(:disabled) { border-color: var(--accent-green); color: var(--accent-green); }
+.recorder-action-btn:disabled { opacity: 0.4; cursor: default; }
+.recorder-action-btn.recorder-stop:not(:disabled) { border-color: #ff3b30; color: #ff3b30; }
+.recorder-action-btn.recorder-stop:hover:not(:disabled) { background: rgba(255,59,48,0.1); }
+.recorder-status-indicator {
+ font-size: 0.8rem;
+ color: var(--text-muted);
+}
+.recorder-status-indicator.rec-active {
color: #ff3b30;
- border-color: #ff3b30;
- background: rgba(255, 59, 48, 0.12);
+ font-weight: 600;
animation: rec-pulse 1.5s ease-in-out infinite;
}
@keyframes rec-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
+.tab.rec-active .tab-icon { color: #ff3b30; }
+.recorder-section { margin-bottom: 1.5rem; }
+.recorder-section-heading {
+ font-size: 0.85rem;
+ font-weight: 600;
+ color: var(--text-secondary);
+ margin-bottom: 0.5rem;
+}
+.recorder-list { font-size: 0.82rem; }
+.recorder-empty { color: var(--text-muted); font-style: italic; margin: 0; }
+.recorder-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 0.82rem;
+}
+.recorder-table th,
+.recorder-table td {
+ padding: 0.35rem 0.6rem;
+ text-align: left;
+ border-bottom: 1px solid var(--border-light);
+}
+.recorder-table th {
+ font-weight: 600;
+ color: var(--text-secondary);
+ font-size: 0.75rem;
+ text-transform: uppercase;
+ letter-spacing: 0.03em;
+}
.header-rig-switch {
display: flex;
align-items: center;