Commit Graph

23 Commits

Author SHA1 Message Date
prosolis
8ec13eab5b games: the casino moves out, and gets a clock of its own
The tables were living in the news app's shell: Pete's face in the header
and the footer, the channel nav, search, the reader, the weather canvas,
the PWA. A casino is not a news page with a felt on it.

So it gets its own layout. What carries over is the design language — the
four palette vars, Fredoka/Nunito, the fat rounded cards, the dropped
shadow. What doesn't is every control it has no use for. gamesPage stops
embedding the news pageData, which is what keeps the furniture from
drifting back one convenient field at a time.

It keeps a clock, but tells a different joke with it: Casinopolis by day,
Casino Night Zone from six, palette and felt and the sign over the door all
changing together. The rule lives in roomAt() for the first paint and again
in the browser, so a player abroad gets their own evening.

And the cards are cards now — corner indices in both corners, the bottom
one upside down as printed, pips on the three-by-seven grid every real deck
has used for four hundred years, courts as a letter with the suit over each
shoulder. Driven in a real browser, both rooms, dealt through to a payout.
2026-07-13 23:40:33 -07:00
prosolis
c69fbb63db games: a blackjack table you can actually sit down at
The engine, the escrow and the wire were all in place; nothing had a browser on
the end of it. This is that end: a lobby, a table, and the five endpoints between
them.

The browser holds no game. It sends intents and gets back a view — the cards it
is entitled to see, and the script of how they arrived, one event per card off
the shoe. The dealer's hole card is not in the payload at all until the reveal,
because a field the client is told to ignore is a field somebody reads in
devtools. The shoe lives in game_live_hands, which also means a redeploy
mid-hand no longer costs a player their stake: the hand is still there when they
come back.

The money is ordered so nothing can be spent twice. The stake leaves the stack in
the same statement that checks it exists, before a card is dealt. Every new hand
is seated with a plain INSERT, so a double-clicked Deal is decided by the primary
key rather than by a read that raced — it loses, gets its chips back, and the
hand in progress is untouched. A double takes its raise up front and hands it
straight back if the engine refuses the move.

Cards are dealt rather than swapped in — they fly out of the shoe and turn over,
which was a requirement and not a flourish. The faces and the chips are still
plain; that's next.
2026-07-13 23:20:42 -07:00
prosolis
44613c4760 games: the wire the euros cross
Three bearer-authed endpoints and gogobee can work the border: poll what's
waiting, claim a row, report what happened to the money. The storage layer
underneath was already done; this is the transport, and deliberately nothing
more.

All three are idempotent, because the thing on the other end of them is a
retrying queue and the thing they move is money. A verdict delivered three
times creates chips once. A rejected buy-in moves nothing and clears the
pending amount so it stops eating the table cap. A cash-out gogobee couldn't
pay gives the chips back rather than vanishing them from both sides.

A verdict for a row Pete has never heard of is a 400, not a shrug: gogobee has
by then moved real euros against it, and no amount of retrying invents the
missing row. Under the contract the adventure seam set, a 400 parks it in
gogobee's queue where a human can find it.
2026-07-13 23:00:19 -07:00
prosolis
99574db3e9 news: the adventure page gets something that's actually happening
Every dispatch Pete publishes is an accomplishment — a death, a clear, a
milestone — and an accomplishment is a newspaper clipping the moment it lands.
No refresh interval fixes that. So the page never felt alive, and it never was
going to.

The board is the other kind of thing: state that is currently true. gogobee
pushes the whole roster, we replace ours with it, and it renders above the
clippings. An open tab re-polls so it keeps telling the truth.

Replace, never merge: anyone gogobee omits (opted out, no character) drops off
the public page. That omission IS the opt-out — a standing row showing class,
level and zone names the player anyway, so "an adventurer" would have been a fig
leaf.

The snapshot time lives in its own row, because an empty board is ambiguous:
nobody playing, or gogobee stopped talking to us. The page has to tell those
apart — one is a quiet realm, the other is a board that lies confidently, which
is worse than one that admits it lost the wire.

Also teaches Pete "departure", so a bored adventurer letting itself out is news.
2026-07-13 18:05:38 -07:00
prosolis
8e0d6aff3e Adventure news: fix posting gates, digest counts, and section gating
- Don't post adventure beats when posting.enabled=false (PostNow ignores the flag)
- Keep the adventure channel out of the round-robin rotation so it can't steal bulletins from the digest
- no_push now retires a fact against the digest, not just the live post
- Default a missing occurred_at to now instead of the Unix epoch
- Keep protocol-relative image URLs on the guarded thumbnailer
- Make digest_hour=0 (midnight UTC) settable
- Quote the true window total in the digest, not the truncated slice
- Hide the Adventure section entirely when it's disabled
2026-07-11 08:07:40 -07:00
prosolis
4c671fb410 Adventure section: ingest, permalink, digest, emblems, noindex
Pete's side of the Adventure news feed. Receives structured game-event
facts from gogobee, templates them in Pete's warm-reporter voice, and
publishes to a new /adventure section + live Matrix posts.

