Add personalization, outbound feeds, and PWA/push to the web UI
A multi-session build turning Pete's read-only web UI into something people return to. Five phases, signed-in features keyed off the OIDC subject; anonymous visitors keep the reverse-chron feed and localStorage-only state. Phase 1 — per-user read + bookmark state: user_story_state table + storage/userstate.go; auth-gated /api/read, /api/bookmark, /api/state and a /bookmarks page; reader.js syncs state server-side for signed-in users. Also hides the Matrix-posting UI when posting.enabled=false (web-only mode). Phase 2 — outbound feeds: storage.ListForFeed + web/feed.go hand-build RSS 2.0 (content:encoded) and JSON Feed 1.1 (no new dep); /feed.xml, /feed.json and per-channel variants; <link rel=alternate> discovery tags. Phase 3 — "For you" + related: storage/rank.go scores recent unread candidates by channel/source affinity + recency decay; RelatedStories via FTS5. ForYou rail + /for-you page; public /api/related feeds the reader's "You might also like". Phase 4 — source-health dashboard: source_health table + storage/sourcehealth.go (RecordPollResult, ListSourceHealth, SourceContentStats), written by the poller; admin-gated /status page behind web.admin_subs. Phase 5 — PWA + offline reader + Web Push: root-scoped manifest.webmanifest and sw.js (app-shell precache, /api/article runtime cache for offline reading, offline fallback, push/notificationclick handlers); PNG icons from pete.avif; pwa.js registers the SW and drives a notifications toggle. Web Push adds webpush-go, a [web.push] config block (pete -genvapid mints VAPID keys), a push_subscriptions table, auth-gated subscribe/unsubscribe endpoints, and a digest sender that pings each subscriber "N new stories" past their watermark, honoring disabled-sources and pruning gone endpoints. Tests added beside each new storage/web file; go test ./... and go vet clean.
This commit is contained in:
@@ -54,6 +54,18 @@ CREATE TABLE IF NOT EXISTS user_preferences (
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
-- Per-user read + bookmark state for signed-in visitors, keyed by OIDC subject.
|
||||
-- One row carries both signals; a NULL timestamp means "not set". A row with
|
||||
-- both timestamps NULL is meaningless and is pruned, so presence of a row means
|
||||
-- the story is read, bookmarked, or both.
|
||||
CREATE TABLE IF NOT EXISTS user_story_state (
|
||||
user_sub TEXT NOT NULL,
|
||||
story_id INTEGER NOT NULL,
|
||||
read_at INTEGER,
|
||||
bookmarked_at INTEGER,
|
||||
PRIMARY KEY (user_sub, story_id)
|
||||
);
|
||||
|
||||
-- Aggregate web usage. page_views holds running view counts keyed by a coarse
|
||||
-- path label ("home", channel slug, …) and the UTC day, so we can report both
|
||||
-- all-time totals and per-day breakdowns without storing any per-request rows.
|
||||
@@ -64,6 +76,35 @@ CREATE TABLE IF NOT EXISTS page_views (
|
||||
PRIMARY KEY (path, day)
|
||||
);
|
||||
|
||||
-- Per-source poll health, one row per configured feed (keyed by source name).
|
||||
-- Written on every poll (success and failure) so the owner-facing dashboard can
|
||||
-- show which feeds are healthy without keeping the poller's in-memory state.
|
||||
-- last_success_at / last_item_count survive failures so a broken feed still
|
||||
-- shows when it last worked and how much it last returned.
|
||||
CREATE TABLE IF NOT EXISTS source_health (
|
||||
source TEXT PRIMARY KEY,
|
||||
last_poll_at INTEGER, -- unix, most recent poll attempt
|
||||
last_success_at INTEGER, -- unix, most recent successful fetch
|
||||
last_error TEXT, -- last failure message ('' when healthy)
|
||||
consecutive_failures INTEGER NOT NULL DEFAULT 0,
|
||||
last_item_count INTEGER NOT NULL DEFAULT 0, -- items in the last successful fetch
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
-- Web Push subscriptions for signed-in users, one row per browser/device
|
||||
-- endpoint. p256dh + auth are the client's encryption keys (RFC 8291); the
|
||||
-- server needs them to encrypt each push. last_notified_at is the per-endpoint
|
||||
-- digest watermark: the sender only counts stories seen after it. A user can
|
||||
-- have several endpoints (phone, desktop) — each is notified independently.
|
||||
CREATE TABLE IF NOT EXISTS push_subscriptions (
|
||||
endpoint TEXT PRIMARY KEY,
|
||||
user_sub TEXT NOT NULL,
|
||||
p256dh TEXT NOT NULL,
|
||||
auth TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
last_notified_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
-- Privacy-preserving daily unique estimate. visitor is a salted hash of
|
||||
-- IP+User-Agent; the salt rotates every UTC day and is never persisted, so the
|
||||
-- hashes are irreversible and cannot be linked across days. We keep only enough
|
||||
@@ -88,6 +129,9 @@ CREATE INDEX IF NOT EXISTS idx_reactions_post_guid ON reactions(post_guid);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_reactions_event_id ON reactions(event_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_page_views_day ON page_views(day);
|
||||
CREATE INDEX IF NOT EXISTS idx_daily_visitors_day ON daily_visitors(day);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_state_bookmarks ON user_story_state(user_sub, bookmarked_at) WHERE bookmarked_at IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_user_state_reads ON user_story_state(user_sub, read_at) WHERE read_at IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_push_sub_user ON push_subscriptions(user_sub);
|
||||
`
|
||||
|
||||
const ftsSchema = `
|
||||
|
||||
Reference in New Issue
Block a user