diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js
index 017fa03b..f0d794fb 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js
@@ -191,6 +191,80 @@
if (response.status !== 404 && !response.ok) throw new Error("Logout failed");
}
+ // src/core/format.ts
+ function formatDuration(milliseconds) {
+ const seconds = Math.floor(milliseconds / 1e3);
+ const days = Math.floor(seconds / 86400);
+ const hours = Math.floor(seconds % 86400 / 3600);
+ const minutes = Math.floor(seconds % 3600 / 60);
+ const remainder = seconds % 60;
+ const parts = [];
+ if (days > 0) parts.push(`${days}d`);
+ if (hours > 0 || days > 0) parts.push(`${hours}h`);
+ parts.push(`${minutes}m`, `${remainder}s`);
+ return parts.join(" ");
+ }
+ function formatFrequency(frequencyHz) {
+ if (!Number.isFinite(frequencyHz)) return "--";
+ if (frequencyHz >= 1e9) return `${(frequencyHz / 1e9).toFixed(3)} GHz`;
+ if (frequencyHz >= 1e7) return `${(frequencyHz / 1e6).toFixed(3)} MHz`;
+ return `${(frequencyHz / 1e3).toFixed(1)} kHz`;
+ }
+ function formatFrequencyForStep(frequencyHz, stepHz) {
+ if (!Number.isFinite(frequencyHz)) return "--";
+ if (stepHz >= 1e6) return (frequencyHz / 1e6).toFixed(6);
+ if (stepHz >= 1e3) return (frequencyHz / 1e3).toFixed(3);
+ if (stepHz >= 1) return String(Math.round(frequencyHz));
+ return formatFrequency(frequencyHz);
+ }
+ function formatFrequencyForHumans(frequencyHz) {
+ if (!Number.isFinite(frequencyHz)) return "--";
+ if (frequencyHz >= 1e9) return `${(frequencyHz / 1e9).toFixed(3)} GHz`;
+ if (frequencyHz >= 1e6) return `${(frequencyHz / 1e6).toFixed(3)} MHz`;
+ if (frequencyHz >= 1e3) return `${(frequencyHz / 1e3).toFixed(3)} kHz`;
+ return `${Math.round(frequencyHz)} Hz`;
+ }
+ function formatWavelength(frequencyHz) {
+ if (!Number.isFinite(frequencyHz) || frequencyHz <= 0) return "--";
+ const meters = 299792458 / frequencyHz;
+ return meters >= 1 ? `${Math.round(meters)} m` : `${Math.round(meters * 100)} cm`;
+ }
+ function parseFrequencyInput(value, defaultStepHz, mode) {
+ if (!value) return null;
+ const match = /^([0-9]+(?:[.,][0-9]+)?)\s*([kmg]hz|[kmg]|hz)?$/.exec(value.trim().toLowerCase());
+ if (!match?.[1]) return null;
+ const rawNumber = match[1];
+ let frequency = Number.parseFloat(rawNumber.replace(",", "."));
+ const unit = match[2] ?? "";
+ if (Number.isNaN(frequency)) return null;
+ if (unit.startsWith("gh") || unit === "g") frequency *= 1e9;
+ else if (unit.startsWith("mh") || unit === "m") frequency *= 1e6;
+ else if (unit.startsWith("kh") || unit === "k") frequency *= 1e3;
+ else if (!unit) {
+ const hasDecimalSeparator = rawNumber.includes(".") || rawNumber.includes(",");
+ if (mode.toUpperCase() === "WFM") {
+ if (hasDecimalSeparator && frequency >= 50 && frequency < 200) {
+ return Math.round(frequency * 1e6);
+ }
+ if (!hasDecimalSeparator && frequency >= 875 && frequency <= 1080) {
+ return Math.round(frequency / 10 * 1e6);
+ }
+ }
+ if (defaultStepHz >= 1e6) frequency *= 1e6;
+ else if (defaultStepHz >= 1e3) frequency *= 1e3;
+ else if (defaultStepHz < 1) {
+ if (frequency < 1e3) frequency *= 1e6;
+ else if (frequency < 1e6) frequency *= 1e3;
+ }
+ }
+ return Math.round(frequency);
+ }
+ function formatByteSize(bytes) {
+ if (bytes < 1024) return `${bytes} B`;
+ if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
+ return `${(bytes / 1048576).toFixed(1)} MB`;
+ }
+
// src/app.js
void loadDecoderRegistry(refreshOperatorLayoutCapabilities);
var authRole = null;
@@ -872,7 +946,7 @@
document.title = originalTitle;
return;
}
- const parts = [formatFreq(freqHz)];
+ const parts = [formatFrequency(freqHz)];
const ps = rds?.program_service;
if (ps && ps.length > 0) {
parts.push(ps);
@@ -1416,23 +1490,10 @@
var aboutUptimeStart = null;
var es;
var esHeartbeat;
- function formatUptime(ms) {
- const s = Math.floor(ms / 1e3);
- const d = Math.floor(s / 86400);
- const h = Math.floor(s % 86400 / 3600);
- const m = Math.floor(s % 3600 / 60);
- const sec = s % 60;
- const parts = [];
- if (d > 0) parts.push(`${d}d`);
- if (h > 0 || d > 0) parts.push(`${h}h`);
- parts.push(`${m}m`);
- parts.push(`${sec}s`);
- return parts.join(" ");
- }
setInterval(() => {
if (!aboutUptimeStart) return;
const el = document.getElementById("about-uptime");
- if (el) el.textContent = formatUptime(Date.now() - aboutUptimeStart);
+ if (el) el.textContent = formatDuration(Date.now() - aboutUptimeStart);
}, 1e3);
var reconnectTimer = null;
var overviewSignalSamples = [];
@@ -1857,36 +1918,13 @@
texData[offset + 2] = _wfLut[p + 2];
texData[offset + 3] = _wfLut[p + 3];
}
- function formatFreq(hz) {
- if (!Number.isFinite(hz)) return "--";
- if (hz >= 1e9) {
- return `${(hz / 1e9).toFixed(3)} GHz`;
- }
- if (hz >= 1e7) {
- return `${(hz / 1e6).toFixed(3)} MHz`;
- }
- return `${(hz / 1e3).toFixed(1)} kHz`;
- }
- function formatFreqForStep(hz, step) {
- if (!Number.isFinite(hz)) return "--";
- if (step >= 1e6) return (hz / 1e6).toFixed(6);
- if (step >= 1e3) return (hz / 1e3).toFixed(3);
- if (step >= 1) return String(Math.round(hz));
- return formatFreq(hz);
- }
- function formatWavelength(hz) {
- if (!Number.isFinite(hz) || hz <= 0) return "--";
- const meters = 299792458 / hz;
- if (meters >= 1) return `${Math.round(meters)} m`;
- return `${Math.round(meters * 100)} cm`;
- }
function refreshWavelengthDisplay(hz) {
if (!wavelengthEl) return;
wavelengthEl.textContent = formatWavelength(hz);
}
function refreshFreqDisplay() {
if (lastFreqHz == null || freqDirty) return;
- freqEl.value = formatFreqForStep(lastFreqHz, jogUnit);
+ freqEl.value = formatFrequencyForStep(lastFreqHz, jogUnit);
refreshWavelengthDisplay(lastFreqHz);
}
function activeRdsChannelId() {
@@ -2296,7 +2334,7 @@
if (lastSpectrumData && Math.abs(nextCenterHz - Number(lastSpectrumData.center_hz)) < 1) return;
await postPath(`/set_center_freq?hz=${nextCenterHz}`);
if (centerFreqEl && !centerFreqDirty) {
- centerFreqEl.value = formatFreqForStep(nextCenterHz, jogUnit);
+ centerFreqEl.value = formatFrequencyForStep(nextCenterHz, jogUnit);
}
}
var _freqOptimisticHz = null;
@@ -2535,7 +2573,7 @@
await postPath(`/set_center_freq?hz=${targetCenterHz}`);
}
if (centerFreqEl && !centerFreqDirty) {
- centerFreqEl.value = formatFreqForStep(targetCenterHz, jogUnit);
+ centerFreqEl.value = formatFrequencyForStep(targetCenterHz, jogUnit);
}
if (Number.isFinite(originalCenterHz) && Math.abs(targetCenterHz - originalCenterHz) < 1) {
showHint("Already at sweet spot", 900);
@@ -2578,7 +2616,7 @@
showHint("Shifting spectrum…", 900);
await postPath(`/set_center_freq?hz=${nextCenterHz}`);
if (centerFreqEl && !centerFreqDirty) {
- centerFreqEl.value = formatFreqForStep(nextCenterHz, jogUnit);
+ centerFreqEl.value = formatFrequencyForStep(nextCenterHz, jogUnit);
}
const nextFreqHz = tunedFrequencyForCenterCoverage(nextCenterHz);
if (Number.isFinite(nextFreqHz) && Math.abs(nextFreqHz - Number(lastFreqHz)) >= 1) {
@@ -2588,51 +2626,7 @@
}
function refreshCenterFreqDisplay() {
if (!centerFreqEl || !lastSpectrumData || centerFreqDirty) return;
- centerFreqEl.value = formatFreqForStep(lastSpectrumData.center_hz, jogUnit);
- }
- function parseFreqInput(val, defaultStep) {
- if (!val) return null;
- const trimmed = val.trim().toLowerCase();
- const match = trimmed.match(/^([0-9]+(?:[.,][0-9]+)?)\s*([kmg]hz|[kmg]|hz)?$/);
- if (!match) return null;
- const rawNumber = match[1];
- let num = parseFloat(rawNumber.replace(",", "."));
- const unit = match[2] || "";
- if (Number.isNaN(num)) return null;
- if (unit.startsWith("gh") || unit === "g") {
- num *= 1e9;
- } else if (unit.startsWith("mh") || unit === "m") {
- num *= 1e6;
- } else if (unit.startsWith("kh") || unit === "k") {
- num *= 1e3;
- } else if (!unit) {
- const mode = (modeEl?.value || "").toUpperCase();
- const hasDecimalSeparator = rawNumber.includes(".") || rawNumber.includes(",");
- if (mode === "WFM") {
- if (hasDecimalSeparator && num >= 50 && num < 200) {
- num *= 1e6;
- return Math.round(num);
- }
- if (!hasDecimalSeparator && num >= 875 && num <= 1080) {
- num = num / 10 * 1e6;
- return Math.round(num);
- }
- }
- if (defaultStep >= 1e6) {
- num *= 1e6;
- } else if (defaultStep >= 1e3) {
- num *= 1e3;
- } else if (defaultStep >= 1) {
- } else {
- if (num >= 1e6) {
- } else if (num >= 1e3) {
- num *= 1e3;
- } else {
- num *= 1e6;
- }
- }
- }
- return Math.round(num);
+ centerFreqEl.value = formatFrequencyForStep(lastSpectrumData.center_hz, jogUnit);
}
function normalizeMinFreqStep(cap) {
const val = Number(cap && cap.min_freq_step_hz);
@@ -2698,18 +2692,11 @@
}
function unsupportedBandSummary() {
if (supportedBands.length === 0) return "No supported frequency ranges were reported by the rig.";
- const ranges = supportedBands.slice().sort((a, b) => a.low - b.low).map((b) => `${formatFreqForHumans(b.low)} to ${formatFreqForHumans(b.high)}`);
+ const ranges = supportedBands.slice().sort((a, b) => a.low - b.low).map((b) => `${formatFrequencyForHumans(b.low)} to ${formatFrequencyForHumans(b.high)}`);
return `Supported ranges: ${ranges.join(", ")}`;
}
- function formatFreqForHumans(hz) {
- if (!Number.isFinite(hz)) return "--";
- if (hz >= 1e9) return `${(hz / 1e9).toFixed(3)} GHz`;
- if (hz >= 1e6) return `${(hz / 1e6).toFixed(3)} MHz`;
- if (hz >= 1e3) return `${(hz / 1e3).toFixed(3)} kHz`;
- return `${Math.round(hz)} Hz`;
- }
function showUnsupportedFreqPopup(hz) {
- const message = `Unsupported frequency: ${formatFreqForHumans(hz)}.
+ const message = `Unsupported frequency: ${formatFrequencyForHumans(hz)}.
${unsupportedBandSummary()}`;
showHint("Out of supported range", 1800);
@@ -3288,7 +3275,7 @@ ${unsupportedBandSummary()}`;
if (hz === null) return;
const mode = entry.mode ? normalizeMode(entry.mode) : "";
const modeText = mode ? ` [${mode}]` : "";
- const label = `${entry.name || String.fromCharCode(65 + idx)}: ${formatFreq(hz)}${modeText}`;
+ const label = `${entry.name || String.fromCharCode(65 + idx)}: ${formatFrequency(hz)}${modeText}`;
const btn = document.createElement("button");
btn.type = "button";
btn.textContent = label;
@@ -3738,7 +3725,7 @@ ${unsupportedBandSummary()}`;
}
});
function applyFreqFromInput() {
- const parsedRaw = parseFreqInput(freqEl.value, jogUnit);
+ const parsedRaw = parseFrequencyInput(freqEl.value, jogUnit, modeEl?.value || "");
const parsed = alignFreqToRigStep(parsedRaw);
if (parsed === null) {
showHint("Freq missing", 1500);
@@ -3753,7 +3740,7 @@ ${unsupportedBandSummary()}`;
}
async function applyCenterFreqFromInput() {
if (!centerFreqEl) return;
- const parsedRaw = parseFreqInput(centerFreqEl.value, jogUnit);
+ const parsedRaw = parseFrequencyInput(centerFreqEl.value, jogUnit, modeEl?.value || "");
const parsed = alignFreqToRigStep(parsedRaw);
if (parsed === null) {
showHint("Central freq missing", 1500);
@@ -4594,11 +4581,11 @@ ${unsupportedBandSummary()}`;
loadSetting,
showHint,
escapeMapHtml: escapeHtml,
- formatFreq,
- formatFreqForHumans,
+ formatFreq: formatFrequency,
+ formatFreqForHumans: formatFrequencyForHumans,
formatWavelength,
formatBwLabel,
- formatUptime,
+ formatUptime: formatDuration,
formatSigStrength,
formatSignal,
postPath,
@@ -5640,11 +5627,6 @@ ${unsupportedBandSummary()}`;
html += "";
el.innerHTML = html;
}
- function recorderFormatSize(bytes) {
- if (bytes < 1024) return bytes + " B";
- if (bytes < 1048576) return (bytes / 1024).toFixed(1) + " KB";
- return (bytes / 1048576).toFixed(1) + " MB";
- }
function recFilterAndSort() {
const filterEl = document.getElementById("recorder-filter");
const sortEl = document.getElementById("recorder-sort");
@@ -5711,7 +5693,7 @@ ${unsupportedBandSummary()}`;
for (const f of page) {
const safeName = escapeHtml(f.name);
const encodedName = encodeURIComponent(f.name);
- html += '
| ' + safeName + " | " + recorderFormatSize(f.size) + ' | |
';
+ html += '| ' + safeName + " | " + formatByteSize(f.size) + ' | |
';
}
html += "";
el.innerHTML = html;
@@ -6513,7 +6495,7 @@ ${unsupportedBandSummary()}`;
let peakIndex = 0;
for (let i = 1; i < bins.length; i += 1) if (bins[i] > bins[peakIndex]) peakIndex = i;
const peakHz = centerHz - sampleRate / 2 + peakIndex / Math.max(1, bins.length - 1) * sampleRate;
- spectrumSummary.textContent = `Spectrum centered at ${formatFreqForHumans(centerHz)}, spanning ${formatFreqForHumans(sampleRate)}. Strongest visible bin near ${formatFreqForHumans(peakHz)} at ${bins[peakIndex]} dB.`;
+ spectrumSummary.textContent = `Spectrum centered at ${formatFrequencyForHumans(centerHz)}, spanning ${formatFrequencyForHumans(sampleRate)}. Strongest visible bin near ${formatFrequencyForHumans(peakHz)} at ${bins[peakIndex]} dB.`;
}
if (spectrumCenterPendingHz !== null && Math.abs(centerHz - spectrumCenterPendingHz) < 1e3) {
spectrumCenterPendingHz = null;
@@ -7458,7 +7440,7 @@ ${unsupportedBandSummary()}`;
return;
}
setRigFrequency(rounded);
- showHint(`Rounded → ${formatFreq(rounded)}`, 1200);
+ showHint(`Rounded → ${formatFrequency(rounded)}`, 1200);
} else {
showHint("Already on step", 1200);
}
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.js b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.js
index f317909b..e595e268 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.js
@@ -20,6 +20,15 @@ import {
login,
logout,
} from "./api/auth.js";
+import {
+ formatByteSize as recorderFormatSize,
+ formatDuration as formatUptime,
+ formatFrequency as formatFreq,
+ formatFrequencyForHumans as formatFreqForHumans,
+ formatFrequencyForStep as formatFreqForStep,
+ formatWavelength,
+ parseFrequencyInput,
+} from "./core/format.js";
// --- Decoder registry (fetched from /decoders on load) ---
void loadDecoderRegistry(refreshOperatorLayoutCapabilities);
@@ -1246,19 +1255,6 @@ let aboutUptimeStart = null;
let es;
let esHeartbeat;
-function formatUptime(ms) {
- const s = Math.floor(ms / 1000);
- const d = Math.floor(s / 86400);
- const h = Math.floor((s % 86400) / 3600);
- const m = Math.floor((s % 3600) / 60);
- const sec = s % 60;
- const parts = [];
- if (d > 0) parts.push(`${d}d`);
- if (h > 0 || d > 0) parts.push(`${h}h`);
- parts.push(`${m}m`);
- parts.push(`${sec}s`);
- return parts.join(" ");
-}
setInterval(() => {
if (!aboutUptimeStart) return;
const el = document.getElementById("about-uptime");
@@ -1738,32 +1734,6 @@ function waterfallLutWrite(texData, offset, db) {
texData[offset + 3] = _wfLut[p + 3];
}
-function formatFreq(hz) {
- if (!Number.isFinite(hz)) return "--";
- if (hz >= 1_000_000_000) {
- return `${(hz / 1_000_000_000).toFixed(3)} GHz`;
- }
- if (hz >= 10_000_000) {
- return `${(hz / 1_000_000).toFixed(3)} MHz`;
- }
- return `${(hz / 1_000).toFixed(1)} kHz`;
-}
-
-function formatFreqForStep(hz, step) {
- if (!Number.isFinite(hz)) return "--";
- if (step >= 1_000_000) return (hz / 1_000_000).toFixed(6);
- if (step >= 1_000) return (hz / 1_000).toFixed(3);
- if (step >= 1) return String(Math.round(hz));
- return formatFreq(hz);
-}
-
-function formatWavelength(hz) {
- if (!Number.isFinite(hz) || hz <= 0) return "--";
- const meters = 299_792_458 / hz;
- if (meters >= 1) return `${Math.round(meters)} m`;
- return `${Math.round(meters * 100)} cm`;
-}
-
function refreshWavelengthDisplay(hz) {
if (!wavelengthEl) return;
wavelengthEl.textContent = formatWavelength(hz);
@@ -2602,55 +2572,6 @@ function refreshCenterFreqDisplay() {
centerFreqEl.value = formatFreqForStep(lastSpectrumData.center_hz, jogUnit);
}
-function parseFreqInput(val, defaultStep) {
- if (!val) return null;
- const trimmed = val.trim().toLowerCase();
- const match = trimmed.match(/^([0-9]+(?:[.,][0-9]+)?)\s*([kmg]hz|[kmg]|hz)?$/);
- if (!match) return null;
- const rawNumber = match[1];
- let num = parseFloat(rawNumber.replace(",", "."));
- const unit = match[2] || "";
- if (Number.isNaN(num)) return null;
- if (unit.startsWith("gh") || unit === "g") {
- num *= 1_000_000_000;
- } else if (unit.startsWith("mh") || unit === "m") {
- num *= 1_000_000;
- } else if (unit.startsWith("kh") || unit === "k") {
- num *= 1_000;
- } else if (!unit) {
- const mode = (modeEl?.value || "").toUpperCase();
- const hasDecimalSeparator = rawNumber.includes(".") || rawNumber.includes(",");
- if (mode === "WFM") {
- if (hasDecimalSeparator && num >= 50 && num < 200) {
- num *= 1_000_000;
- return Math.round(num);
- }
- if (!hasDecimalSeparator && num >= 875 && num <= 1080) {
- num = (num / 10) * 1_000_000;
- return Math.round(num);
- }
- }
- // Use currently selected input unit when user omits suffix.
- if (defaultStep >= 1_000_000) {
- num *= 1_000_000;
- } else if (defaultStep >= 1_000) {
- num *= 1_000;
- } else if (defaultStep >= 1) {
- // already Hz
- } else {
- // Fallback heuristic.
- if (num >= 1_000_000) {
- // Assume already Hz.
- } else if (num >= 1_000) {
- num *= 1_000;
- } else {
- num *= 1_000_000;
- }
- }
- }
- return Math.round(num);
-}
-
function normalizeMinFreqStep(cap) {
const val = Number(cap && cap.min_freq_step_hz);
if (!Number.isFinite(val) || val < 1) return 1;
@@ -2740,14 +2661,6 @@ function unsupportedBandSummary() {
return `Supported ranges: ${ranges.join(", ")}`;
}
-function formatFreqForHumans(hz) {
- if (!Number.isFinite(hz)) return "--";
- if (hz >= 1_000_000_000) return `${(hz / 1_000_000_000).toFixed(3)} GHz`;
- if (hz >= 1_000_000) return `${(hz / 1_000_000).toFixed(3)} MHz`;
- if (hz >= 1_000) return `${(hz / 1_000).toFixed(3)} kHz`;
- return `${Math.round(hz)} Hz`;
-}
-
function showUnsupportedFreqPopup(hz) {
const message = `Unsupported frequency: ${formatFreqForHumans(hz)}.\n\n${unsupportedBandSummary()}`;
showHint("Out of supported range", 1800);
@@ -3913,7 +3826,7 @@ pttBtn.addEventListener("click", async () => {
});
function applyFreqFromInput() {
- const parsedRaw = parseFreqInput(freqEl.value, jogUnit);
+ const parsedRaw = parseFrequencyInput(freqEl.value, jogUnit, modeEl?.value || "");
const parsed = alignFreqToRigStep(parsedRaw);
if (parsed === null) {
showHint("Freq missing", 1500);
@@ -3930,7 +3843,7 @@ function applyFreqFromInput() {
async function applyCenterFreqFromInput() {
if (!centerFreqEl) return;
- const parsedRaw = parseFreqInput(centerFreqEl.value, jogUnit);
+ const parsedRaw = parseFrequencyInput(centerFreqEl.value, jogUnit, modeEl?.value || "");
const parsed = alignFreqToRigStep(parsedRaw);
if (parsed === null) {
showHint("Central freq missing", 1500);
@@ -5839,12 +5752,6 @@ function renderRecorderActive(list) {
el.innerHTML = html;
}
-function recorderFormatSize(bytes) {
- if (bytes < 1024) return bytes + " B";
- if (bytes < 1048576) return (bytes / 1024).toFixed(1) + " KB";
- return (bytes / 1048576).toFixed(1) + " MB";
-}
-
function recFilterAndSort() {
const filterEl = document.getElementById("recorder-filter");
const sortEl = document.getElementById("recorder-sort");
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/core/format.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/core/format.ts
new file mode 100644
index 00000000..3e163a72
--- /dev/null
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/core/format.ts
@@ -0,0 +1,88 @@
+// SPDX-FileCopyrightText: 2026 Stan Grams
+//
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+export function formatDuration(milliseconds: number): string {
+ const seconds = Math.floor(milliseconds / 1000);
+ const days = Math.floor(seconds / 86400);
+ const hours = Math.floor((seconds % 86400) / 3600);
+ const minutes = Math.floor((seconds % 3600) / 60);
+ const remainder = seconds % 60;
+ const parts: string[] = [];
+ if (days > 0) parts.push(`${days}d`);
+ if (hours > 0 || days > 0) parts.push(`${hours}h`);
+ parts.push(`${minutes}m`, `${remainder}s`);
+ return parts.join(" ");
+}
+
+export function formatFrequency(frequencyHz: number): string {
+ if (!Number.isFinite(frequencyHz)) return "--";
+ if (frequencyHz >= 1_000_000_000) return `${(frequencyHz / 1_000_000_000).toFixed(3)} GHz`;
+ if (frequencyHz >= 10_000_000) return `${(frequencyHz / 1_000_000).toFixed(3)} MHz`;
+ return `${(frequencyHz / 1_000).toFixed(1)} kHz`;
+}
+
+export function formatFrequencyForStep(frequencyHz: number, stepHz: number): string {
+ if (!Number.isFinite(frequencyHz)) return "--";
+ if (stepHz >= 1_000_000) return (frequencyHz / 1_000_000).toFixed(6);
+ if (stepHz >= 1_000) return (frequencyHz / 1_000).toFixed(3);
+ if (stepHz >= 1) return String(Math.round(frequencyHz));
+ return formatFrequency(frequencyHz);
+}
+
+export function formatFrequencyForHumans(frequencyHz: number): string {
+ if (!Number.isFinite(frequencyHz)) return "--";
+ if (frequencyHz >= 1_000_000_000) return `${(frequencyHz / 1_000_000_000).toFixed(3)} GHz`;
+ if (frequencyHz >= 1_000_000) return `${(frequencyHz / 1_000_000).toFixed(3)} MHz`;
+ if (frequencyHz >= 1_000) return `${(frequencyHz / 1_000).toFixed(3)} kHz`;
+ return `${Math.round(frequencyHz)} Hz`;
+}
+
+export function formatWavelength(frequencyHz: number): string {
+ if (!Number.isFinite(frequencyHz) || frequencyHz <= 0) return "--";
+ const meters = 299_792_458 / frequencyHz;
+ return meters >= 1 ? `${Math.round(meters)} m` : `${Math.round(meters * 100)} cm`;
+}
+
+export function parseFrequencyInput(
+ value: string,
+ defaultStepHz: number,
+ mode: string,
+): number | null {
+ if (!value) return null;
+ const match = /^([0-9]+(?:[.,][0-9]+)?)\s*([kmg]hz|[kmg]|hz)?$/
+ .exec(value.trim().toLowerCase());
+ if (!match?.[1]) return null;
+ const rawNumber = match[1];
+ let frequency = Number.parseFloat(rawNumber.replace(",", "."));
+ const unit = match[2] ?? "";
+ if (Number.isNaN(frequency)) return null;
+
+ if (unit.startsWith("gh") || unit === "g") frequency *= 1_000_000_000;
+ else if (unit.startsWith("mh") || unit === "m") frequency *= 1_000_000;
+ else if (unit.startsWith("kh") || unit === "k") frequency *= 1_000;
+ else if (!unit) {
+ const hasDecimalSeparator = rawNumber.includes(".") || rawNumber.includes(",");
+ if (mode.toUpperCase() === "WFM") {
+ if (hasDecimalSeparator && frequency >= 50 && frequency < 200) {
+ return Math.round(frequency * 1_000_000);
+ }
+ if (!hasDecimalSeparator && frequency >= 875 && frequency <= 1080) {
+ return Math.round((frequency / 10) * 1_000_000);
+ }
+ }
+ if (defaultStepHz >= 1_000_000) frequency *= 1_000_000;
+ else if (defaultStepHz >= 1_000) frequency *= 1_000;
+ else if (defaultStepHz < 1) {
+ if (frequency < 1_000) frequency *= 1_000_000;
+ else if (frequency < 1_000_000) frequency *= 1_000;
+ }
+ }
+ return Math.round(frequency);
+}
+
+export function formatByteSize(bytes: number): string {
+ if (bytes < 1024) return `${bytes} B`;
+ if (bytes < 1_048_576) return `${(bytes / 1024).toFixed(1)} KB`;
+ return `${(bytes / 1_048_576).toFixed(1)} MB`;
+}
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/format.test.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/format.test.mjs
new file mode 100644
index 00000000..c44c6c65
--- /dev/null
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/format.test.mjs
@@ -0,0 +1,37 @@
+// SPDX-FileCopyrightText: 2026 Stan Grams
+//
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+import assert from "node:assert/strict";
+import test from "node:test";
+import vm from "node:vm";
+import { build } from "esbuild";
+
+async function loadFormats() {
+ const result = await build({
+ entryPoints: [new URL("../src/core/format.ts", import.meta.url).pathname],
+ bundle: true,
+ format: "cjs",
+ platform: "browser",
+ target: "es2022",
+ write: false,
+ });
+ const module = { exports: {} };
+ vm.runInNewContext(result.outputFiles[0].text, { module, exports: module.exports });
+ return module.exports;
+}
+
+test("frequency parsing respects explicit units and WFM shorthand", async () => {
+ const { parseFrequencyInput } = await loadFormats();
+ assert.equal(parseFrequencyInput("145.500 MHz", 1000, "FM"), 145_500_000);
+ assert.equal(parseFrequencyInput("99.5", 100_000, "WFM"), 99_500_000);
+ assert.equal(parseFrequencyInput("995", 100_000, "WFM"), 99_500_000);
+ assert.equal(parseFrequencyInput("not a frequency", 1000, "USB"), null);
+});
+
+test("frequency and byte formatters handle display boundaries", async () => {
+ const { formatByteSize, formatFrequencyForHumans, formatWavelength } = await loadFormats();
+ assert.equal(formatFrequencyForHumans(145_500_000), "145.500 MHz");
+ assert.equal(formatByteSize(1536), "1.5 KB");
+ assert.equal(formatWavelength(299_792_458), "1 m");
+});