- adventure.go: bearer ingest + fact-guard + 13 event templates;
  /adventure/{guid} permalink (story.html); per-event SVG emblems at
  /adventure/art/{type}.svg (card image + og:image); NoPush suppresses
  the live Matrix post (cold-start backfill).
- adventure_digest.go: daily BULLETIN roundup at DigestHour (UTC);
  unposted-in-48h = bulletins; marks them digested; per-day ?digest= URL
  avoids canonical dedup.
- config AdventureConfig (enabled/ingest_token/channel/digest_hour);
  web.New takes the seam + a priority poster; started in main.
- adventure theme colors; thumbURL passes through local emblem paths;
  adventure pages are noindex (player-named; gap #5).

Claude-Session: https://claude.ai/code/session_017mEwUmmS7aQTP2NQXj6rUa
2026-07-11 00:53:59 -07:00
prosolis
dbcb459908 Add server-side Piper read-aloud with voice picker
Reader read-aloud now streams neural WAV audio from a new POST /api/tts
endpoint that shells out to Piper, instead of the browser's Web Speech
voice. Each paragraph is synthesized on demand with the next one
prefetched during playback, keeping the existing highlight/scroll sync.

Voices are configured under [web.tts] (piper binary + voices_dir + a
labelled voice list) and exposed to the client as window.PETE_TTS; the
reader gets a Voice selector in the Aa menu, persisted per-device. Still
a signed-in-only perk and gated on auth.
2026-07-07 23:00:27 -07:00
prosolis
77581ac152 Make source-health page public with a trimmed reader view
/status was admin-only (404 for everyone else). Serve it to all: a
reader view with per-feed live/idle/delayed status and last-update time,
while admins additionally get poll cadence, item/story counts, paywall
rates, posting times, and raw fetch errors. Error strings are stripped
server-side for non-admins so feed-specific workarounds and upstream URLs
never reach the public payload. Nav status link now shows for everyone.
2026-07-07 17:56:22 -07:00
prosolis
8863b75916 Fix push SSRF, cross-user unsub, and personalization edge cases
Code review of the personalization/feeds/PWA/push work surfaced ten
confirmed issues, now fixed:

- Web Push delivery bypassed the SSRF guard (unguarded default client);
  now routes through safehttp.NewClient with a hard timeout, and the
  subscribe handler validates the endpoint URL.
- Push unsubscribe deleted by endpoint with no owner check; added
  RemovePushSubscriptionForUser scoped to the signed-in user.
- Byte-slice body/content truncation could split a UTF-8 rune and break
  the RSS content:encoded XML; added a rune-safe truncateUTF8 helper.
- Digest sender could permanently starve a user who hid a high-volume
  source; step the watermark past a full hidden-source scan window.
- Service worker cached personalized HTML navigations into a shared
  cache (identity leak across PWA users); navigations are now
  network-only, CACHE_VERSION bumped to v2 to purge stale pages.
- Public /api/article leaked discarded/unclassified bodies; filter to
  classified, non-sentinel stories.
- runLocal never started the push sender; digests now fire in -local.
- Push client had no timeout, so one hung endpoint stalled all sends.
- Reader migration resurrected cross-device-cleared reads; gate it
  behind a one-time flag so the server stays authoritative.
- Bookmarks count didn't match the classified list filter.
2026-07-07 01:08:42 -07:00
prosolis
71f7050f41 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.
2026-07-07 00:07:19 -07:00
prosolis
55aa167151 Add feed/reader mode with full-article capture at ingest
Reader mode presents the stories on a page one at a time in a focused
overlay, marking each read as it's shown. Left/right arrows (or the header
book button / `f`) page through them; read stories dim on the grid. Read
state is device-local in localStorage.

Backing this required actually capturing article bodies, which Pete wasn't
doing — it kept only the RSS <description> lede and discarded content:encoded:

- stories.content column (idempotent migration; old rows fall back to lede)
- parser keeps content:encoded as paragraph-preserving text
- article fetch already done for paywall detection now also returns its body,
  so ingest stores the richer of feed-content vs scraped body with no extra
  request (prefers the archive snapshot body for paywalled stories)
- GET /api/article?id= serves the stored text; card queries now select id and
  expose it as data-id for the reader

Tests cover content extraction, the storage round-trip, and the article
endpoint + card rendering end to end.
2026-07-06 22:46:14 -07:00
prosolis
e65ffb1373 Add server-side web usage metrics with !petestats command
Track per-page/per-channel view counts and a privacy-preserving daily
unique-visitor estimate (salted IP+UA hash, salt rotated daily and never
persisted). No third-party analytics, no JS beacon. Surfaced via the
admin-gated !petestats Matrix command (named to avoid an existing !stats
bot in the rooms).
2026-06-22 01:54:34 -07:00
prosolis
cbbedd9894 Add optional Authentik (OIDC) sign-in with server-side preference sync
Signed-in users get their preferences (hidden feeds, weather location,
weather toggle) stored server-side keyed by their OIDC subject and synced
across devices. Anonymous visitors keep using browser localStorage, so the
site stays public. First sign-in migrates existing localStorage prefs up.

