From ab3173adf00328f55c969714879eec1504d21da5 Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Tue, 13 Oct 2026 14:31:04 +0200 Subject: [PATCH] fix(refresh): don't call a stored copy "nothing newer", and point an entry at its feature When the network failed behind the service worker, it answered from its own store. Check for new data then said nothing newer was published when the server was never reached. The worker now marks that copy, and a fresh check treats it as unreachable; ordinary loads still use it offline. The built-in tasks entry linked to a follow-up fix instead of the feature, and the changelog spec's details now match what shipped. Co-Authored-By: Claude Opus 5.5 --- .../specs/2026-10-13-changelog-design.md | 10 ++++------ src/client/api.ts | 5 +++++ src/client/sw.js | 9 ++++++++- src/shared/changelog.ts | 2 +- test/refresh.test.tsx | 19 +++++++++++++++++++ 5 files changed, 37 insertions(+), 8 deletions(-) diff --git a/docs/superpowers/specs/2026-10-13-changelog-design.md b/docs/superpowers/specs/2026-10-13-changelog-design.md index b33dd30..c2dc303 100644 --- a/docs/superpowers/specs/2026-10-13-changelog-design.md +++ b/docs/superpowers/specs/2026-10-13-changelog-design.md @@ -88,19 +88,17 @@ launch window rather than replaying everything. **What's new card** — top of the main column, above the lists; never in the rail, which holds instructions. Eyebrow `What's new`, up to five entries (`New` / `Fixed` / `Games & sources` tag, -title, date), `See all` (opens the changelog) and `Got it`. More than five shows `+ 3 more` inside -See all. Dismissible, never modal, never shown on first run. +title, date), `See all` (opens the changelog) and `Got it`. More than five reads `See all (3 more)`. Dismissible, never modal, never shown on first run. -**Changelog sheet** — a `What's new` link in the footer beside the existing credits opens a sheet +**Changelog sheet** — a `Changelog` link in the footer beside *Source code* opens a sheet in the detail sheet's style. Release notes, newest first, each with title, date, summary paragraphs, `commits from…to` (compare link) and spec links; the short entries a note covers listed under it with commit links. Entries with no note are listed by month at the end. ## Conventions this adds -AGENTS.md § Conventions gains: **a change a reader can see adds a short entry in the same commit**, -with the ref filled in by a follow-up `chore(changelog)` commit when the change's own hash cannot -be known in advance (a commit cannot contain its own hash). A feature that spans several commits +AGENTS.md § Conventions gains: **a change a reader can see adds a short entry**, in a follow-up +`chore(changelog)` commit naming the change's commit — a commit cannot contain its own hash. A feature that spans several commits gets a release note when it lands, summarising its spec and range. ## Backfill diff --git a/src/client/api.ts b/src/client/api.ts index cb43d54..427aac3 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -29,6 +29,11 @@ export async function fetchFeed( if (!res.ok) { throw new Error(`Feed request failed (${res.status}).`); } + // Asked for the newest data, a copy the service worker served from its own + // store is not an answer from the server: the network failed behind it. + if (options.fresh === true && res.headers.get("x-served-from") === "sw-cache") { + throw new Error("The server could not be reached; this is the stored copy."); + } const json: unknown = await res.json(); const version = (json as { schemaVersion?: number }).schemaVersion; diff --git a/src/client/sw.js b/src/client/sw.js index 4925b50..33646c6 100644 --- a/src/client/sw.js +++ b/src/client/sw.js @@ -146,7 +146,14 @@ async function feedFirst(request) { return response; } catch { const cached = await cache.match(request); - if (cached !== undefined) return cached; + // Marked as the stored copy, so a reader who asked for the newest data + // ("Check for new data") is told the server was not reached rather than + // that nothing newer was published. An ordinary load ignores the mark. + if (cached !== undefined) { + const headers = new Headers(cached.headers); + headers.set("x-served-from", "sw-cache"); + return new Response(cached.body, { status: cached.status, headers }); + } // No network and nothing cached: say so in the feed's own shape, so the // client renders its error state rather than failing to parse. return new Response( diff --git a/src/shared/changelog.ts b/src/shared/changelog.ts index 1792e34..10e7480 100644 --- a/src/shared/changelog.ts +++ b/src/shared/changelog.ts @@ -72,7 +72,7 @@ export const ENTRIES: Entry[] = [ kind: "feature", title: "Built-in weekly tasks for Zenless Zone Zero and Arknights: Endfield", body: "Ridu Weekly (1,300 points) and the Weekly Routine (10 points), each dated by the page it comes from. You're asked once per game; Make it mine copies one into your own tasks to edit.", - ref: "1c7df63", + ref: "624c725", note: "weekly-and-monthly-tasks", }, // 2026-10-13 — weekly and monthly tasks diff --git a/test/refresh.test.tsx b/test/refresh.test.tsx index c000583..cd0e8c1 100644 --- a/test/refresh.test.tsx +++ b/test/refresh.test.tsx @@ -107,3 +107,22 @@ describe("the age right after a refresh", () => { expect(html).not.toContain("ended ago"); }); }); + +describe("review fix: a stored copy is not an answer from the server", () => { + const original = globalThis.fetch; + afterEach(() => { + globalThis.fetch = original; + }); + + test("asked fresh, the service worker's fallback copy counts as not reaching the server", async () => { + globalThis.fetch = (async () => + new Response(JSON.stringify(feed()), { + status: 200, + headers: { "x-served-from": "sw-cache" }, + })) as unknown as typeof fetch; + (globalThis as { document?: unknown }).document ??= { baseURI: "https://example.test/" }; + await expect(fetchFeed(undefined, { fresh: true })).rejects.toThrow(); + // An ordinary load still takes it: offline, the stored copy is the point. + await expect(fetchFeed()).resolves.toBeDefined(); + }); +});