[feat](trx-frontend-http): add Live/History views to Weather Satellites panel

Replace flat image list with two switchable views:
- Live: decoder state cards (Idle/Listening), descriptions, latest image
- History: filterable table with columns for time, type, satellite,
  channels, lines, and download link. Supports text filter, type filter
  (All/APT/LRPT), and sort order (newest/oldest).

https://claude.ai/code/session_01JA13DHuzuHUL4nSBBRU83f
Signed-off-by: Claude <noreply@anthropic.com>
This commit is contained in:
Claude
2026-03-28 09:26:26 +00:00
committed by Stan Grams
parent 4d40c29e49
commit 6d0c01c6c4
4 changed files with 266 additions and 45 deletions
@@ -3247,6 +3247,7 @@ function render(update) {
lrptToggleBtn.style.borderColor = lrptOn ? "#00d17f" : ""; lrptToggleBtn.style.borderColor = lrptOn ? "#00d17f" : "";
lrptToggleBtn.style.color = lrptOn ? "#00d17f" : ""; lrptToggleBtn.style.color = lrptOn ? "#00d17f" : "";
} }
if (window.updateWxsatLiveState) window.updateWxsatLiveState(update);
const cwAutoEl = document.getElementById("cw-auto"); const cwAutoEl = document.getElementById("cw-auto");
const cwWpmEl = document.getElementById("cw-wpm"); const cwWpmEl = document.getElementById("cw-wpm");
const cwToneEl = document.getElementById("cw-tone"); const cwToneEl = document.getElementById("cw-tone");
@@ -807,6 +807,23 @@
<button id="lrpt-decode-toggle-btn" type="button">Enable Meteor LRPT</button> <button id="lrpt-decode-toggle-btn" type="button">Enable Meteor LRPT</button>
<small id="wxsat-status" style="color:var(--text-muted);">Waiting for satellite pass</small> <small id="wxsat-status" style="color:var(--text-muted);">Waiting for satellite pass</small>
</div> </div>
<!-- View selector -->
<div class="wxsat-view-bar">
<button id="wxsat-view-live" class="wxsat-view-btn wxsat-view-active" type="button">Live</button>
<button id="wxsat-view-history" class="wxsat-view-btn" type="button">History</button>
</div>
<!-- Live view -->
<div id="wxsat-live-view">
<div class="wxsat-live-grid">
<div class="wxsat-live-card">
<span class="wxsat-live-label">NOAA APT</span>
<span id="wxsat-apt-state" class="wxsat-live-value wxsat-state-idle">Idle</span>
</div>
<div class="wxsat-live-card">
<span class="wxsat-live-label">Meteor LRPT</span>
<span id="wxsat-lrpt-state" class="wxsat-live-value wxsat-state-idle">Idle</span>
</div>
</div>
<div style="margin:0.5rem 0;"> <div style="margin:0.5rem 0;">
<div style="color:var(--text-muted); font-size:0.82rem; line-height:1.5;"> <div style="color:var(--text-muted); font-size:0.82rem; line-height:1.5;">
<strong>NOAA APT</strong> &mdash; Automatic Picture Transmission from NOAA-15/18/19 (137 MHz FM). <strong>NOAA APT</strong> &mdash; Automatic Picture Transmission from NOAA-15/18/19 (137 MHz FM).
@@ -817,7 +834,33 @@
Multi-channel CCSDS-framed imagery (APIDs 64&ndash;69) with RGB composite output. Multi-channel CCSDS-framed imagery (APIDs 64&ndash;69) with RGB composite output.
</div> </div>
</div> </div>
<div id="wxsat-images"></div> <div id="wxsat-live-latest" style="margin-top:0.5rem;"></div>
</div>
<!-- History view -->
<div id="wxsat-history-view" style="display:none;">
<div class="wxsat-history-controls">
<input id="wxsat-filter" class="ft8-filter" type="text" placeholder="Filter (e.g. NOAA-18, Meteor, APT)" />
<select id="wxsat-sort" class="wxsat-sort-select">
<option value="newest">Newest first</option>
<option value="oldest">Oldest first</option>
</select>
<select id="wxsat-type-filter" class="wxsat-sort-select">
<option value="all">All types</option>
<option value="apt">NOAA APT only</option>
<option value="lrpt">Meteor LRPT only</option>
</select>
</div>
<div class="wxsat-history-header">
<span class="wxsat-col-time">Time</span>
<span class="wxsat-col-type">Type</span>
<span class="wxsat-col-sat">Satellite</span>
<span class="wxsat-col-ch">Channels</span>
<span class="wxsat-col-lines">Lines</span>
<span class="wxsat-col-link">Image</span>
</div>
<div id="wxsat-history-list"></div>
<small id="wxsat-history-count" style="color:var(--text-muted);font-size:0.75rem;">No images yet</small>
</div>
</div> </div>
</div> </div>
<div id="tab-map" class="tab-panel" style="display:none;"> <div id="tab-map" class="tab-panel" style="display:none;">
@@ -1,10 +1,27 @@
// --- Weather Satellite Decoder Plugin --- // --- Weather Satellite Decoder Plugin ---
// Live view: decoder state, latest image card
// History view: filterable table of all decoded images
// ── DOM references ──────────────────────────────────────────────────
const wxsatStatus = document.getElementById("wxsat-status"); const wxsatStatus = document.getElementById("wxsat-status");
const wxsatImagesEl = document.getElementById("wxsat-images"); const wxsatLiveView = document.getElementById("wxsat-live-view");
const wxsatHistoryView = document.getElementById("wxsat-history-view");
const wxsatLiveLatest = document.getElementById("wxsat-live-latest");
const wxsatHistoryList = document.getElementById("wxsat-history-list");
const wxsatHistoryCount = document.getElementById("wxsat-history-count");
const wxsatFilterInput = document.getElementById("wxsat-filter");
const wxsatSortSelect = document.getElementById("wxsat-sort");
const wxsatTypeFilter = document.getElementById("wxsat-type-filter");
const wxsatAptState = document.getElementById("wxsat-apt-state");
const wxsatLrptState = document.getElementById("wxsat-lrpt-state");
// ── State ───────────────────────────────────────────────────────────
let wxsatImageHistory = []; let wxsatImageHistory = [];
const WXSAT_MAX_IMAGES = 20; const WXSAT_MAX_IMAGES = 100;
let wxsatFilterText = "";
let wxsatActiveView = "live"; // "live" | "history"
// ── UI scheduler helper ─────────────────────────────────────────────
function scheduleWxsatUi(key, job) { function scheduleWxsatUi(key, job) {
if (typeof window.trxScheduleUiFrameJob === "function") { if (typeof window.trxScheduleUiFrameJob === "function") {
window.trxScheduleUiFrameJob(key, job); window.trxScheduleUiFrameJob(key, job);
@@ -13,48 +30,160 @@ function scheduleWxsatUi(key, job) {
job(); job();
} }
function renderWxsatImage(img) { // ── View switching ──────────────────────────────────────────────────
const card = document.createElement("div"); const wxsatViewLiveBtn = document.getElementById("wxsat-view-live");
card.className = "wxsat-image-card"; const wxsatViewHistoryBtn = document.getElementById("wxsat-view-history");
card.style.cssText =
"border:1px solid var(--border-color);border-radius:0.5rem;padding:0.5rem;margin-bottom:0.75rem;background:var(--bg-secondary);";
const ts = img._ts || new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); function switchWxsatView(view) {
wxsatActiveView = view;
if (wxsatLiveView) wxsatLiveView.style.display = view === "live" ? "" : "none";
if (wxsatHistoryView) wxsatHistoryView.style.display = view === "history" ? "" : "none";
if (wxsatViewLiveBtn) {
wxsatViewLiveBtn.classList.toggle("wxsat-view-active", view === "live");
}
if (wxsatViewHistoryBtn) {
wxsatViewHistoryBtn.classList.toggle("wxsat-view-active", view === "history");
}
if (view === "history") {
renderWxsatHistoryTable();
}
}
wxsatViewLiveBtn?.addEventListener("click", () => switchWxsatView("live"));
wxsatViewHistoryBtn?.addEventListener("click", () => switchWxsatView("history"));
// ── Live view: decoder state ────────────────────────────────────────
// Updated from app.js render() via window.updateWxsatLiveState
window.updateWxsatLiveState = function (update) {
if (!wxsatAptState || !wxsatLrptState) return;
const aptOn = !!update.wxsat_decode_enabled;
const lrptOn = !!update.lrpt_decode_enabled;
wxsatAptState.textContent = aptOn ? "Listening" : "Idle";
wxsatAptState.className = "wxsat-live-value " + (aptOn ? "wxsat-state-listening" : "wxsat-state-idle");
wxsatLrptState.textContent = lrptOn ? "Listening" : "Idle";
wxsatLrptState.className = "wxsat-live-value " + (lrptOn ? "wxsat-state-listening" : "wxsat-state-idle");
};
function renderWxsatLatestCard() {
if (!wxsatLiveLatest) return;
if (wxsatImageHistory.length === 0) {
wxsatLiveLatest.innerHTML =
'<div style="color:var(--text-muted);font-size:0.82rem;">No images decoded yet. Enable a decoder and wait for a satellite pass.</div>';
return;
}
const img = wxsatImageHistory[0];
const decoder = img._decoder || "unknown"; const decoder = img._decoder || "unknown";
const typeName = decoder === "lrpt" ? "Meteor LRPT" : "NOAA APT";
const satellite = img.satellite || ""; const satellite = img.satellite || "";
const channels = img.channels || ""; const channels = img.channels || img.channel_a || "";
const lines = img.line_count || img.mcu_count || 0; const lines = img.line_count || img.mcu_count || 0;
const unit = decoder === "lrpt" ? "MCU rows" : "lines";
const ts = img._ts || "--";
const date = img._tsMs ? new Date(img._tsMs).toLocaleDateString() : "";
let metaParts = [`<strong>${decoder === "lrpt" ? "Meteor LRPT" : "NOAA APT"}</strong>`]; let meta = [typeName];
if (satellite) metaParts.push(satellite); if (satellite) meta.push(satellite);
if (channels) metaParts.push("ch " + channels); if (channels) meta.push(channels);
metaParts.push(lines + (decoder === "lrpt" ? " MCU rows" : " lines")); meta.push(`${lines} ${unit}`);
metaParts.push(ts); meta.push(`${date} ${ts}`);
card.innerHTML =
`<div style="font-size:0.82rem;color:var(--text-muted);margin-bottom:0.35rem;">${metaParts.join(" &middot; ")}</div>`;
let html = `<div class="wxsat-latest-card">`;
html += `<div class="wxsat-latest-title">Latest decoded image</div>`;
html += `<div class="wxsat-latest-meta">${meta.join(" &middot; ")}</div>`;
if (img.path) { if (img.path) {
const link = document.createElement("a"); html += `<a href="${img.path}" target="_blank" style="font-size:0.8rem;color:var(--accent);display:inline-block;margin-top:0.25rem;">Download PNG</a>`;
link.href = img.path; }
link.target = "_blank"; html += `</div>`;
link.textContent = "Download image"; wxsatLiveLatest.innerHTML = html;
link.style.cssText = "font-size:0.8rem;color:var(--accent);";
card.appendChild(link);
} }
return card; // ── History view: table ─────────────────────────────────────────────
function getFilteredHistory() {
let items = wxsatImageHistory;
// Type filter
const typeVal = wxsatTypeFilter ? wxsatTypeFilter.value : "all";
if (typeVal === "apt") items = items.filter((i) => i._decoder === "apt");
else if (typeVal === "lrpt") items = items.filter((i) => i._decoder === "lrpt");
// Text filter
if (wxsatFilterText) {
items = items.filter((i) => {
const haystack = [
i._decoder === "lrpt" ? "meteor lrpt" : "noaa apt",
i.satellite || "",
i.channels || "",
i.channel_a || "",
i.channel_b || "",
]
.join(" ")
.toUpperCase();
return haystack.includes(wxsatFilterText);
});
} }
function renderWxsatHistory() { // Sort
if (!wxsatImagesEl) return; const sortVal = wxsatSortSelect ? wxsatSortSelect.value : "newest";
if (sortVal === "oldest") {
items = items.slice().reverse();
}
return items;
}
function renderWxsatHistoryRow(img) {
const row = document.createElement("div");
row.className = "wxsat-history-row";
const decoder = img._decoder || "unknown";
const typeName = decoder === "lrpt" ? "Meteor LRPT" : "NOAA APT";
const typeClass = decoder === "lrpt" ? "wxsat-type-lrpt" : "wxsat-type-apt";
const ts = img._ts || "--";
const date = img._tsMs ? new Date(img._tsMs).toLocaleDateString([], { month: "short", day: "numeric" }) : "";
const satellite = img.satellite || "--";
const channels = decoder === "lrpt" ? (img.channels || "--") : (img.channel_a && img.channel_b ? `A:${img.channel_a} B:${img.channel_b}` : img.channel_a || "--");
const lines = img.line_count || img.mcu_count || 0;
const unit = decoder === "lrpt" ? "MCU" : "ln";
const link = img.path
? `<a href="${img.path}" target="_blank" style="color:var(--accent);">PNG</a>`
: "--";
row.innerHTML = [
`<span>${date} ${ts}</span>`,
`<span class="wxsat-col-type ${typeClass}">${typeName}</span>`,
`<span>${satellite}</span>`,
`<span>${channels}</span>`,
`<span>${lines} ${unit}</span>`,
`<span>${link}</span>`,
].join("");
return row;
}
function renderWxsatHistoryTable() {
if (!wxsatHistoryList) return;
const items = getFilteredHistory();
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
for (let i = 0; i < wxsatImageHistory.length; i += 1) { for (let i = 0; i < items.length; i += 1) {
fragment.appendChild(renderWxsatImage(wxsatImageHistory[i])); fragment.appendChild(renderWxsatHistoryRow(items[i]));
}
wxsatHistoryList.replaceChildren(fragment);
if (wxsatHistoryCount) {
const total = wxsatImageHistory.length;
const shown = items.length;
wxsatHistoryCount.textContent =
total === 0
? "No images yet"
: shown === total
? `${total} image${total === 1 ? "" : "s"}`
: `${shown} of ${total} images`;
} }
wxsatImagesEl.replaceChildren(fragment);
} }
// ── Add image to history ────────────────────────────────────────────
function addWxsatImage(img, decoder) { function addWxsatImage(img, decoder) {
const tsMs = Number.isFinite(img.ts_ms) ? Number(img.ts_ms) : Date.now(); const tsMs = Number.isFinite(img.ts_ms) ? Number(img.ts_ms) : Date.now();
img._tsMs = tsMs; img._tsMs = tsMs;
@@ -69,10 +198,14 @@ function addWxsatImage(img, decoder) {
if (wxsatImageHistory.length > WXSAT_MAX_IMAGES) { if (wxsatImageHistory.length > WXSAT_MAX_IMAGES) {
wxsatImageHistory = wxsatImageHistory.slice(0, WXSAT_MAX_IMAGES); wxsatImageHistory = wxsatImageHistory.slice(0, WXSAT_MAX_IMAGES);
} }
scheduleWxsatUi("wxsat-history", () => renderWxsatHistory());
scheduleWxsatUi("wxsat-latest", () => renderWxsatLatestCard());
if (wxsatActiveView === "history") {
scheduleWxsatUi("wxsat-history", () => renderWxsatHistoryTable());
}
} }
// Server-dispatched callbacks // ── Server callbacks ────────────────────────────────────────────────
window.onServerWxsatImage = function (msg) { window.onServerWxsatImage = function (msg) {
if (wxsatStatus) wxsatStatus.textContent = "Image received (NOAA APT)"; if (wxsatStatus) wxsatStatus.textContent = "Image received (NOAA APT)";
addWxsatImage(msg, "apt"); addWxsatImage(msg, "apt");
@@ -85,10 +218,17 @@ window.onServerLrptImage = function (msg) {
window.resetWxsatHistoryView = function () { window.resetWxsatHistoryView = function () {
wxsatImageHistory = []; wxsatImageHistory = [];
if (wxsatImagesEl) wxsatImagesEl.innerHTML = ""; if (wxsatHistoryList) wxsatHistoryList.innerHTML = "";
renderWxsatLatestCard();
renderWxsatHistoryTable();
}; };
// Toggle buttons window.pruneWxsatHistoryView = function () {
renderWxsatHistoryTable();
renderWxsatLatestCard();
};
// ── Toggle buttons ──────────────────────────────────────────────────
const wxsatDecodeToggleBtn = document.getElementById("wxsat-decode-toggle-btn"); const wxsatDecodeToggleBtn = document.getElementById("wxsat-decode-toggle-btn");
wxsatDecodeToggleBtn?.addEventListener("click", async () => { wxsatDecodeToggleBtn?.addEventListener("click", async () => {
try { try {
@@ -109,7 +249,16 @@ lrptDecodeToggleBtn?.addEventListener("click", async () => {
} }
}); });
// Clear history button // ── Filter / sort event listeners ───────────────────────────────────
wxsatFilterInput?.addEventListener("input", () => {
wxsatFilterText = wxsatFilterInput.value.trim().toUpperCase();
renderWxsatHistoryTable();
});
wxsatSortSelect?.addEventListener("change", () => renderWxsatHistoryTable());
wxsatTypeFilter?.addEventListener("change", () => renderWxsatHistoryTable());
// ── Settings: clear history ─────────────────────────────────────────
document document
.getElementById("settings-clear-wxsat-history") .getElementById("settings-clear-wxsat-history")
?.addEventListener("click", async () => { ?.addEventListener("click", async () => {
@@ -122,5 +271,6 @@ document
} }
}); });
// Initial render // ── Initial render ──────────────────────────────────────────────────
renderWxsatHistory(); renderWxsatLatestCard();
renderWxsatHistoryTable();
@@ -4538,3 +4538,30 @@ button:focus-visible, input:focus-visible, select:focus-visible {
width: 100%; width: 100%;
} }
} }
/* ── Weather Satellite panel ────────────────────────────────────────── */
.wxsat-view-bar { display: flex; gap: 0; margin-bottom: 0.75rem; border-bottom: 1px solid var(--border); }
.wxsat-view-btn { flex-shrink: 0; background: transparent; border: none; border-bottom: 2px solid transparent; border-radius: 0; padding: 0.3rem 0.9rem; color: var(--text-muted); cursor: pointer; font-size: 0.82rem; }
.wxsat-view-active { border-bottom-color: var(--accent-green); color: var(--accent-green); font-weight: 600; }
.wxsat-view-btn:hover:not(.wxsat-view-active) { color: var(--text); }
.wxsat-live-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; margin-bottom: 0.5rem; }
.wxsat-live-card { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 0.35rem; padding: 0.5rem 0.75rem; display: flex; flex-direction: column; gap: 0.15rem; }
.wxsat-live-label { font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.04em; }
.wxsat-live-value { font-size: 0.9rem; font-weight: 600; }
.wxsat-state-idle { color: var(--text-muted); }
.wxsat-state-listening { color: var(--accent-green); }
.wxsat-state-decoding { color: #f0a020; }
.wxsat-history-controls { display: flex; gap: 0.5rem; align-items: center; margin-bottom: 0.5rem; flex-wrap: wrap; }
.wxsat-sort-select { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 0.25rem; color: var(--text); padding: 0.25rem 0.4rem; font-size: 0.82rem; }
.wxsat-history-header { display: grid; grid-template-columns: 7rem 5.5rem 9rem 6rem 4.5rem 1fr; gap: 0.25rem; padding: 0.25rem 0.4rem; font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.03em; border-bottom: 1px solid var(--border); }
.wxsat-history-row { display: grid; grid-template-columns: 7rem 5.5rem 9rem 6rem 4.5rem 1fr; gap: 0.25rem; padding: 0.35rem 0.4rem; font-size: 0.82rem; border-bottom: 1px solid var(--border-faint, rgba(255,255,255,0.04)); }
.wxsat-history-row:hover { background: var(--bg-hover, rgba(255,255,255,0.02)); }
.wxsat-col-type { font-weight: 500; }
.wxsat-type-apt { color: #6ec6ff; }
.wxsat-type-lrpt { color: #b39ddb; }
.wxsat-latest-card { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 0.4rem; padding: 0.6rem 0.75rem; }
.wxsat-latest-card .wxsat-latest-title { font-size: 0.82rem; font-weight: 600; margin-bottom: 0.25rem; }
.wxsat-latest-card .wxsat-latest-meta { font-size: 0.78rem; color: var(--text-muted); }
@media (max-width: 600px) {
.wxsat-live-grid { grid-template-columns: 1fr; }
.wxsat-history-header, .wxsat-history-row { grid-template-columns: 5rem 4rem 6rem 4rem 3.5rem 1fr; font-size: 0.75rem; }
}