Complete TypeScript frontend migration #22

Merged
sjg merged 51 commits from feat/typescript-frontend-migration into main 2026-08-01 23:06:19 +02:00
7 changed files with 7953 additions and 7964 deletions
Showing only changes of commit 50e07e2715 - Show all commits
File diff suppressed because it is too large Load Diff
@@ -3041,9 +3041,13 @@
}
function autoInitIfVisible() {
const panel = mapEl("tab-map");
if (panel && panel.style.display !== "none" && typeof _initMapWhenReady === "function") {
_initMapWhenReady();
if (panel.style.display === "none") return;
if (typeof L === "undefined") {
window.setTimeout(autoInitIfVisible, 50);
return;
}
initAprsMap();
requestAnimationFrame(() => requestAnimationFrame(sizeAprsMapToViewport));
}
modules.map = {
initAprsMap,
@@ -16,7 +16,6 @@ await rm(outputDir, { recursive: true, force: true });
await build({
entryPoints: {
"api-client": path.join(sourceDir, "api", "client.ts"),
app: path.join(sourceDir, "app.js"),
"ui-core": path.join(sourceDir, "ui-core.ts"),
"plugin-loader": path.join(sourceDir, "plugin-loader.ts"),
"plugin-runtime": path.join(sourceDir, "plugin-runtime.ts"),
@@ -32,6 +31,19 @@ await build({
logLevel: "info",
});
await build({
entryPoints: { app: path.join(sourceDir, "bootstrap.ts") },
outdir: outputDir,
bundle: true,
format: "iife",
platform: "browser",
target: "es2022",
sourcemap: false,
legalComments: "inline",
charset: "utf8",
logLevel: "info",
});
await build({
entryPoints: {
ft2: path.join(sourceDir, "plugins", "ft2.ts"),
@@ -2,6 +2,14 @@
//
// SPDX-License-Identifier: GPL-2.0-or-later
import {
formatDistanceKm,
formatTimeAgo,
haversineKm,
latLonToMaidenhead,
locatorToLatLon,
} from "./core/geo.js";
// --- Decoder registry (fetched from /decoders on load) ---
/** @type {Array<{id:string,label:string,activation:string,active_modes:string[],background_decode:boolean,bookmark_selectable:boolean}>} */
let decoderRegistry = [];
@@ -4913,41 +4921,6 @@ window.addEventListener("resize", resizeHeaderSignalCanvas);
// Core communicates with the map module via window.trx.modules.map.* namespace.
// ── Geo utilities (shared with map-core.js via window.trx) ─────────────────
function haversineKm(lat1, lon1, lat2, lon2) {
const R = 6371;
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(dLat / 2) ** 2
+ Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
function locatorToLatLon(locator) {
const raw = String(locator || "").trim().toUpperCase();
if (!/^[A-R]{2}\d{2}([A-X]{2})?$/.test(raw)) return null;
let lon = -180;
let lat = -90;
lon += (raw.charCodeAt(0) - 65) * 20;
lat += (raw.charCodeAt(1) - 65) * 10;
lon += Number(raw.slice(2, 3)) * 2;
lat += Number(raw.slice(3, 4));
if (raw.length >= 6) {
lon += (raw.charCodeAt(4) - 65) * (5 / 60);
lat += (raw.charCodeAt(5) - 65) * (2.5 / 60);
lon += 2.5 / 60;
lat += 1.25 / 60;
} else {
lon += 1;
lat += 0.5;
}
return { lat, lon };
}
function formatDistanceKm(distKm) {
if (!Number.isFinite(distKm)) return null;
return distKm < 1 ? `${Math.round(distKm * 1000)} m` : `${distKm.toFixed(1)} km`;
}
function bookmarkDistanceText(bm) {
if (!bm || serverLat == null || serverLon == null) return null;
const latLon = locatorToLatLon(bm.locator);
@@ -4989,33 +4962,6 @@ function nearestBookmarkForHz(hz, widthPx, range) {
return best;
}
function formatTimeAgo(tsMs) {
if (!tsMs) return null;
const secs = Math.round((Date.now() - tsMs) / 1000);
if (secs < 60) return `${secs}s ago`;
const mins = Math.round(secs / 60);
if (mins < 60) return `${mins} min ago`;
const hrs = Math.floor(mins / 60);
const remMins = mins % 60;
return remMins > 0 ? `${hrs}h ${remMins}min ago` : `${hrs}h ago`;
}
function latLonToMaidenhead(lat, lon) {
const adjustedLon = lon + 180;
const adjustedLat = lat + 90;
const A = "A".charCodeAt(0);
const a = "a".charCodeAt(0);
const field1 = String.fromCharCode(A + Math.floor(adjustedLon / 20));
const field2 = String.fromCharCode(A + Math.floor(adjustedLat / 10));
const square1 = Math.floor((adjustedLon % 20) / 2);
const square2 = Math.floor(adjustedLat % 10);
const sub1 = String.fromCharCode(a + Math.floor((adjustedLon % 2) * 12));
const sub2 = String.fromCharCode(a + Math.floor((adjustedLat % 1) * 24));
return `${field1}${field2}${square1}${square2}${sub1}${sub2}`;
}
// --- Sub-tab navigation ---
function _wireSubTabBar(bar) {
if (bar._subtabWired) return;
@@ -0,0 +1,5 @@
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
//
// SPDX-License-Identifier: GPL-2.0-or-later
import "./app.js";
@@ -0,0 +1,62 @@
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
//
// SPDX-License-Identifier: GPL-2.0-or-later
export interface LatLon {
readonly lat: number;
readonly lon: number;
}
export function haversineKm(lat1: number, lon1: number, lat2: number, lon2: number): number {
const radiusKm = 6371;
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(dLat / 2) ** 2
+ Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon / 2) ** 2;
return radiusKm * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
export function locatorToLatLon(locator: unknown): LatLon | null {
const raw = typeof locator === "string" ? locator.trim().toUpperCase() : "";
if (!/^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(raw)) return null;
let lon = -180 + (raw.charCodeAt(0) - 65) * 20 + Number(raw.slice(2, 3)) * 2;
let lat = -90 + (raw.charCodeAt(1) - 65) * 10 + Number(raw.slice(3, 4));
if (raw.length >= 6) {
lon += (raw.charCodeAt(4) - 65) * (5 / 60) + 2.5 / 60;
lat += (raw.charCodeAt(5) - 65) * (2.5 / 60) + 1.25 / 60;
} else {
lon += 1;
lat += 0.5;
}
return { lat, lon };
}
export function formatDistanceKm(distanceKm: number): string | null {
if (!Number.isFinite(distanceKm)) return null;
return distanceKm < 1 ? `${Math.round(distanceKm * 1000)} m` : `${distanceKm.toFixed(1)} km`;
}
export function formatTimeAgo(timestampMs: number): string | null {
if (!timestampMs) return null;
const seconds = Math.round((Date.now() - timestampMs) / 1000);
if (seconds < 60) return `${seconds}s ago`;
const minutes = Math.round(seconds / 60);
if (minutes < 60) return `${minutes} min ago`;
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}min ago` : `${hours}h ago`;
}
export function latLonToMaidenhead(lat: number, lon: number): string {
const adjustedLon = lon + 180;
const adjustedLat = lat + 90;
const upperA = "A".charCodeAt(0);
const lowerA = "a".charCodeAt(0);
const field1 = String.fromCharCode(upperA + Math.floor(adjustedLon / 20));
const field2 = String.fromCharCode(upperA + Math.floor(adjustedLat / 10));
const square1 = Math.floor((adjustedLon % 20) / 2);
const square2 = Math.floor(adjustedLat % 10);
const sub1 = String.fromCharCode(lowerA + Math.floor((adjustedLon % 2) * 12));
const sub2 = String.fromCharCode(lowerA + Math.floor((adjustedLat % 1) * 24));
return `${field1}${field2}${square1}${square2}${sub1}${sub2}`;
}
@@ -3610,12 +3610,15 @@ const mapWindow = window as unknown as MapWindow;
// Auto-init map if the map tab is already visible (e.g. direct /map navigation).
// Delegates to _initMapWhenReady() in app.js which handles the Leaflet load race.
function autoInitIfVisible() {
const panel = mapEl("tab-map");
if (panel && panel.style.display !== "none" && typeof _initMapWhenReady === "function") {
_initMapWhenReady();
if (panel.style.display === "none") return;
if (typeof L === "undefined") {
window.setTimeout(autoInitIfVisible, 50);
return;
}
initAprsMap();
requestAnimationFrame(() => requestAnimationFrame(sizeAprsMapToViewport));
}
// Register module API for core to call