[feat](trx-frontend-http): add bandplan strip above spectrum waterfall

Add a bandplan display strip that shows IARU frequency allocations
(CW, Phone, Digital, FM, Beacon, Satellite) above the spectrum plot.
Includes IARU Region 1/2/3 data for all HF/VHF/UHF bands, a settings
submenu for region selection and label toggle, and color-coded segments
that pan/zoom with the spectrum view.

https://claude.ai/code/session_01AyBktp6b8qFjchyyqwL7dv
Signed-off-by: Claude <noreply@anthropic.com>
This commit is contained in:
Claude
2026-03-29 21:42:59 +00:00
committed by Stan Grams
parent 4aae2fa725
commit 4c095e64f0
7 changed files with 700 additions and 0 deletions
@@ -9908,6 +9908,7 @@ function drawSpectrum(data) {
updateSpectrumFreqAxis(range); updateSpectrumFreqAxis(range);
updateBookmarkAxis(range); updateBookmarkAxis(range);
updateBandplanStrip(range);
drawSignalOverlay(); drawSignalOverlay();
} }
@@ -11226,4 +11227,150 @@ if (spectrumCenterRightBtn) {
if (lastSpectrumData) scheduleSpectrumDraw(); if (lastSpectrumData) scheduleSpectrumDraw();
}); });
} }
// ── Bandplan strip ──────────────────────────────────────────────────────────
let bandplanData = null;
let bandplanRegion = loadSetting("bandplanRegion", "off");
let bandplanShowLabels = loadSetting("bandplanLabels", true);
let bandplanSegmentsCache = null;
let bandplanCacheKey = "";
const bandplanStripEl = document.getElementById("spectrum-bandplan-strip");
const bandplanRegionSelect = document.getElementById("bandplan-region-select");
const bandplanLabelsCheck = document.getElementById("bandplan-labels-check");
(function loadBandplanJson() {
fetch("/bandplan.json")
.then((r) => { if (!r.ok) throw new Error(r.status); return r.json(); })
.then((d) => { bandplanData = d; bandplanSegmentsCache = null; bandplanCacheKey = ""; })
.catch(() => {});
})();
if (bandplanRegionSelect) {
bandplanRegionSelect.value = bandplanRegion;
bandplanRegionSelect.addEventListener("change", () => {
bandplanRegion = bandplanRegionSelect.value;
saveSetting("bandplanRegion", bandplanRegion);
bandplanSegmentsCache = null;
bandplanCacheKey = "";
if (lastSpectrumData) scheduleSpectrumDraw();
});
}
if (bandplanLabelsCheck) {
bandplanLabelsCheck.checked = bandplanShowLabels;
bandplanLabelsCheck.addEventListener("change", () => {
bandplanShowLabels = bandplanLabelsCheck.checked;
saveSetting("bandplanLabels", bandplanShowLabels);
bandplanSegmentsCache = null;
bandplanCacheKey = "";
if (lastSpectrumData) scheduleSpectrumDraw();
});
}
function bandplanVisibleSegments(region, loHz, hiHz) {
if (!bandplanData || !bandplanData[region]) return [];
const bands = bandplanData[region].bands;
const result = [];
for (const band of bands) {
if (band.high_hz < loHz || band.low_hz > hiHz) continue;
for (const seg of band.segments) {
if (seg.high_hz <= loHz || seg.low_hz >= hiHz) continue;
result.push({
low_hz: seg.low_hz,
high_hz: seg.high_hz,
mode: seg.mode,
label: seg.label,
band: band.name,
});
}
}
return result;
}
function updateBandplanStrip(range) {
if (!bandplanStripEl) return;
if (bandplanRegion === "off" || !bandplanData) {
if (bandplanStripEl.classList.contains("bp-visible")) {
bandplanStripEl.classList.remove("bp-visible");
bandplanStripEl.innerHTML = "";
bandplanCacheKey = "";
}
return;
}
const segments = bandplanVisibleSegments(bandplanRegion, range.visLoHz, range.visHiHz);
if (segments.length === 0) {
if (bandplanStripEl.classList.contains("bp-visible")) {
bandplanStripEl.classList.remove("bp-visible");
bandplanStripEl.innerHTML = "";
bandplanCacheKey = "";
}
return;
}
bandplanStripEl.classList.add("bp-visible");
const newKey = bandplanRegion + ":" + (bandplanShowLabels ? "L" : "N") + ":" +
segments.map((s) => s.low_hz + "-" + s.high_hz).join(",");
const stripW = bandplanStripEl.clientWidth || 1;
if (bandplanCacheKey !== newKey) {
bandplanCacheKey = newKey;
bandplanStripEl.innerHTML = "";
const seenBands = new Set();
for (const seg of segments) {
const el = document.createElement("div");
el.className = "bp-segment";
el.dataset.mode = seg.mode;
el.title = seg.band + " \u2013 " + seg.label + " (" + seg.mode + ")";
if (bandplanShowLabels) {
const lbl = document.createElement("span");
lbl.className = "bp-segment-label";
lbl.textContent = seg.label;
el.appendChild(lbl);
}
bandplanStripEl.appendChild(el);
if (!seenBands.has(seg.band)) {
seenBands.add(seg.band);
const bandLbl = document.createElement("div");
bandLbl.className = "bp-band-label";
bandLbl.textContent = seg.band;
bandLbl.dataset.bandLow = seg.low_hz;
bandplanStripEl.appendChild(bandLbl);
}
}
bandplanSegmentsCache = segments;
}
const children = bandplanStripEl.querySelectorAll(".bp-segment");
const bandLabels = bandplanStripEl.querySelectorAll(".bp-band-label");
const segs = bandplanSegmentsCache || segments;
segs.forEach((seg, i) => {
const el = children[i];
if (!el) return;
const l = Math.max(0, (seg.low_hz - range.visLoHz) / range.visSpanHz);
const r = Math.min(1, (seg.high_hz - range.visLoHz) / range.visSpanHz);
const leftPx = l * stripW;
const widthPx = Math.max(1, (r - l) * stripW);
el.style.left = leftPx + "px";
el.style.width = widthPx + "px";
const lbl = el.querySelector(".bp-segment-label");
if (lbl) {
lbl.style.display = widthPx < 20 ? "none" : "";
}
});
bandLabels.forEach((lbl) => {
const bandLow = Number(lbl.dataset.bandLow);
const frac = (bandLow - range.visLoHz) / range.visSpanHz;
const px = Math.max(2, frac * stripW);
lbl.style.left = px + "px";
lbl.style.display = (frac < -0.1 || frac > 1.05) ? "none" : "";
});
}
})(); })();
@@ -0,0 +1,408 @@
{
"iaru_r1": {
"name": "IARU Region 1",
"bands": [
{
"name": "160m", "low_hz": 1810000, "high_hz": 2000000,
"segments": [
{ "low_hz": 1810000, "high_hz": 1838000, "mode": "CW", "label": "CW" },
{ "low_hz": 1838000, "high_hz": 1840000, "mode": "Narrow", "label": "Narrow" },
{ "low_hz": 1840000, "high_hz": 2000000, "mode": "All", "label": "All Modes" }
]
},
{
"name": "80m", "low_hz": 3500000, "high_hz": 3800000,
"segments": [
{ "low_hz": 3500000, "high_hz": 3570000, "mode": "CW", "label": "CW" },
{ "low_hz": 3570000, "high_hz": 3600000, "mode": "Narrow", "label": "Narrow/Digi" },
{ "low_hz": 3600000, "high_hz": 3620000, "mode": "All", "label": "All Modes" },
{ "low_hz": 3620000, "high_hz": 3800000, "mode": "Phone", "label": "Phone" }
]
},
{
"name": "60m", "low_hz": 5351500, "high_hz": 5366500,
"segments": [
{ "low_hz": 5351500, "high_hz": 5354000, "mode": "CW", "label": "CW" },
{ "low_hz": 5354000, "high_hz": 5366500, "mode": "All", "label": "All Modes" }
]
},
{
"name": "40m", "low_hz": 7000000, "high_hz": 7200000,
"segments": [
{ "low_hz": 7000000, "high_hz": 7040000, "mode": "CW", "label": "CW" },
{ "low_hz": 7040000, "high_hz": 7060000, "mode": "Narrow", "label": "Narrow/Digi" },
{ "low_hz": 7060000, "high_hz": 7100000, "mode": "All", "label": "All Modes" },
{ "low_hz": 7100000, "high_hz": 7200000, "mode": "Phone", "label": "Phone" }
]
},
{
"name": "30m", "low_hz": 10100000, "high_hz": 10150000,
"segments": [
{ "low_hz": 10100000, "high_hz": 10140000, "mode": "CW", "label": "CW" },
{ "low_hz": 10140000, "high_hz": 10150000, "mode": "Narrow", "label": "Narrow/Digi" }
]
},
{
"name": "20m", "low_hz": 14000000, "high_hz": 14350000,
"segments": [
{ "low_hz": 14000000, "high_hz": 14070000, "mode": "CW", "label": "CW" },
{ "low_hz": 14070000, "high_hz": 14099000, "mode": "Narrow", "label": "Narrow/Digi" },
{ "low_hz": 14099000, "high_hz": 14101000, "mode": "Beacon", "label": "Beacon" },
{ "low_hz": 14101000, "high_hz": 14112000, "mode": "All", "label": "All Modes" },
{ "low_hz": 14112000, "high_hz": 14350000, "mode": "Phone", "label": "Phone" }
]
},
{
"name": "17m", "low_hz": 18068000, "high_hz": 18168000,
"segments": [
{ "low_hz": 18068000, "high_hz": 18095000, "mode": "CW", "label": "CW" },
{ "low_hz": 18095000, "high_hz": 18109000, "mode": "Narrow", "label": "Narrow/Digi" },
{ "low_hz": 18109000, "high_hz": 18111000, "mode": "Beacon", "label": "Beacon" },
{ "low_hz": 18111000, "high_hz": 18168000, "mode": "Phone", "label": "Phone" }
]
},
{
"name": "15m", "low_hz": 21000000, "high_hz": 21450000,
"segments": [
{ "low_hz": 21000000, "high_hz": 21070000, "mode": "CW", "label": "CW" },
{ "low_hz": 21070000, "high_hz": 21149000, "mode": "Narrow", "label": "Narrow/Digi" },
{ "low_hz": 21149000, "high_hz": 21151000, "mode": "Beacon", "label": "Beacon" },
{ "low_hz": 21151000, "high_hz": 21450000, "mode": "Phone", "label": "Phone" }
]
},
{
"name": "12m", "low_hz": 24890000, "high_hz": 24990000,
"segments": [
{ "low_hz": 24890000, "high_hz": 24915000, "mode": "CW", "label": "CW" },
{ "low_hz": 24915000, "high_hz": 24929000, "mode": "Narrow", "label": "Narrow/Digi" },
{ "low_hz": 24929000, "high_hz": 24931000, "mode": "Beacon", "label": "Beacon" },
{ "low_hz": 24931000, "high_hz": 24990000, "mode": "Phone", "label": "Phone" }
]
},
{
"name": "10m", "low_hz": 28000000, "high_hz": 29700000,
"segments": [
{ "low_hz": 28000000, "high_hz": 28070000, "mode": "CW", "label": "CW" },
{ "low_hz": 28070000, "high_hz": 28190000, "mode": "Narrow", "label": "Narrow/Digi" },
{ "low_hz": 28190000, "high_hz": 28225000, "mode": "Beacon", "label": "Beacon" },
{ "low_hz": 28225000, "high_hz": 28320000, "mode": "All", "label": "All Modes" },
{ "low_hz": 28320000, "high_hz": 29100000, "mode": "Phone", "label": "Phone" },
{ "low_hz": 29100000, "high_hz": 29510000, "mode": "FM", "label": "FM" },
{ "low_hz": 29510000, "high_hz": 29700000, "mode": "Satellite", "label": "Satellite" }
]
},
{
"name": "6m", "low_hz": 50000000, "high_hz": 54000000,
"segments": [
{ "low_hz": 50000000, "high_hz": 50100000, "mode": "CW", "label": "CW/Beacon" },
{ "low_hz": 50100000, "high_hz": 50500000, "mode": "Phone", "label": "SSB" },
{ "low_hz": 50500000, "high_hz": 51000000, "mode": "All", "label": "All Modes" },
{ "low_hz": 51000000, "high_hz": 52000000, "mode": "FM", "label": "FM Repeaters" },
{ "low_hz": 52000000, "high_hz": 54000000, "mode": "All", "label": "All Modes" }
]
},
{
"name": "2m", "low_hz": 144000000, "high_hz": 146000000,
"segments": [
{ "low_hz": 144000000, "high_hz": 144150000, "mode": "CW", "label": "CW/EME" },
{ "low_hz": 144150000, "high_hz": 144400000, "mode": "Phone", "label": "SSB" },
{ "low_hz": 144400000, "high_hz": 144490000, "mode": "Beacon", "label": "Beacon" },
{ "low_hz": 144490000, "high_hz": 144500000, "mode": "Beacon", "label": "NCDXF Beacon" },
{ "low_hz": 144500000, "high_hz": 144794000, "mode": "All", "label": "All Modes" },
{ "low_hz": 144794000, "high_hz": 144990000, "mode": "Narrow", "label": "Digital/APRS" },
{ "low_hz": 144990000, "high_hz": 145194000, "mode": "FM", "label": "FM Simplex" },
{ "low_hz": 145194000, "high_hz": 145806000, "mode": "FM", "label": "FM Repeaters" },
{ "low_hz": 145806000, "high_hz": 146000000, "mode": "Satellite", "label": "Satellite" }
]
},
{
"name": "70cm", "low_hz": 430000000, "high_hz": 440000000,
"segments": [
{ "low_hz": 430000000, "high_hz": 432000000, "mode": "FM", "label": "FM Repeaters" },
{ "low_hz": 432000000, "high_hz": 432150000, "mode": "CW", "label": "CW/EME" },
{ "low_hz": 432150000, "high_hz": 432500000, "mode": "Phone", "label": "SSB" },
{ "low_hz": 432500000, "high_hz": 432800000, "mode": "All", "label": "All Modes" },
{ "low_hz": 432800000, "high_hz": 433000000, "mode": "Beacon", "label": "Beacon" },
{ "low_hz": 433000000, "high_hz": 435000000, "mode": "FM", "label": "FM" },
{ "low_hz": 435000000, "high_hz": 438000000, "mode": "Satellite", "label": "Satellite" },
{ "low_hz": 438000000, "high_hz": 440000000, "mode": "FM", "label": "FM" }
]
},
{
"name": "23cm", "low_hz": 1240000000, "high_hz": 1300000000,
"segments": [
{ "low_hz": 1240000000, "high_hz": 1243000000, "mode": "All", "label": "All Modes" },
{ "low_hz": 1243000000, "high_hz": 1260000000, "mode": "Narrow", "label": "Digital/ATV" },
{ "low_hz": 1260000000, "high_hz": 1270000000, "mode": "Satellite", "label": "Satellite" },
{ "low_hz": 1270000000, "high_hz": 1300000000, "mode": "All", "label": "All Modes" }
]
}
]
},
"iaru_r2": {
"name": "IARU Region 2",
"bands": [
{
"name": "160m", "low_hz": 1800000, "high_hz": 2000000,
"segments": [
{ "low_hz": 1800000, "high_hz": 1840000, "mode": "CW", "label": "CW" },
{ "low_hz": 1840000, "high_hz": 1850000, "mode": "Narrow", "label": "CW/Digi" },
{ "low_hz": 1850000, "high_hz": 2000000, "mode": "Phone", "label": "Phone" }
]
},
{
"name": "80m", "low_hz": 3500000, "high_hz": 4000000,
"segments": [
{ "low_hz": 3500000, "high_hz": 3570000, "mode": "CW", "label": "CW" },
{ "low_hz": 3570000, "high_hz": 3600000, "mode": "Narrow", "label": "Narrow/Digi" },
{ "low_hz": 3600000, "high_hz": 3700000, "mode": "All", "label": "All Modes" },
{ "low_hz": 3700000, "high_hz": 4000000, "mode": "Phone", "label": "Phone" }
]
},
{
"name": "60m", "low_hz": 5330500, "high_hz": 5403500,
"segments": [
{ "low_hz": 5330500, "high_hz": 5403500, "mode": "All", "label": "All Modes" }
]
},
{
"name": "40m", "low_hz": 7000000, "high_hz": 7300000,
"segments": [
{ "low_hz": 7000000, "high_hz": 7040000, "mode": "CW", "label": "CW" },
{ "low_hz": 7040000, "high_hz": 7060000, "mode": "Narrow", "label": "Narrow/Digi" },
{ "low_hz": 7060000, "high_hz": 7100000, "mode": "All", "label": "All Modes" },
{ "low_hz": 7100000, "high_hz": 7125000, "mode": "All", "label": "All Modes" },
{ "low_hz": 7125000, "high_hz": 7300000, "mode": "Phone", "label": "Phone" }
]
},
{
"name": "30m", "low_hz": 10100000, "high_hz": 10150000,
"segments": [
{ "low_hz": 10100000, "high_hz": 10140000, "mode": "CW", "label": "CW" },
{ "low_hz": 10140000, "high_hz": 10150000, "mode": "Narrow", "label": "Narrow/Digi" }
]
},
{
"name": "20m", "low_hz": 14000000, "high_hz": 14350000,
"segments": [
{ "low_hz": 14000000, "high_hz": 14070000, "mode": "CW", "label": "CW" },
{ "low_hz": 14070000, "high_hz": 14099000, "mode": "Narrow", "label": "Narrow/Digi" },
{ "low_hz": 14099000, "high_hz": 14101000, "mode": "Beacon", "label": "Beacon" },
{ "low_hz": 14101000, "high_hz": 14112000, "mode": "All", "label": "All Modes" },
{ "low_hz": 14112000, "high_hz": 14350000, "mode": "Phone", "label": "Phone" }
]
},
{
"name": "17m", "low_hz": 18068000, "high_hz": 18168000,
"segments": [
{ "low_hz": 18068000, "high_hz": 18095000, "mode": "CW", "label": "CW" },
{ "low_hz": 18095000, "high_hz": 18109000, "mode": "Narrow", "label": "Narrow/Digi" },
{ "low_hz": 18109000, "high_hz": 18111000, "mode": "Beacon", "label": "Beacon" },
{ "low_hz": 18111000, "high_hz": 18168000, "mode": "Phone", "label": "Phone" }
]
},
{
"name": "15m", "low_hz": 21000000, "high_hz": 21450000,
"segments": [
{ "low_hz": 21000000, "high_hz": 21070000, "mode": "CW", "label": "CW" },
{ "low_hz": 21070000, "high_hz": 21149000, "mode": "Narrow", "label": "Narrow/Digi" },
{ "low_hz": 21149000, "high_hz": 21151000, "mode": "Beacon", "label": "Beacon" },
{ "low_hz": 21151000, "high_hz": 21450000, "mode": "Phone", "label": "Phone" }
]
},
{
"name": "12m", "low_hz": 24890000, "high_hz": 24990000,
"segments": [
{ "low_hz": 24890000, "high_hz": 24915000, "mode": "CW", "label": "CW" },
{ "low_hz": 24915000, "high_hz": 24929000, "mode": "Narrow", "label": "Narrow/Digi" },
{ "low_hz": 24929000, "high_hz": 24931000, "mode": "Beacon", "label": "Beacon" },
{ "low_hz": 24931000, "high_hz": 24990000, "mode": "Phone", "label": "Phone" }
]
},
{
"name": "10m", "low_hz": 28000000, "high_hz": 29700000,
"segments": [
{ "low_hz": 28000000, "high_hz": 28070000, "mode": "CW", "label": "CW" },
{ "low_hz": 28070000, "high_hz": 28190000, "mode": "Narrow", "label": "Narrow/Digi" },
{ "low_hz": 28190000, "high_hz": 28225000, "mode": "Beacon", "label": "Beacon" },
{ "low_hz": 28225000, "high_hz": 28300000, "mode": "All", "label": "All Modes" },
{ "low_hz": 28300000, "high_hz": 29100000, "mode": "Phone", "label": "Phone" },
{ "low_hz": 29100000, "high_hz": 29510000, "mode": "FM", "label": "FM" },
{ "low_hz": 29510000, "high_hz": 29700000, "mode": "Satellite", "label": "Satellite" }
]
},
{
"name": "6m", "low_hz": 50000000, "high_hz": 54000000,
"segments": [
{ "low_hz": 50000000, "high_hz": 50100000, "mode": "CW", "label": "CW/Beacon" },
{ "low_hz": 50100000, "high_hz": 50300000, "mode": "Phone", "label": "SSB" },
{ "low_hz": 50300000, "high_hz": 50600000, "mode": "All", "label": "All Modes" },
{ "low_hz": 50600000, "high_hz": 51000000, "mode": "Narrow", "label": "Digital" },
{ "low_hz": 51000000, "high_hz": 54000000, "mode": "FM", "label": "FM" }
]
},
{
"name": "2m", "low_hz": 144000000, "high_hz": 148000000,
"segments": [
{ "low_hz": 144000000, "high_hz": 144100000, "mode": "CW", "label": "CW/EME" },
{ "low_hz": 144100000, "high_hz": 144275000, "mode": "Phone", "label": "SSB" },
{ "low_hz": 144275000, "high_hz": 144400000, "mode": "Beacon", "label": "Beacon/Packet" },
{ "low_hz": 144400000, "high_hz": 145500000, "mode": "FM", "label": "FM Simplex" },
{ "low_hz": 145500000, "high_hz": 146000000, "mode": "FM", "label": "FM Repeaters" },
{ "low_hz": 146000000, "high_hz": 148000000, "mode": "FM", "label": "FM Repeaters" }
]
},
{
"name": "70cm", "low_hz": 420000000, "high_hz": 450000000,
"segments": [
{ "low_hz": 420000000, "high_hz": 426000000, "mode": "All", "label": "All Modes" },
{ "low_hz": 426000000, "high_hz": 432000000, "mode": "FM", "label": "FM Repeaters" },
{ "low_hz": 432000000, "high_hz": 432100000, "mode": "CW", "label": "CW/EME" },
{ "low_hz": 432100000, "high_hz": 433000000, "mode": "Phone", "label": "SSB/All" },
{ "low_hz": 433000000, "high_hz": 435000000, "mode": "FM", "label": "FM/Links" },
{ "low_hz": 435000000, "high_hz": 438000000, "mode": "Satellite", "label": "Satellite" },
{ "low_hz": 438000000, "high_hz": 444000000, "mode": "FM", "label": "FM Repeaters" },
{ "low_hz": 444000000, "high_hz": 450000000, "mode": "FM", "label": "FM Repeaters" }
]
},
{
"name": "23cm", "low_hz": 1240000000, "high_hz": 1300000000,
"segments": [
{ "low_hz": 1240000000, "high_hz": 1260000000, "mode": "All", "label": "All Modes/ATV" },
{ "low_hz": 1260000000, "high_hz": 1270000000, "mode": "Satellite", "label": "Satellite" },
{ "low_hz": 1270000000, "high_hz": 1295000000, "mode": "FM", "label": "FM Repeaters" },
{ "low_hz": 1295000000, "high_hz": 1300000000, "mode": "Narrow", "label": "Narrowband" }
]
}
]
},
"iaru_r3": {
"name": "IARU Region 3",
"bands": [
{
"name": "160m", "low_hz": 1800000, "high_hz": 2000000,
"segments": [
{ "low_hz": 1800000, "high_hz": 1840000, "mode": "CW", "label": "CW" },
{ "low_hz": 1840000, "high_hz": 1850000, "mode": "Narrow", "label": "CW/Digi" },
{ "low_hz": 1850000, "high_hz": 2000000, "mode": "Phone", "label": "Phone" }
]
},
{
"name": "80m", "low_hz": 3500000, "high_hz": 3900000,
"segments": [
{ "low_hz": 3500000, "high_hz": 3570000, "mode": "CW", "label": "CW" },
{ "low_hz": 3570000, "high_hz": 3600000, "mode": "Narrow", "label": "Narrow/Digi" },
{ "low_hz": 3600000, "high_hz": 3620000, "mode": "All", "label": "All Modes" },
{ "low_hz": 3620000, "high_hz": 3900000, "mode": "Phone", "label": "Phone" }
]
},
{
"name": "40m", "low_hz": 7000000, "high_hz": 7300000,
"segments": [
{ "low_hz": 7000000, "high_hz": 7040000, "mode": "CW", "label": "CW" },
{ "low_hz": 7040000, "high_hz": 7060000, "mode": "Narrow", "label": "Narrow/Digi" },
{ "low_hz": 7060000, "high_hz": 7100000, "mode": "All", "label": "All Modes" },
{ "low_hz": 7100000, "high_hz": 7300000, "mode": "Phone", "label": "Phone" }
]
},
{
"name": "30m", "low_hz": 10100000, "high_hz": 10150000,
"segments": [
{ "low_hz": 10100000, "high_hz": 10140000, "mode": "CW", "label": "CW" },
{ "low_hz": 10140000, "high_hz": 10150000, "mode": "Narrow", "label": "Narrow/Digi" }
]
},
{
"name": "20m", "low_hz": 14000000, "high_hz": 14350000,
"segments": [
{ "low_hz": 14000000, "high_hz": 14070000, "mode": "CW", "label": "CW" },
{ "low_hz": 14070000, "high_hz": 14099000, "mode": "Narrow", "label": "Narrow/Digi" },
{ "low_hz": 14099000, "high_hz": 14101000, "mode": "Beacon", "label": "Beacon" },
{ "low_hz": 14101000, "high_hz": 14112000, "mode": "All", "label": "All Modes" },
{ "low_hz": 14112000, "high_hz": 14350000, "mode": "Phone", "label": "Phone" }
]
},
{
"name": "17m", "low_hz": 18068000, "high_hz": 18168000,
"segments": [
{ "low_hz": 18068000, "high_hz": 18095000, "mode": "CW", "label": "CW" },
{ "low_hz": 18095000, "high_hz": 18109000, "mode": "Narrow", "label": "Narrow/Digi" },
{ "low_hz": 18109000, "high_hz": 18111000, "mode": "Beacon", "label": "Beacon" },
{ "low_hz": 18111000, "high_hz": 18168000, "mode": "Phone", "label": "Phone" }
]
},
{
"name": "15m", "low_hz": 21000000, "high_hz": 21450000,
"segments": [
{ "low_hz": 21000000, "high_hz": 21070000, "mode": "CW", "label": "CW" },
{ "low_hz": 21070000, "high_hz": 21149000, "mode": "Narrow", "label": "Narrow/Digi" },
{ "low_hz": 21149000, "high_hz": 21151000, "mode": "Beacon", "label": "Beacon" },
{ "low_hz": 21151000, "high_hz": 21450000, "mode": "Phone", "label": "Phone" }
]
},
{
"name": "12m", "low_hz": 24890000, "high_hz": 24990000,
"segments": [
{ "low_hz": 24890000, "high_hz": 24915000, "mode": "CW", "label": "CW" },
{ "low_hz": 24915000, "high_hz": 24929000, "mode": "Narrow", "label": "Narrow/Digi" },
{ "low_hz": 24929000, "high_hz": 24931000, "mode": "Beacon", "label": "Beacon" },
{ "low_hz": 24931000, "high_hz": 24990000, "mode": "Phone", "label": "Phone" }
]
},
{
"name": "10m", "low_hz": 28000000, "high_hz": 29700000,
"segments": [
{ "low_hz": 28000000, "high_hz": 28070000, "mode": "CW", "label": "CW" },
{ "low_hz": 28070000, "high_hz": 28190000, "mode": "Narrow", "label": "Narrow/Digi" },
{ "low_hz": 28190000, "high_hz": 28225000, "mode": "Beacon", "label": "Beacon" },
{ "low_hz": 28225000, "high_hz": 28300000, "mode": "All", "label": "All Modes" },
{ "low_hz": 28300000, "high_hz": 29100000, "mode": "Phone", "label": "Phone" },
{ "low_hz": 29100000, "high_hz": 29510000, "mode": "FM", "label": "FM" },
{ "low_hz": 29510000, "high_hz": 29700000, "mode": "Satellite", "label": "Satellite" }
]
},
{
"name": "6m", "low_hz": 50000000, "high_hz": 54000000,
"segments": [
{ "low_hz": 50000000, "high_hz": 50100000, "mode": "CW", "label": "CW/Beacon" },
{ "low_hz": 50100000, "high_hz": 50300000, "mode": "Phone", "label": "SSB" },
{ "low_hz": 50300000, "high_hz": 50600000, "mode": "All", "label": "All Modes" },
{ "low_hz": 50600000, "high_hz": 51000000, "mode": "Narrow", "label": "Digital" },
{ "low_hz": 51000000, "high_hz": 54000000, "mode": "FM", "label": "FM" }
]
},
{
"name": "2m", "low_hz": 144000000, "high_hz": 148000000,
"segments": [
{ "low_hz": 144000000, "high_hz": 144100000, "mode": "CW", "label": "CW/EME" },
{ "low_hz": 144100000, "high_hz": 144400000, "mode": "Phone", "label": "SSB" },
{ "low_hz": 144400000, "high_hz": 144500000, "mode": "Beacon", "label": "Beacon" },
{ "low_hz": 144500000, "high_hz": 145000000, "mode": "All", "label": "All Modes" },
{ "low_hz": 145000000, "high_hz": 146000000, "mode": "FM", "label": "FM Simplex" },
{ "low_hz": 146000000, "high_hz": 148000000, "mode": "FM", "label": "FM Repeaters" }
]
},
{
"name": "70cm", "low_hz": 430000000, "high_hz": 450000000,
"segments": [
{ "low_hz": 430000000, "high_hz": 432000000, "mode": "FM", "label": "FM Repeaters" },
{ "low_hz": 432000000, "high_hz": 432100000, "mode": "CW", "label": "CW/EME" },
{ "low_hz": 432100000, "high_hz": 432400000, "mode": "Phone", "label": "SSB" },
{ "low_hz": 432400000, "high_hz": 432500000, "mode": "Beacon", "label": "Beacon" },
{ "low_hz": 432500000, "high_hz": 435000000, "mode": "All", "label": "All Modes" },
{ "low_hz": 435000000, "high_hz": 438000000, "mode": "Satellite", "label": "Satellite" },
{ "low_hz": 438000000, "high_hz": 440000000, "mode": "FM", "label": "FM" },
{ "low_hz": 440000000, "high_hz": 450000000, "mode": "FM", "label": "FM Repeaters" }
]
},
{
"name": "23cm", "low_hz": 1240000000, "high_hz": 1300000000,
"segments": [
{ "low_hz": 1240000000, "high_hz": 1260000000, "mode": "All", "label": "All Modes" },
{ "low_hz": 1260000000, "high_hz": 1270000000, "mode": "Satellite", "label": "Satellite" },
{ "low_hz": 1270000000, "high_hz": 1300000000, "mode": "FM", "label": "FM/ATV" }
]
}
]
}
}
@@ -115,6 +115,7 @@
</div> </div>
<div id="spectrum-panel" style="display:none;"> <div id="spectrum-panel" style="display:none;">
<div class="spectrum-wrap"> <div class="spectrum-wrap">
<div id="spectrum-bandplan-strip" aria-label="Band plan allocations"></div>
<div id="spectrum-bookmark-axis"></div> <div id="spectrum-bookmark-axis"></div>
<div id="spectrum-bookmark-side-left" class="spectrum-bookmark-side spectrum-bookmark-side-left" aria-hidden="true"></div> <div id="spectrum-bookmark-side-left" class="spectrum-bookmark-side spectrum-bookmark-side-left" aria-hidden="true"></div>
<canvas id="spectrum-canvas"></canvas> <canvas id="spectrum-canvas"></canvas>
@@ -991,6 +992,7 @@
<div class="sub-tab-bar"> <div class="sub-tab-bar">
<button class="sub-tab active" data-subtab="settings-scheduler">Scheduler</button> <button class="sub-tab active" data-subtab="settings-scheduler">Scheduler</button>
<button class="sub-tab" data-subtab="settings-background-decode">Background Decode</button> <button class="sub-tab" data-subtab="settings-background-decode">Background Decode</button>
<button class="sub-tab" data-subtab="settings-bandplan">Bandplan</button>
<button class="sub-tab" data-subtab="settings-history">History</button> <button class="sub-tab" data-subtab="settings-history">History</button>
</div> </div>
<div id="subtab-settings-scheduler" class="sub-tab-panel"> <div id="subtab-settings-scheduler" class="sub-tab-panel">
@@ -1204,6 +1206,43 @@
</div> </div>
</div> </div>
</div> </div>
<div id="subtab-settings-bandplan" class="sub-tab-panel" style="display:none;">
<div class="sch-panel">
<div class="sch-section">
<div class="sch-section-title">Bandplan Display</div>
<div class="sch-row">
<label class="sch-label">Region
<select id="bandplan-region-select" class="status-input" aria-label="Bandplan region">
<option value="off">Off</option>
<option value="iaru_r1">IARU Region 1 (Europe, Africa, Middle East, N. Asia)</option>
<option value="iaru_r2">IARU Region 2 (Americas)</option>
<option value="iaru_r3">IARU Region 3 (S/E Asia, Pacific)</option>
</select>
</label>
</div>
<div class="sch-row">
<label class="sch-label">Show Labels
<input type="checkbox" id="bandplan-labels-check" checked aria-label="Show segment labels on bandplan strip" />
</label>
</div>
<div class="sch-row">
<small style="color:var(--text-muted);">The bandplan strip is shown above the spectrum plot when a region is selected and the visible frequency range overlaps a band allocation.</small>
</div>
<div class="sch-section" id="bandplan-legend" style="margin-top:0.5rem;">
<div class="sch-section-title">Legend</div>
<div class="bandplan-legend-grid">
<span class="bandplan-legend-item"><span class="bandplan-legend-swatch" data-mode="CW"></span> CW</span>
<span class="bandplan-legend-item"><span class="bandplan-legend-swatch" data-mode="Phone"></span> Phone / SSB</span>
<span class="bandplan-legend-item"><span class="bandplan-legend-swatch" data-mode="Narrow"></span> Narrow / Digital</span>
<span class="bandplan-legend-item"><span class="bandplan-legend-swatch" data-mode="FM"></span> FM</span>
<span class="bandplan-legend-item"><span class="bandplan-legend-swatch" data-mode="All"></span> All Modes</span>
<span class="bandplan-legend-item"><span class="bandplan-legend-swatch" data-mode="Beacon"></span> Beacon</span>
<span class="bandplan-legend-item"><span class="bandplan-legend-swatch" data-mode="Satellite"></span> Satellite</span>
</div>
</div>
</div>
</div>
</div>
<div id="subtab-settings-history" class="sub-tab-panel" style="display:none;"> <div id="subtab-settings-history" class="sub-tab-panel" style="display:none;">
<div class="sch-panel"> <div class="sch-panel">
<div class="sch-section"> <div class="sch-section">
@@ -3235,6 +3235,98 @@ button:focus-visible, input:focus-visible, select:focus-visible {
cursor: crosshair; cursor: crosshair;
touch-action: none; touch-action: none;
} }
/* ── Bandplan strip ─────────────────────────────────────────────────────────── */
#spectrum-bandplan-strip {
position: relative;
left: 0;
right: 0;
width: 100%;
height: 0;
overflow: hidden;
transition: height 80ms ease;
z-index: 5;
background: var(--bg-secondary, #121828);
border-bottom: 1px solid transparent;
}
#spectrum-bandplan-strip.bp-visible {
height: 18px;
overflow: hidden;
border-bottom-color: var(--border-light, rgba(255,255,255,0.06));
}
.bp-segment {
position: absolute;
top: 0;
height: 100%;
box-sizing: border-box;
border-right: 1px solid var(--bg-secondary, #121828);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
cursor: default;
min-width: 0;
}
.bp-segment-label {
font-size: 0.56rem;
font-weight: 600;
line-height: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 2px;
pointer-events: none;
opacity: 0.92;
}
.bp-segment[data-mode="CW"] { background: rgba(74,144,217,0.55); color: #d0e4ff; }
.bp-segment[data-mode="Phone"] { background: rgba(76,175,80,0.50); color: #d0f5d2; }
.bp-segment[data-mode="Narrow"] { background: rgba(217,74,122,0.50); color: #ffd0e4; }
.bp-segment[data-mode="FM"] { background: rgba(255,152,0,0.50); color: #fff0d0; }
.bp-segment[data-mode="All"] { background: rgba(120,120,120,0.40); color: #ddd; }
.bp-segment[data-mode="Beacon"] { background: rgba(156,39,176,0.50); color: #f0d0ff; }
.bp-segment[data-mode="Satellite"]{ background: rgba(0,188,212,0.50); color: #d0f8ff; }
.bp-band-label {
position: absolute;
top: 0;
height: 100%;
display: flex;
align-items: center;
font-size: 0.54rem;
font-weight: 700;
color: var(--text-muted);
opacity: 0.65;
pointer-events: none;
padding-left: 3px;
z-index: 1;
}
/* Legend in settings */
.bandplan-legend-grid {
display: flex;
flex-wrap: wrap;
gap: 0.6rem 1.2rem;
}
.bandplan-legend-item {
display: inline-flex;
align-items: center;
gap: 0.35rem;
font-size: 0.8rem;
color: var(--text);
}
.bandplan-legend-swatch {
display: inline-block;
width: 18px;
height: 12px;
border-radius: 2px;
}
.bandplan-legend-swatch[data-mode="CW"] { background: rgba(74,144,217,0.75); }
.bandplan-legend-swatch[data-mode="Phone"] { background: rgba(76,175,80,0.70); }
.bandplan-legend-swatch[data-mode="Narrow"] { background: rgba(217,74,122,0.70); }
.bandplan-legend-swatch[data-mode="FM"] { background: rgba(255,152,0,0.70); }
.bandplan-legend-swatch[data-mode="All"] { background: rgba(120,120,120,0.55); }
.bandplan-legend-swatch[data-mode="Beacon"] { background: rgba(156,39,176,0.70); }
.bandplan-legend-swatch[data-mode="Satellite"]{ background: rgba(0,188,212,0.70); }
#spectrum-bookmark-axis { #spectrum-bookmark-axis {
position: absolute; position: absolute;
top: calc(-1 * var(--overview-plot-height)); top: calc(-1 * var(--overview-plot-height));
@@ -65,6 +65,7 @@ define_gz_cache!(
"background-decode.js" "background-decode.js"
); );
define_gz_cache!(gz_vchan_js, status::VCHAN_JS, "vchan.js"); define_gz_cache!(gz_vchan_js, status::VCHAN_JS, "vchan.js");
define_gz_cache!(gz_bandplan_json, status::BANDPLAN_JSON, "bandplan.json");
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// HTML page routes (all serve the SPA index) // HTML page routes (all serve the SPA index)
@@ -350,3 +351,14 @@ pub(crate) async fn vchan_js(req: HttpRequest) -> impl Responder {
&c.etag, &c.etag,
) )
} }
#[get("/bandplan.json")]
pub(crate) async fn bandplan_json(req: HttpRequest) -> impl Responder {
let c = gz_bandplan_json();
static_asset_response(
&req,
"application/json; charset=utf-8",
&c.gz,
&c.etag,
)
}
@@ -630,6 +630,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
.service(assets::sat_scheduler_js) .service(assets::sat_scheduler_js)
.service(assets::background_decode_js) .service(assets::background_decode_js)
.service(assets::vchan_js) .service(assets::vchan_js)
.service(assets::bandplan_json)
// Virtual channels // Virtual channels
.service(vchan::list_channels) .service(vchan::list_channels)
.service(vchan::allocate_channel) .service(vchan::allocate_channel)
@@ -30,6 +30,7 @@ pub const SCHEDULER_JS: &str = include_str!("../assets/web/plugins/scheduler.js"
pub const SAT_SCHEDULER_JS: &str = include_str!("../assets/web/plugins/sat-scheduler.js"); pub const SAT_SCHEDULER_JS: &str = include_str!("../assets/web/plugins/sat-scheduler.js");
pub const BACKGROUND_DECODE_JS: &str = include_str!("../assets/web/plugins/background-decode.js"); pub const BACKGROUND_DECODE_JS: &str = include_str!("../assets/web/plugins/background-decode.js");
pub const VCHAN_JS: &str = include_str!("../assets/web/plugins/vchan.js"); pub const VCHAN_JS: &str = include_str!("../assets/web/plugins/vchan.js");
pub const BANDPLAN_JSON: &str = include_str!("../assets/web/bandplan.json");
/// Build version tag used for cache-busting asset URLs and ETag headers. /// Build version tag used for cache-busting asset URLs and ETag headers.
/// Computed once from `PKG_VERSION` + `CLIENT_BUILD_DATE`. /// Computed once from `PKG_VERSION` + `CLIENT_BUILD_DATE`.