- reader.js: guard read-aloud against the loading placeholder (bodyReady)
and invalidate stale speechSynthesis callbacks with a generation token
- storage: check rows.Err() after iterating story-view/content-length reads
- metrics: reuse placeholders() instead of duplicating the IN-clause builder
Surface read counts and sharpen the reader:
- story_views table + RecordStoryView on /api/article (background,
filter-guarded); "Popular this week" home rail via TrendingStories;
read-count badge and reading-time chip decorated onto every listing
- reader: signed-in-only read-aloud (TTS), native share/copy, and an
Aa typography popover (size/serif/sepia) persisted per device
- real alt text on card/reader/related/search images; time-of-day Pete
greeting on the home hero
- harden exec() to skip (not panic) on a nil DB so background writes
can't crash on a closed handle
Tests: story_views_test.go, trending_test.go. Suite green, CSS rebuilt.
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).