Files
trx-rs/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/sat.js
T
sjgandClaude Opus 5 0d4c657b97
CI / lint (pull_request) Failing after 2s
CI / test (pull_request) Failing after 1s
CI / frontend (pull_request) Failing after 41s
CI / reuse (pull_request) Failing after 2s
CI / lint (push) Failing after 2s
CI / test (push) Failing after 2s
CI / frontend (push) Failing after 36s
CI / reuse (push) Failing after 1s
[fix](trx-frontend-http): route feature bundles through the host contract
The bookmark fix addressed one instance of a defect the TypeScript
migration left across the feature entries.  app.js stopped being a
classic script, so its top-level declarations are no longer shared
globals, but the converted entries kept reading them as window
properties that nothing publishes.

Restore the broken behavior:

- ais, aprs, hf-aprs read serverLat, serverLon and haversineKm as
  undefined, so every positioned packet rendered an empty distance.
- ais, aprs, hf-aprs, cw, sat, vdes, wefax, wspr called an undefined
  postPath, so clear-history and decoder toggles threw.
- scheduler read authRole as undefined, so the lazy-load path never
  self-initialized and the Settings tab opened an inert scheduler.
- background-decode read authEnabled as undefined, so control gating
  fell back to role-only.
- vchan read fifteen application values and services as undefined:
  mode and bandwidth sync, the out-of-band hint, RX audio restart, and
  the frequency field all silently no-opped on a virtual channel.
- vchan wrapped window.refreshFreqDisplay, capturing an undefined
  original exactly as it did for setRigFrequency, so leaving a channel
  never restored the application's own frequency display.
- _audioChannelOverride was a const that nothing could assign, so RX
  audio always subscribed to the primary channel.
- ftx-family read fmtTime, a helper legacy ft8.js owned locally, so
  decode bar timestamps rendered empty.

Declare the contract once in plugins/host.ts and import it from the
feature entries, rather than restoring globals that
docs/frontend-architecture.md excludes.  trx.state gains jogUnit,
rxActive and audioChannelOverride, and makes lastModeName writable;
trx.core gains the tuning, RDS, WFM, jog and RX audio services the
entries need.  vchan interception moves to an interceptFreqDisplay
service method that refreshFreqDisplay calls, matching the frequency,
mode and bandwidth interception it already registers.

Reading registry-built elements through a strict lookup is the same
defect as in bookmarks: renderTimelineNeedle guards its result, but
schedulerEl throws, so the now-initializing scheduler crashed on the
timeline needle group that its own SVG creates.

Feature tests move onto a shared host fixture, and entries that now
import a common module are bundled through bundleEntry like the other
shared-module entries.  Covers scheduler self-initialization and the
distance path that the bare window reads broke.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-08-02 11:20:17 +02:00

484 lines
19 KiB
JavaScript

