From 347d4ddad81b4b6fa3d0f9fe3a40400644b977e1 Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Fri, 18 Sep 2026 08:54:06 +0200 Subject: [PATCH] feat(ui): differentiate pull vs site update blame in colophon staleness notice Show whether staleness is due to crawler checks or site updates in Colophon, and add an expandable per-source breakdown. --- docs/PRD.md | 5 ++ src/client/components/Colophon.tsx | 80 ++++++++++++++++++++++++++++-- test/custom-ui.test.tsx | 27 ++++++++++ test/issue-templates.test.tsx | 2 + 4 files changed, 110 insertions(+), 4 deletions(-) diff --git a/docs/PRD.md b/docs/PRD.md index 0883ff0..2941c41 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -462,6 +462,11 @@ counted, in the same words the empty list uses — *the games you have switched column beside it is owed to every source we read and is not scoped; nor is the "last refreshed" line, which reports the feed and feeds the bug form. +**The staleness notice differentiates crawler checks from site updates.** If our crawler successfully +checked the wiki recently (e.g. confirming HTTP 304) but the site's content has not changed in over +two days, the notice explicitly distinguishes the two: "data pulled X ago, site updated Y ago", with +an expandable breakdown detailing each source's check and content update times. + **F15 — Light mode, with dark still the default.** The app is a lit instrument panel and that is what it should be on first sight, but it is also read on a train in daylight and by people who find a dark UI harder rather than moodier. A public fork diff --git a/src/client/components/Colophon.tsx b/src/client/components/Colophon.tsx index f5db915..0e31dc2 100644 --- a/src/client/components/Colophon.tsx +++ b/src/client/components/Colophon.tsx @@ -1,6 +1,6 @@ import { useGameMeta } from "../state/gameMeta.tsx"; import type { LaneId } from "../../shared/custom.ts"; -import { freshness, type SourceHealth } from "../../shared/feed.ts"; +import { freshness, STALE_AFTER_MS, type SourceHealth, type StaleGame } from "../../shared/feed.ts"; import { formatAbsolute, formatRemaining } from "../../shared/time.ts"; export const REPO_URL = "https://github.com/StereotypicalCat/gacha-event-tracker"; @@ -124,6 +124,29 @@ function siteFor(url: string): { name: string; url: string } { } } +function formatStaleAge(s: StaleGame, now: number): string { + if (s.lastSuccessAt === null && s.contentChangedAt === null) { + return " (never)"; + } + + const contentDate = s.contentChangedAt ?? s.lastSuccessAt; + const contentAgo = contentDate + ? `${formatRemaining(now - Date.parse(contentDate))} ago` + : null; + + // Differentiate blame when crawler confirmed the source recently (<= STALE_AFTER_MS) + // but the site content has not changed in over 2 days. + if (s.lastConfirmedAt !== null) { + const confirmedAge = now - Date.parse(s.lastConfirmedAt); + if (confirmedAge <= STALE_AFTER_MS) { + const confirmedAgo = `${formatRemaining(confirmedAge)} ago`; + return ` (data pulled ${confirmedAgo}, site updated ${contentAgo ?? "unknown"})`; + } + } + + return contentAgo ? ` (${contentAgo})` : " (never)"; +} + /** * Credit, disclaimer, and where the code lives. * @@ -253,9 +276,7 @@ export function Colophon({ {i > 0 && (i === shown.length - 1 && shownStale.length <= STALE_NAMES ? " and " : ", ")} {gameMeta(s.game).name} - {s.lastSuccessAt === null - ? " (never)" - : ` (${formatRemaining(now - Date.parse(s.lastSuccessAt))} ago)`} + {formatStaleAge(s, now)} ))} {shownStale.length > STALE_NAMES && @@ -267,6 +288,57 @@ export function Colophon({ )}

)} + + {shownStale.some((s) => s.sources && s.sources.length > 0) && ( +
+ + Source breakdown + + +
+ )}
diff --git a/test/custom-ui.test.tsx b/test/custom-ui.test.tsx index 500af49..875dc04 100644 --- a/test/custom-ui.test.tsx +++ b/test/custom-ui.test.tsx @@ -237,6 +237,8 @@ describe("Colophon freshness notice (PRD F7)", () => { game: "genshin" as const, url: "https://game8.co/games/Genshin-Impact/archives/301601", lastSuccessAt: new Date(NOW - 3 * HOUR).toISOString(), + lastConfirmedAt: null, + contentChangedAt: null, eventCount: 9, parsedCount: 9, @@ -277,6 +279,31 @@ describe("Colophon freshness notice (PRD F7)", () => { expect(html).toContain("3h 0m ago"); }); + test("differentiates blame when data was pulled recently but site has not changed", () => { + const html = renderToStaticMarkup( + , + ); + expect(html).toContain("Arknights: Endfield"); + expect(html).toContain("data pulled 11h 0m ago, site updated 2d 9h ago"); + expect(html).toContain("Source breakdown"); + expect(html).toContain("wiki.gg"); + expect(html).toContain("site has no new updates"); + }); + test("summarises instead of listing when every game is behind", () => { // What a refresh that stopped running looks like. Ten names each repeating // the same age is less readable than the count this replaced. diff --git a/test/issue-templates.test.tsx b/test/issue-templates.test.tsx index 67a12d8..1e46197 100644 --- a/test/issue-templates.test.tsx +++ b/test/issue-templates.test.tsx @@ -165,6 +165,8 @@ describe("the app's links into them", () => { game: "genshin", url: "https://game8.co/games/Genshin-Impact/archives/301601", lastSuccessAt: new Date(NOW - 3 * 60 * 60 * 1000).toISOString(), + lastConfirmedAt: null, + contentChangedAt: new Date(NOW - 3 * 60 * 60 * 1000).toISOString(), eventCount: 9, parsedCount: 9,