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 30c8ce44..23604021 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 @@ -1839,6 +1839,7 @@ function hideAuthGate() { }); navigateToTab(tabFromPath2(), { updateHistory: false, replaceHistory: true }); syncTopBarAccess(); + if (!bandplanData) void loadBandplanJson(); } function showAuthError(msg) { const el = requiredElement("auth-error"); @@ -9353,8 +9354,8 @@ var bandplanCacheKey = ""; var bandplanStripEl = document.getElementById("spectrum-bandplan-strip"); var bandplanRegionSelect = document.getElementById("bandplan-region-select"); var bandplanLabelsCheck = document.getElementById("bandplan-labels-check"); -(function loadBandplanJson() { - fetch("/bandplan.json").then(async (response) => { +function loadBandplanJson() { + return fetch("/bandplan.json").then(async (response) => { if (!response.ok) throw new Error(String(response.status)); return await responseJsonUnknown(response); }).then((data) => { @@ -9362,9 +9363,12 @@ var bandplanLabelsCheck = document.getElementById("bandplan-labels-check"); bandplanData = data; bandplanSegmentsCache = null; bandplanCacheKey = ""; - }).catch(() => { + if (lastSpectrumData) scheduleSpectrumDraw(); + }).catch((err) => { + console.warn("Band plan unavailable", err); }); -})(); +} +void loadBandplanJson(); if (bandplanRegionSelect) { bandplanRegionSelect.value = bandplanRegion; bandplanRegionSelect.addEventListener("change", () => { diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.ts index fa1c9020..e565561d 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.ts +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.ts @@ -477,6 +477,8 @@ function hideAuthGate() { }); navigateToTab(tabFromPath(), { updateHistory: false, replaceHistory: true }); syncTopBarAccess(); + // The startup fetch may have run before there was a session to authorise it. + if (!bandplanData) void loadBandplanJson(); } function showAuthError(msg: string) { @@ -8603,8 +8605,13 @@ const bandplanStripEl = document.getElementById("spectrum-bandplan-strip"); const bandplanRegionSelect = document.getElementById("bandplan-region-select") as HTMLSelectElement | null; const bandplanLabelsCheck = document.getElementById("bandplan-labels-check") as HTMLInputElement | null; -(function loadBandplanJson() { - fetch("/bandplan.json") +// Fired at startup, and again once the session exists. The first attempt can +// land before the user is authenticated, and it used to fail silently and +// never retry, which is why the band plan sometimes only appeared after a +// manual reload. Redraws on arrival: the strip is painted from the spectrum +// draw, and a rig sitting between frames would otherwise stay blank. +function loadBandplanJson(): Promise { + return fetch("/bandplan.json") .then(async (response) => { if (!response.ok) throw new Error(String(response.status)); return await responseJsonUnknown(response); @@ -8614,9 +8621,13 @@ const bandplanLabelsCheck = document.getElementById("bandplan-labels-check") as bandplanData = data as BandplanData; bandplanSegmentsCache = null; bandplanCacheKey = ""; + if (lastSpectrumData) scheduleSpectrumDraw(); }) - .catch(() => {}); -})(); + .catch((err) => { + console.warn("Band plan unavailable", err); + }); +} +void loadBandplanJson(); if (bandplanRegionSelect) { bandplanRegionSelect.value = bandplanRegion; diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/spectrum-layout.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/spectrum-layout.mjs index f88ad397..de6a39a1 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/spectrum-layout.mjs +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/spectrum-layout.mjs @@ -125,3 +125,30 @@ try { await browser.close(); await fixture.close(); } + +// The band plan is fetched once at startup, which can land before the session +// exists. It used to fail silently and never retry, so the allocations only +// turned up if the operator reloaded the page by hand. +const retryFixture = await startWebFixture({ + spectrum: true, + bandplan: BANDPLAN, + bandplanEnabled: true, + bandplanUnauthorizedFirst: true, +}); +const retry = await startBrowser(chromium); +try { + await retry.page.setViewportSize({ width: 1600, height: 950 }); + await retry.page.goto(retryFixture.origin, { waitUntil: "domcontentloaded" }); + await retry.page.locator("#spectrum-panel").waitFor({ state: "visible" }); + await retry.page.waitForTimeout(2000); + const strip = await retry.page.evaluate(() => { + const element = document.getElementById("spectrum-bandplan-strip"); + return { segments: element.children.length, empty: element.classList.contains("bp-empty") }; + }); + assert.ok(strip.segments > 0, + "the band plan never arrived after its first request was refused"); + assert.equal(strip.empty, false, "the strip is still showing its placeholder"); +} finally { + await retry.browser.close(); + await retryFixture.close(); +} diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/web-fixture.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/web-fixture.mjs index e3f4b444..276ac0cd 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/web-fixture.mjs +++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/web-fixture.mjs @@ -67,6 +67,7 @@ export async function startWebFixture({ bookmarks = [], bandplan = {}, bandplanEnabled = false, + bandplanUnauthorizedFirst = false, } = {}) { const rigItems = ["rig-a", "rig-b"].map((remote) => ({ remote, @@ -83,6 +84,7 @@ export async function startWebFixture({ const rigsResponse = { rigs: rigItems, active_remote: "rig-a" }; const selectedRigs = []; const state = { centerHz: 7074000 }; + let bandplanServed = false; const status = { info: { @@ -169,6 +171,14 @@ export async function startWebFixture({ response.writeHead(200).end(); return; } + // Rejects the first band plan request the way the server did before it was + // classified as a public asset: the page asks for it at startup, which can + // land before the session exists. + if (bandplanUnauthorizedFirst && url.pathname === "/bandplan.json" && !bandplanServed) { + bandplanServed = true; + response.writeHead(401).end(); + return; + } if (jsonRoutes.has(url.pathname)) { response.writeHead(200, { "content-type": "application/json" }); response.end(JSON.stringify(jsonRoutes.get(url.pathname))); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/auth.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/auth.rs index 223ee7c7..b486c8ce 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/auth.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/auth.rs @@ -504,7 +504,14 @@ impl RouteAccess { return Self::Public; } - // Static assets + // Static assets. The band plan is one of them: it is compiled into the + // binary and identical for every user, but ".json" is not an asset + // suffix, so it used to fall through to Control — leaving read-only + // users without a band plan, and everyone else without one whenever the + // page requested it before the session was established. + if path == "/bandplan.json" { + return Self::Public; + } if path.starts_with("/style.css") || path.starts_with("/app.js") || path.ends_with(".js") @@ -696,6 +703,9 @@ mod tests { assert_eq!(RouteAccess::from_path("/auth/logout"), RouteAccess::Public); assert_eq!(RouteAccess::from_path("/style.css"), RouteAccess::Public); assert_eq!(RouteAccess::from_path("/app.js"), RouteAccess::Public); + // Static reference data, served to every role: ".json" is not in the + // asset suffix list, so this one has to be named. + assert_eq!(RouteAccess::from_path("/bandplan.json"), RouteAccess::Public); } #[test]