import {
hostCore
} from "./chunk-KL66PICH.js";
// src/plugins/sat.ts
var satWindow = window;
var satDom = {
status: document.getElementById("sat-status"),
liveView: document.getElementById("sat-live-view"),
historyView: document.getElementById("sat-history-view"),
predictionsView: document.getElementById("sat-predictions-view"),
liveLatest: document.getElementById("sat-live-latest"),
historyList: document.getElementById("sat-history-list"),
historyCount: document.getElementById("sat-history-count"),
filterInput: document.getElementById("sat-filter"),
sortSelect: document.getElementById("sat-sort"),
typeFilter: document.getElementById("sat-type-filter"),
lrptState: document.getElementById("sat-lrpt-state"),
viewLiveBtn: document.getElementById("sat-view-live"),
viewHistoryBtn: document.getElementById("sat-view-history"),
viewPredBtn: document.getElementById("sat-view-predictions"),
predFilter: document.getElementById("sat-pred-filter"),
predMinEl: document.getElementById("sat-pred-min-el"),
predCategory: document.getElementById("sat-pred-category"),
predCurrentList: document.getElementById("sat-pred-current-list"),
predUpcomingList: document.getElementById("sat-pred-list"),
predCurrentSec: document.getElementById("sat-pred-current-section"),
predUpcomingSec: document.getElementById("sat-pred-upcoming-section"),
predStatus: document.getElementById("sat-pred-status")
};
var satImageHistory = [];
var SAT_MAX_IMAGES = 100;
var SAT_PRED_PAGE_SIZE = 50;
var satPredShowAll = false;
var satFilterText = "";
var satActiveView = "live";
var satPredData = [];
var satPredFilterText = "";
var satPredMinEl = 0;
var satPredCategory = "all";
var satPredSatCount = 0;
var satPredCountdownTimer = null;
function scheduleSatUi(key, job) {
if (typeof satWindow.trxScheduleUiFrameJob === "function") {
satWindow.trxScheduleUiFrameJob(key, job);
return;
}
job();
}
function switchSatView(view) {
const leavingPredictions = satActiveView === "predictions" && view !== "predictions";
satActiveView = view;
if (satDom.liveView) satDom.liveView.style.display = view === "live" ? "" : "none";
if (satDom.historyView) satDom.historyView.style.display = view === "history" ? "" : "none";
if (satDom.predictionsView) satDom.predictionsView.style.display = view === "predictions" ? "" : "none";
if (satDom.viewLiveBtn) satDom.viewLiveBtn.classList.toggle("sat-view-active", view === "live");
if (satDom.viewHistoryBtn) satDom.viewHistoryBtn.classList.toggle("sat-view-active", view === "history");
if (satDom.viewPredBtn) satDom.viewPredBtn.classList.toggle("sat-view-active", view === "predictions");
if (leavingPredictions) clearPredictionDom();
if (view === "history") {
renderSatHistoryTable();
} else if (view === "predictions") {
satPredShowAll = false;
void loadSatPredictions();
}
}
function clearPredictionDom() {
stopCountdownTimer();
if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
}
satWindow.clearSatPredictionDom = clearPredictionDom;
satDom.viewLiveBtn?.addEventListener("click", () => {
switchSatView("live");
});
satDom.viewHistoryBtn?.addEventListener("click", () => {
switchSatView("history");
});
satDom.viewPredBtn?.addEventListener("click", () => {
switchSatView("predictions");
});
var lastSatLrptOn = null;
satWindow.updateSatLiveState = function(update) {
if (!satDom.lrptState) return;
const lrptOn = !!update.lrpt_decode_enabled;
if (lrptOn !== lastSatLrptOn) {
lastSatLrptOn = lrptOn;
satDom.lrptState.textContent = lrptOn ? "Listening" : "Idle";
satDom.lrptState.className = "sat-live-value " + (lrptOn ? "sat-state-listening" : "sat-state-idle");
if (satDom.status) {
if (lrptOn) {
satDom.status.textContent = "Decoder active — waiting for signal";
} else {
satDom.status.textContent = "Decoder idle";
}
}
}
};
function renderSatLatestCard() {
if (!satDom.liveLatest) return;
if (satImageHistory.length === 0) {
satDom.liveLatest.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 = satImageHistory[0];
if (!img) return;
const typeName = "Meteor LRPT";
const satellite = img.satellite || "";
const channels = img.channels || img.channel_a || "";
const lines = img.mcu_count || img.line_count || 0;
const unit = "MCU rows";
const ts = img._ts || "--";
const date = img._tsMs ? new Date(img._tsMs).toLocaleDateString() : "";
const meta = [typeName];
if (satellite) meta.push(satellite);
if (channels) meta.push(channels);
meta.push(`${lines} ${unit}`);
meta.push(`${date} ${ts}`);
let html = `<div class="sat-latest-card">`;
html += `<div class="sat-latest-title">Latest decoded image</div>`;
html += `<div class="sat-latest-meta">${meta.join(" &middot; ")}</div>`;
if (img.path) {
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>`;
}
if (img.geo_bounds) {
html += ` <button type="button" class="sat-map-btn" onclick="window.satShowOnMap(${img.geo_bounds[0]},${img.geo_bounds[1]},${img.geo_bounds[2]},${img.geo_bounds[3]})" style="font-size:0.8rem;margin-top:0.25rem;margin-left:0.5rem;cursor:pointer;background:none;border:1px solid var(--accent);color:var(--accent);border-radius:3px;padding:1px 6px;">Show on Map</button>`;
}
html += `</div>`;
satDom.liveLatest.innerHTML = html;
}
function getSatFilteredHistory() {
let items = satImageHistory;
const typeVal = satDom.typeFilter ? satDom.typeFilter.value : "all";
if (typeVal === "lrpt") items = items.filter((i) => i._decoder === "lrpt");
if (satFilterText) {
items = items.filter((i) => {
const haystack = [
"meteor lrpt",
i.satellite || "",
i.channels || "",
i.channel_a || "",
i.channel_b || ""
].join(" ").toUpperCase();
return haystack.includes(satFilterText);
});
}
const sortVal = satDom.sortSelect ? satDom.sortSelect.value : "newest";
if (sortVal === "oldest") items = items.slice().reverse();
return items;
}
function renderSatHistoryRow(img) {
const row = document.createElement("div");
row.className = "sat-history-row";
const typeName = "Meteor LRPT";
const typeClass = "sat-type-lrpt";
const ts = img._ts || "--";
const date = img._tsMs ? new Date(img._tsMs).toLocaleDateString([], { month: "short", day: "numeric" }) : "";
const satellite = img.satellite || "--";
const channels = img.channels || "--";
const lines = img.mcu_count || img.line_count || 0;
const unit = "MCU";
let link = img.path ? `<a href="${img.path}" target="_blank" style="color:var(--accent);">PNG</a>` : "--";
if (img.geo_bounds) {
link += ` <a href="javascript:void(0)" onclick="window.satShowOnMap(${img.geo_bounds[0]},${img.geo_bounds[1]},${img.geo_bounds[2]},${img.geo_bounds[3]})" style="color:var(--accent);">Map</a>`;
}
row.innerHTML = [
`<span>${date} ${ts}</span>`,
`<span class="sat-col-type ${typeClass}">${typeName}</span>`,
`<span>${satellite}</span>`,
`<span>${channels}</span>`,
`<span>${lines} ${unit}</span>`,
`<span>${link}</span>`
].join("");
return row;
}
function renderSatHistoryTable() {
if (!satDom.historyList) return;
const items = getSatFilteredHistory();
const fragment = document.createDocumentFragment();
for (const image of items) {
fragment.appendChild(renderSatHistoryRow(image));
}
satDom.historyList.replaceChildren(fragment);
if (satDom.historyCount) {
const total = satImageHistory.length;
const shown = items.length;
satDom.historyCount.textContent = total === 0 ? "No images yet" : shown === total ? `${total} image${total === 1 ? "" : "s"}` : `${shown} of ${total} images`;
}
}
function addSatImage(img, decoder) {
const tsMs = Number.isFinite(img.ts_ms) ? Number(img.ts_ms) : Date.now();
img._tsMs = tsMs;
img._ts = new Date(tsMs).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
second: "2-digit"
});
img._decoder = decoder;
satImageHistory.unshift(img);
if (satImageHistory.length > SAT_MAX_IMAGES) {
satImageHistory = satImageHistory.slice(0, SAT_MAX_IMAGES);
}
scheduleSatUi("sat-latest", () => {
renderSatLatestCard();
});
if (satActiveView === "history") {
scheduleSatUi("sat-history", () => {
renderSatHistoryTable();
});
}
}
function onServerLrptProgress(msg) {
if (satDom.status && (msg.mcu_count ?? 0) > 0) {
satDom.status.textContent = `Receiving — ${String(msg.mcu_count ?? 0)} MCU rows decoded`;
}
}
function onServerLrptImage(msg) {
if (satDom.status) satDom.status.textContent = "Image received (Meteor LRPT)";
addSatImage(msg, "lrpt");
if (msg.geo_bounds && msg.path && satWindow.addSatMapOverlay) {
satWindow.addSatMapOverlay(msg);
}
}
function resetSatHistoryView() {
satImageHistory = [];
if (satDom.historyList) satDom.historyList.innerHTML = "";
renderSatLatestCard();
renderSatHistoryTable();
satWindow.clearSatMapOverlays?.();
}
function pruneSatHistoryView() {
renderSatHistoryTable();
renderSatLatestCard();
}
satWindow.trxPluginRuntime.registerDecoder({
id: "lrpt_image",
onMessage: onServerLrptImage,
reset: resetSatHistoryView,
prune: pruneSatHistoryView
});
satWindow.trxPluginRuntime.registerDecoder({
id: "lrpt_progress",
onMessage: onServerLrptProgress
});
var lrptDecodeToggleBtn = document.getElementById("lrpt-decode-toggle-btn");
lrptDecodeToggleBtn?.addEventListener("click", () => {
void (async () => {
try {
await satWindow.takeSchedulerControlForDecoderDisable?.(lrptDecodeToggleBtn);
await hostCore.postPath("/toggle_lrpt_decode");
} catch (e) {
console.error("LRPT toggle failed", e);
}
})();
});
var satFilterInput = satDom.filterInput;
satFilterInput?.addEventListener("input", () => {
satFilterText = satFilterInput.value.trim().toUpperCase();
renderSatHistoryTable();
});
satDom.sortSelect?.addEventListener("change", () => {
renderSatHistoryTable();
});
satDom.typeFilter?.addEventListener("change", () => {
renderSatHistoryTable();
});
document.getElementById("settings-clear-sat-history")?.addEventListener("click", () => {
void (async () => {
if (!await satWindow.trxUi.confirm({ title: "Clear satellite history?", message: "All stored satellite decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
try {
await hostCore.postPath("/clear_lrpt_decode");
resetSatHistoryView();
} catch (e) {
console.error("Weather satellite history clear failed", e);
}
})();
});
function azToCardinal(deg) {
const dirs = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"];
return dirs[Math.round(deg / 45) % 8] ?? "N";
}
function formatPredTime(ms) {
const d = new Date(ms);
const now = /* @__PURE__ */ new Date();
const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const day = d.getUTCDay() !== now.getUTCDay() ? `${dayNames[d.getUTCDay()] ?? ""} ` : "";
const hh = String(d.getUTCHours()).padStart(2, "0");
const mm = String(d.getUTCMinutes()).padStart(2, "0");
return `${day}${hh}:${mm}`;
}
function formatPredDuration(s) {
if (s >= 60) return `${Math.round(s / 60)} min`;
return `${s}s`;
}
function formatCountdown(ms) {
const totalSec = Math.max(0, Math.floor(ms / 1e3));
const m = Math.floor(totalSec / 60);
const s = totalSec % 60;
return `${m}:${String(s).padStart(2, "0")}`;
}
function elevationClass(deg) {
if (deg >= 45) return "sat-pred-el-high";
if (deg >= 10) return "sat-pred-el-mid";
return "sat-pred-el-low";
}
function stopCountdownTimer() {
if (satPredCountdownTimer) {
clearInterval(satPredCountdownTimer);
satPredCountdownTimer = null;
}
}
function startCountdownTimer(container) {
const countdownEls = container?.querySelectorAll(".sat-pred-col-countdown") ?? [];
if (countdownEls.length === 0) return;
satPredCountdownTimer = setInterval(() => {
if (satActiveView !== "predictions") {
stopCountdownTimer();
return;
}
const n = Date.now();
let anyActive = false;
for (const el of countdownEls) {
const los = Number.parseInt(el.dataset.los ?? "0", 10);
const rem = los - n;
if (rem > 0) {
el.textContent = formatCountdown(rem);
anyActive = true;
} else {
el.textContent = "0:00";
}
}
if (!anyActive) {
stopCountdownTimer();
renderSatPredictions(getFilteredPredictions());
}
}, 1e3);
}
function buildCurrentPassRow(pass, now) {
const row = document.createElement("div");
row.className = "sat-pred-row-current";
const dir = `${azToCardinal(pass.azimuth_aos_deg)}${azToCardinal(pass.azimuth_los_deg)}`;
const remaining = Math.max(0, pass.los_ms - now);
row.innerHTML = [
`<span class="sat-pred-col-sat">${pass.satellite}</span>`,
`<span class="sat-pred-col-el ${elevationClass(pass.max_elevation_deg)}">${pass.max_elevation_deg.toFixed(1)}°</span>`,
`<span class="sat-pred-col-time">${formatPredTime(pass.aos_ms)}</span>`,
`<span class="sat-pred-col-time">${formatPredTime(pass.los_ms)}</span>`,
`<span class="sat-pred-col-countdown" data-los="${pass.los_ms}">${formatCountdown(remaining)}</span>`,
`<span class="sat-pred-col-dir">${dir}</span>`
].join("");
return row;
}
function buildUpcomingPassRow(pass) {
const row = document.createElement("div");
row.className = "sat-pred-row";
const dir = `${azToCardinal(pass.azimuth_aos_deg)}${azToCardinal(pass.azimuth_los_deg)}`;
row.innerHTML = [
`<span class="sat-pred-col-time">${formatPredTime(pass.aos_ms)}</span>`,
`<span class="sat-pred-col-sat">${pass.satellite}</span>`,
`<span class="sat-pred-col-el ${elevationClass(pass.max_elevation_deg)}">${pass.max_elevation_deg.toFixed(1)}°</span>`,
`<span class="sat-pred-col-dur">${formatPredDuration(pass.duration_s)}</span>`,
`<span class="sat-pred-col-dir">${dir}</span>`
].join("");
return row;
}
function getFilteredPredictions() {
let items = satPredData;
if (satPredCategory !== "all") items = items.filter((p) => p.category === satPredCategory);
if (satPredMinEl > 0) items = items.filter((p) => p.max_elevation_deg >= satPredMinEl);
if (satPredFilterText) items = items.filter((p) => p.satellite.toUpperCase().includes(satPredFilterText));
return items;
}
function applyPredFilters() {
renderSatPredictions(getFilteredPredictions());
}
var satPredictionFilter = satDom.predFilter;
satPredictionFilter?.addEventListener("input", () => {
satPredFilterText = satPredictionFilter.value.trim().toUpperCase();
applyPredFilters();
});
var satPredictionMinElevation = satDom.predMinEl;
satPredictionMinElevation?.addEventListener("change", () => {
satPredMinEl = Number.parseInt(satPredictionMinElevation.value, 10) || 0;
applyPredFilters();
});
var satPredictionCategory = satDom.predCategory;
satPredictionCategory?.addEventListener("change", () => {
satPredCategory = satPredictionCategory.value;
applyPredFilters();
});
function renderSatPredictions(passes, error) {
stopCountdownTimer();
if (error) {
if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
if (satDom.predCurrentSec) satDom.predCurrentSec.style.display = "none";
if (satDom.predUpcomingSec) satDom.predUpcomingSec.style.display = "none";
if (satDom.predStatus) satDom.predStatus.textContent = error;
return;
}
if (!Array.isArray(passes) || passes.length === 0) {
if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
if (satDom.predCurrentSec) satDom.predCurrentSec.style.display = "none";
if (satDom.predUpcomingSec) satDom.predUpcomingSec.style.display = "none";
if (satDom.predStatus) satDom.predStatus.textContent = "No passes found in the next 24 hours.";
return;
}
const now = Date.now();
const current = passes.filter((p) => p.aos_ms <= now && p.los_ms > now);
const upcoming = passes.filter((p) => p.aos_ms > now);
if (satDom.predCurrentSec) satDom.predCurrentSec.style.display = current.length > 0 ? "" : "none";
if (satDom.predCurrentList) {
if (current.length === 0) {
satDom.predCurrentList.innerHTML = "";
} else {
const frag = document.createDocumentFragment();
for (const pass of current) frag.appendChild(buildCurrentPassRow(pass, now));
satDom.predCurrentList.replaceChildren(frag);
}
}
const upcomingLimit = satPredShowAll ? upcoming.length : SAT_PRED_PAGE_SIZE;
const visibleUpcoming = upcoming.slice(0, upcomingLimit);
const hiddenCount = upcoming.length - visibleUpcoming.length;
if (satDom.predUpcomingSec) satDom.predUpcomingSec.style.display = upcoming.length > 0 ? "" : "none";
if (satDom.predUpcomingList) {
const frag = document.createDocumentFragment();
for (const pass of visibleUpcoming) frag.appendChild(buildUpcomingPassRow(pass));
if (hiddenCount > 0) {
const moreRow = document.createElement("div");
moreRow.className = "sat-pred-row";
moreRow.style.cursor = "pointer";
moreRow.style.textAlign = "center";
moreRow.innerHTML = `<span style="grid-column:1/-1;color:var(--accent);font-size:0.82rem;">Show ${hiddenCount} more passes…</span>`;
moreRow.addEventListener("click", () => {
satPredShowAll = true;
renderSatPredictions(getFilteredPredictions());
});
frag.appendChild(moreRow);
}
satDom.predUpcomingList.replaceChildren(frag);
}
if (satDom.predStatus) {
let text = `${current.length} active · ${upcoming.length} upcoming · times in UTC`;
if (satPredSatCount > 0) text += ` · ${satPredSatCount} satellites tracked`;
satDom.predStatus.textContent = text;
}
if (current.length > 0 && satActiveView === "predictions") {
startCountdownTimer(satDom.predCurrentList);
}
}
async function loadSatPredictions() {
if (satDom.predStatus) satDom.predStatus.textContent = "Loading predictions…";
if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
try {
const resp = await fetch("/sat_passes");
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const data = await resp.json();
satPredSatCount = data.satellite_count || 0;
if (data.error) {
satPredData = [];
renderSatPredictions([], data.error);
} else {
satPredData = data.passes || [];
renderSatPredictions(getFilteredPredictions());
}
} catch (error) {
renderSatPredictions([], `Failed to load predictions: ${error instanceof Error ? error.message : String(error)}`);
}
}
satWindow.satShowOnMap = function(south, west, north, east) {
if (typeof satWindow.enableMapSourceFilter === "function") {
satWindow.enableMapSourceFilter("sat");
}
const lat = (south + north) / 2;
const lon = (west + east) / 2;
if (satWindow.navigateToAprsMap) {
satWindow.navigateToAprsMap(lat, lon);
}
};
renderSatLatestCard();
renderSatHistoryTable();