feat: add local-only completion and preference state

Completions and prefs live in localStorage and never reach the server —
there is no account and nothing to log into. Reads never throw, so a corrupt
value costs a preference rather than the whole screen.

Import merges and never removes: a completion present on either side stays
completed, because nothing else holds a copy to restore from.

The feed client refuses a schemaVersion it does not know rather than
guessing at unfamiliar fields.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5 committed 2026-08-15 00:28:45 +02:00
1 parent 49d52e3587
commit ac165f0df7
4 files changed
+177

No files matched your search

+34
View File
@@ -0,0 +1,34 @@
import { EventFeed, SCHEMA_VERSION } from "../shared/feed.ts";
export type FeedState =
| { status: "loading" }
| { status: "ready"; feed: EventFeed }
| { status: "error"; message: string };
/**
* Fetch the published feed.
*
* A `schemaVersion` we do not recognise is refused rather than rendered — the
* client would be guessing at unfamiliar fields, and a calendar that guesses is
* worse than one that asks you to reload.
*/
export async function fetchFeed(signal?: AbortSignal): Promise<EventFeed> {
const res = await fetch("/data/events.v1.json", { signal: signal ?? null });
if (!res.ok) {
throw new Error(`Feed request failed (${res.status}).`);
}
const json: unknown = await res.json();
const version = (json as { schemaVersion?: number }).schemaVersion;
if (version !== SCHEMA_VERSION) {
throw new Error(
`This page expects feed v${SCHEMA_VERSION} but the server sent v${String(version)}. Reload to get the current app.`,
);
}
const parsed = EventFeed.safeParse(json);
if (!parsed.success) {
throw new Error("The feed did not match the expected shape.");
}
return parsed.data;
}