diff --git a/app/gateway.ts b/app/gateway.ts index 619986e..30dddc5 100644 --- a/app/gateway.ts +++ b/app/gateway.ts @@ -41,6 +41,12 @@ const DASHBOARD = "https://dashboard.render.com"; /** A failed read of the workflow ID is tried again after this, not on each poll. */ const WORKFLOW_ID_RETRY_MS = 60_000; +/** The browser revalidates each UI file, so it never mixes two deploys. */ +const noCache: MiddlewareHandler = async (c, next) => { + await next(); + c.header("Cache-Control", "no-cache"); +}; + /** The ID of the workflow for links to the Render Dashboard, if it is known. */ type WorkflowIdReader = (taskRunId: string | null) => string | null; @@ -93,13 +99,17 @@ export function createGateway(): Hono { app.delete("/ui/apps/:runId", (c) => deleteRun(c, "/ui/apps", credentials.username), ); + // Each page and script expects the others from the same deploy. A cached + // script from an earlier deploy looks up elements the new page lacks. + const uiFile = (path: string) => + [uiAuth, noCache, serveStatic({ path: `./public/${path}` })] as const; // Two views of the same UI. Each one has a button that opens the other. - app.get("/", uiAuth, serveStatic({ path: "./public/index.html" })); - app.get("/table", uiAuth, serveStatic({ path: "./public/table.html" })); - app.get("/app.js", uiAuth, serveStatic({ path: "./public/app.js" })); - app.get("/table.js", uiAuth, serveStatic({ path: "./public/table.js" })); - app.get("/runs.js", uiAuth, serveStatic({ path: "./public/runs.js" })); - app.get("/style.css", uiAuth, serveStatic({ path: "./public/style.css" })); + app.get("/", ...uiFile("index.html")); + app.get("/table", ...uiFile("table.html")); + app.get("/app.js", ...uiFile("app.js")); + app.get("/table.js", ...uiFile("table.js")); + app.get("/runs.js", ...uiFile("runs.js")); + app.get("/style.css", ...uiFile("style.css")); return app; } diff --git a/tests/gateway.test.ts b/tests/gateway.test.ts index 7691c37..958b39a 100644 --- a/tests/gateway.test.ts +++ b/tests/gateway.test.ts @@ -266,6 +266,16 @@ describe("browser UI", () => { }, ); + it.each(["/", "/table", "/table.js", "/runs.js", "/app.js", "/style.css"])( + "makes the browser revalidate %s after each deploy", + async (path) => { + const response = await createGateway().request(path, { + headers: { authorization }, + }); + expect(response.headers.get("cache-control")).toBe("no-cache"); + }, + ); + it("explains each run stage in the table view, with its time and its dashboard links", async () => { const { RUN_STAGES } = await vi.importActual("../app/store.js");