- config: [web.auth] section (issuer, client_id/secret, redirect, session_secret)
- storage: user_preferences table + Get/PutUserPrefs
- web/auth: OIDC code flow, HMAC-signed session cookie, CSRF state + nonce
- web/prefs_api: GET/PUT /api/preferences (auth-gated, 64KB cap)
- frontend: prefs.js sync layer seeds localStorage from server, pushes on write
- header: sign-in / account control

OIDC discovery is non-fatal at boot: if Authentik is down, Pete serves
anonymously rather than refusing to start.
2026-06-21 15:44:53 -07:00
prosolis
95f6e71933 Add LEGO channel and stock-recommendation feeds to Finance
LEGO channel (🧱, red theme) posts to Matrix with Brickset + The Brick Fan
feeds. Finance gains MarketBeat (analyst ratings/upgrades) and Seeking Alpha
(tier 2; expect paywall stamps).
2026-06-20 23:51:12 -07:00
prosolis
0b94ce7fe5 Add Finance channel (web-only) with markets/macro feeds
New web-only category mirroring the EU channel: shows in the web UI but
does not post to Matrix. Adds the channel entry, emerald (money green)
theme utilities, and two macro/finance feeds (Naked Capitalism, Wolf
Street) to the example config.
2026-06-01 17:05:16 -07:00
prosolis
8aceb259ca Add kids news channel; document per-source language filter
- New kids channel (/kids) with theme color, surfaced in the web UI
  and routed to by BBC Newsround, DOGO News, Science News Explores,
  and NASA for Students in the example config.
- README and config.example.toml now mention the optional
  per-source `language` filter added in the previous commit.
2026-05-26 23:06:17 -07:00
prosolis
a15025089d Add per-feed visibility settings panel
Visitors can hide individual feeds via a gear-icon panel in the header.
Preferences live in localStorage; the server ships the full source list
(name + channel) as window.PETE_SOURCES so the panel lists every feed,
grouped by channel, regardless of what's on the current page.
2026-05-26 17:15:46 -07:00
prosolis
71dc6d77ab Add anime and foss channel themes
Two new web-only channels alongside EU: anime (🌸 sakura pink #ec5e8a)
and foss (🐧 amber #d97706). Both get the full bg/text/decoration/
border/glow theme classes.
2026-05-25 11:48:33 -07:00
prosolis
ec1f130ed5 Add full-text search, EU channel theme, and web-only sources
Search: new FTS5-backed SearchStories query, /search JSON endpoint,
client-side overlay (search.js) wired into the layout header. EU
channel gets its own theme color (#003399) across bg/text/border/glow
classes. Sources can now route to non-Matrix channels without
validation error (web-only mode); a warning still flags typos.
2026-05-25 11:23:52 -07:00
prosolis
24103394ae Canvas-based seasonal weather effects + demo page
Server picks variant from Lisbon-local date (rain/petals/jacaranda/motes/leaves)
and renders behind content via a canvas particle layer. Each variant has a
hand-drawn silhouette so shapes are recognizable. /weather demo route exposes
variant + intensity + phase pickers, locking the time-of-day phase override.
2026-05-25 09:20:35 -07:00
prosolis
5ea64c1d32 Paywall signal detection, sqlite driver registration, /img route fix
- Detect explicit paywall markers (article:content_tier meta, JSON-LD
  isAccessibleForFree) so metered articles fall back to Wayback even
  when body length is above threshold
- Blank-import go.mau.fi/util/dbutil/litestream so the sqlite3-fk-wal
  driver mautrix's cryptohelper depends on is registered
- Fix /img route: Go ServeMux requires {wildcard} to be a whole segment,
  so capture {name} and strip the .avif suffix in the handler
2026-05-25 07:50:36 -07:00
prosolis
bddd15f7d1 Web UI: music channel, posted-to-Matrix glow, redesigned index, AVIF thumbnails
- Add music as a fourth channel (nav + theme color)
- Glowing themed border on cards that have been posted to Matrix
- Replace per-channel index sections with: "Pete just posted" strip,
  channel dashboard (last post, 24h count, totals), unified latest feed
- /img proxy: SSRF-guarded thumbnail re-encoder that resizes to 800px
  and runs avifenc -q 45, cached under data/img-cache
2026-05-24 23:07:17 -07:00
prosolis
fbd4b84eaf Add read-only web UI
Serves Pete's classified-story archive over HTTP alongside the Matrix
bot. Three sections (gaming/tech/politics), Animal-Crossing-vibe Tailwind
templates, day/night palette driven by the visitor's browser clock.
Web port configurable via web.listen_addr and ${PETE_WEB_PORT} in
docker-compose. Tailwind built in a node stage in the Dockerfile so
deployments don't need node at runtime.
2026-05-24 21:51:49 -07:00