[fix](trx-frontend-http): serve the band plan to every session
/bandplan.json needed the control role. Route access is decided by suffix for static assets — .js, .css, .png and so on — and ".json" is not among them, so the band plan matched nothing and fell through to the catch-all. It is compiled into the binary and identical for every user, so it is public now, like the rest of them. Two things followed from that. Read-only sessions never saw a band plan at all. And since the page asks for it during startup, the request can land before the session is established: that 401 was swallowed by an empty catch and never retried, which is why the allocations sometimes only appeared after a manual reload. So the client no longer hides the failure, retries once the auth gate clears — which is exactly when a startup 401 becomes fixable — and schedules a draw when the data lands, since the strip is painted from the spectrum draw and a rig sitting between frames would stay blank. The fixture can now refuse the first request the way the server did, and the spectrum layout test holds the client to recovering from it. Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -1839,6 +1839,7 @@ function hideAuthGate() {
|
|||||||
});
|
});
|
||||||
navigateToTab(tabFromPath2(), { updateHistory: false, replaceHistory: true });
|
navigateToTab(tabFromPath2(), { updateHistory: false, replaceHistory: true });
|
||||||
syncTopBarAccess();
|
syncTopBarAccess();
|
||||||
|
if (!bandplanData) void loadBandplanJson();
|
||||||
}
|
}
|
||||||
function showAuthError(msg) {
|
function showAuthError(msg) {
|
||||||
const el = requiredElement("auth-error");
|
const el = requiredElement("auth-error");
|
||||||
@@ -9353,8 +9354,8 @@ var bandplanCacheKey = "";
|
|||||||
var bandplanStripEl = document.getElementById("spectrum-bandplan-strip");
|
var bandplanStripEl = document.getElementById("spectrum-bandplan-strip");
|
||||||
var bandplanRegionSelect = document.getElementById("bandplan-region-select");
|
var bandplanRegionSelect = document.getElementById("bandplan-region-select");
|
||||||
var bandplanLabelsCheck = document.getElementById("bandplan-labels-check");
|
var bandplanLabelsCheck = document.getElementById("bandplan-labels-check");
|
||||||
(function loadBandplanJson() {
|
function loadBandplanJson() {
|
||||||
fetch("/bandplan.json").then(async (response) => {
|
return fetch("/bandplan.json").then(async (response) => {
|
||||||
if (!response.ok) throw new Error(String(response.status));
|
if (!response.ok) throw new Error(String(response.status));
|
||||||
return await responseJsonUnknown(response);
|
return await responseJsonUnknown(response);
|
||||||
}).then((data) => {
|
}).then((data) => {
|
||||||
@@ -9362,9 +9363,12 @@ var bandplanLabelsCheck = document.getElementById("bandplan-labels-check");
|
|||||||
bandplanData = data;
|
bandplanData = data;
|
||||||
bandplanSegmentsCache = null;
|
bandplanSegmentsCache = null;
|
||||||
bandplanCacheKey = "";
|
bandplanCacheKey = "";
|
||||||
}).catch(() => {
|
if (lastSpectrumData) scheduleSpectrumDraw();
|
||||||
|
}).catch((err) => {
|
||||||
|
console.warn("Band plan unavailable", err);
|
||||||
});
|
});
|
||||||
})();
|
}
|
||||||
|
void loadBandplanJson();
|
||||||
if (bandplanRegionSelect) {
|
if (bandplanRegionSelect) {
|
||||||
bandplanRegionSelect.value = bandplanRegion;
|
bandplanRegionSelect.value = bandplanRegion;
|
||||||
bandplanRegionSelect.addEventListener("change", () => {
|
bandplanRegionSelect.addEventListener("change", () => {
|
||||||
|
|||||||
@@ -477,6 +477,8 @@ function hideAuthGate() {
|
|||||||
});
|
});
|
||||||
navigateToTab(tabFromPath(), { updateHistory: false, replaceHistory: true });
|
navigateToTab(tabFromPath(), { updateHistory: false, replaceHistory: true });
|
||||||
syncTopBarAccess();
|
syncTopBarAccess();
|
||||||
|
// The startup fetch may have run before there was a session to authorise it.
|
||||||
|
if (!bandplanData) void loadBandplanJson();
|
||||||
}
|
}
|
||||||
|
|
||||||
function showAuthError(msg: string) {
|
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 bandplanRegionSelect = document.getElementById("bandplan-region-select") as HTMLSelectElement | null;
|
||||||
const bandplanLabelsCheck = document.getElementById("bandplan-labels-check") as HTMLInputElement | null;
|
const bandplanLabelsCheck = document.getElementById("bandplan-labels-check") as HTMLInputElement | null;
|
||||||
|
|
||||||
(function loadBandplanJson() {
|
// Fired at startup, and again once the session exists. The first attempt can
|
||||||
fetch("/bandplan.json")
|
// 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<void> {
|
||||||
|
return fetch("/bandplan.json")
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
if (!response.ok) throw new Error(String(response.status));
|
if (!response.ok) throw new Error(String(response.status));
|
||||||
return await responseJsonUnknown(response);
|
return await responseJsonUnknown(response);
|
||||||
@@ -8614,9 +8621,13 @@ const bandplanLabelsCheck = document.getElementById("bandplan-labels-check") as
|
|||||||
bandplanData = data as BandplanData;
|
bandplanData = data as BandplanData;
|
||||||
bandplanSegmentsCache = null;
|
bandplanSegmentsCache = null;
|
||||||
bandplanCacheKey = "";
|
bandplanCacheKey = "";
|
||||||
|
if (lastSpectrumData) scheduleSpectrumDraw();
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch((err) => {
|
||||||
})();
|
console.warn("Band plan unavailable", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
void loadBandplanJson();
|
||||||
|
|
||||||
if (bandplanRegionSelect) {
|
if (bandplanRegionSelect) {
|
||||||
bandplanRegionSelect.value = bandplanRegion;
|
bandplanRegionSelect.value = bandplanRegion;
|
||||||
|
|||||||
@@ -125,3 +125,30 @@ try {
|
|||||||
await browser.close();
|
await browser.close();
|
||||||
await fixture.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();
|
||||||
|
}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export async function startWebFixture({
|
|||||||
bookmarks = [],
|
bookmarks = [],
|
||||||
bandplan = {},
|
bandplan = {},
|
||||||
bandplanEnabled = false,
|
bandplanEnabled = false,
|
||||||
|
bandplanUnauthorizedFirst = false,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const rigItems = ["rig-a", "rig-b"].map((remote) => ({
|
const rigItems = ["rig-a", "rig-b"].map((remote) => ({
|
||||||
remote,
|
remote,
|
||||||
@@ -83,6 +84,7 @@ export async function startWebFixture({
|
|||||||
const rigsResponse = { rigs: rigItems, active_remote: "rig-a" };
|
const rigsResponse = { rigs: rigItems, active_remote: "rig-a" };
|
||||||
const selectedRigs = [];
|
const selectedRigs = [];
|
||||||
const state = { centerHz: 7074000 };
|
const state = { centerHz: 7074000 };
|
||||||
|
let bandplanServed = false;
|
||||||
|
|
||||||
const status = {
|
const status = {
|
||||||
info: {
|
info: {
|
||||||
@@ -169,6 +171,14 @@ export async function startWebFixture({
|
|||||||
response.writeHead(200).end();
|
response.writeHead(200).end();
|
||||||
return;
|
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)) {
|
if (jsonRoutes.has(url.pathname)) {
|
||||||
response.writeHead(200, { "content-type": "application/json" });
|
response.writeHead(200, { "content-type": "application/json" });
|
||||||
response.end(JSON.stringify(jsonRoutes.get(url.pathname)));
|
response.end(JSON.stringify(jsonRoutes.get(url.pathname)));
|
||||||
|
|||||||
@@ -504,7 +504,14 @@ impl RouteAccess {
|
|||||||
return Self::Public;
|
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")
|
if path.starts_with("/style.css")
|
||||||
|| path.starts_with("/app.js")
|
|| path.starts_with("/app.js")
|
||||||
|| path.ends_with(".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("/auth/logout"), RouteAccess::Public);
|
||||||
assert_eq!(RouteAccess::from_path("/style.css"), RouteAccess::Public);
|
assert_eq!(RouteAccess::from_path("/style.css"), RouteAccess::Public);
|
||||||
assert_eq!(RouteAccess::from_path("/app.js"), 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]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user