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.
This commit is contained in:
Lucas Winther committed 2026-09-18 08:55:10 +02:00
1 parent 6c09accc20
commit 347d4ddad8
4 files changed
+110 -4

No files matched your search

+76 -4
View File
@@ -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({
<span key={s.game}>
{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)}
</span>
))}
{shownStale.length > STALE_NAMES &&
@@ -267,6 +288,57 @@ export function Colophon({
)}
</p>
)}
{shownStale.some((s) => s.sources && s.sources.length > 0) && (
<details className="mt-2 text-faint">
<summary className="cursor-pointer hover:text-muted focus-visible:outline-none">
Source breakdown
</summary>
<ul className="mt-1.5 space-y-1 border-l border-hairline pl-3">
{shownStale.flatMap((g) =>
g.sources.map((src) => {
const site = siteFor(src.url);
const confirmed = src.lastConfirmedAt
? `${formatRemaining(now - Date.parse(src.lastConfirmedAt))} ago`
: "never";
const updated = src.contentChangedAt
? `${formatRemaining(now - Date.parse(src.contentChangedAt))} ago`
: src.lastSuccessAt
? `${formatRemaining(now - Date.parse(src.lastSuccessAt))} ago`
: "never";
const isPullRecent =
src.lastConfirmedAt !== null &&
now - Date.parse(src.lastConfirmedAt) <= STALE_AFTER_MS;
const isContentStale =
src.contentChangedAt === null ||
now - Date.parse(src.contentChangedAt) > STALE_AFTER_MS;
const diagnosis =
isPullRecent && isContentStale
? "site has no new updates"
: !isPullRecent
? "pull overdue"
: "up to date";
return (
<li key={src.sourceId}>
<span className="text-muted">{gameMeta(g.game).name}</span>
{" — "}
<a
href={src.url}
target="_blank"
rel="noreferrer noopener"
className={LINK}
>
{site.name}
</a>
{`: pulled ${confirmed}, site updated ${updated} (${diagnosis})`}
</li>
);
}),
)}
</ul>
</details>
)}
</div>
<div className="mt-5 lg:mt-0">