Replace the pre-classified store (visits/crawlers/abuse lists written at
collection time, plus abuse_ips and in-memory pending/session tables) with
a raw append-only log: one Get record per document GET (full path, true
HTTP status, referer origin, preload flag) and one Msg per /_ws activity
message. Visitor/crawler/abuse classification, visit grouping (30-minute
inactivity gap), status/referer/UTM attribution and all aggregates are
derived in Store.display(), so future rule changes never invalidate stored
data. The viewer payload keeps its exact shape.
Fixes structurally:
- Abuser 404s on slug-format paths showed up as "articles read": the
not-found branch recorded the request twice through separate status
plumbing. Each request is now recorded once with its true status.
- 404 trail links never rendered red: cache-served navigations issue no
GET and the only real GET (the idle preload) was discarded before status
recording. Preloads are now recorded with pre=True, never counted, and
used for status attribution.
- formatAbuseRows merged a path's 404 probes and 200 reads into one entry;
the collapse is now keyed by (path, status class).
Rule improvements enabled by the redesign:
- The plain-404 abuse threshold counts within a 1-hour sliding window, so
long-time readers accumulating misses never classify (scanners spray).
- Hidden (admin) clients never trigger abuse classification: editing means
visiting not-found pages.
- /.well-known/ probes (RFC 8615, e.g. Chrome devtools) are never abuse
evidence; //foo-style empty path segments are an instant telltale.
- Visits can no longer open on an external exit URL; favicon fetches skip
hidden clients' referers/exits.
Legacy analytics.json files are set aside as .bak-legacy on startup.
Visits with under 5 s of total reported reading time are JS-running bots:
display() converts them to crawler hits (one per trail page, with referer
and UTM query) and excludes them from every aggregate. Crawler referers
join the favicon fetch origins and render with their icon in the crawler
table. Abuse hits now record the real response status, so the abuse table
splits 404 probes from the articles the abuser actually read (200 GETs),
shown as trail links like the visitor/crawler tables.
The first positional CLI argument (default localhost) names the site's
public hostname and its data directory under the cwd, replacing the
CWD-relative pagerite.* files. The public origin (https://<hostname>)
is now authoritative configuration instead of a value learned from
admin browsers: POST /_api/site-url, Data.site_url and the pagerite.js
reporter are removed, and page rendering, sitemap, robots.txt and
analytics own_origin all use SITE_URL consistently (localhost falls
back to the request's base URL).
Document GETs now stash their status (200/404) in a pending table,
consumed by the matching ping: visits gain a per-path statuses map and
crawler hits a status field. Trail links with a 404 status render in
red with the status code in the tooltip, alongside the read time.
- Fonts scale with the svg instead of the --u constant-screen-size
compensation (ResizeObserver machinery removed)
- Larger node text (slug 19px, count 15px)
- Pill labels no longer ellipsis-truncated: text is clipped at the pill
border via per-node clipPaths; captions center when they fit and anchor
left on overflow so the title's beginning survives; count lines stay
centered
- Bounding box crops to pill half extents plus the ribbon halo instead of
the diagonal radius, removing the large top/bottom margins
- Charts grow to a larger intrinsic size (1052x174) and never upscale
past it; centered with equal side margins above the cap, full width
below, svg always within page bounds; overflow visible so wider fonts
don't clip at the viewBox edge
- Totals row aligns its left edge with the charts and shrinks (gap first,
then font) via container units to always stay on one line
- Cull connections whose thin middle would render below ~0.8px
(MIN_WMID); drop external source/exit nodes whose connectors are
all culled, while site page nodes always stay
- Bead simulation persists across data reloads: emitters keyed per edge
direction, beads tracked by progress, so unrelated count changes no
longer reshuffle bead positions
- Bead speed relative to span length: constant 1.5s traversal per edge
- Top lane labeled with a house icon; all lane labels left-aligned just
past the source pill (half height on near-vertical branch lanes), with
guides running to the lane end so long slugs are never truncated
- Unified chart text at 11px system-ui; fixed size independent of theme font
- Day view y axis reads "visits / 5 min" / "views / 5 min"
- Left margin and y tick spacing tightened (MARGIN_L 56 -> 40)
- Gap between visits and views charts removed
- Legend repositioned for the larger font
- weeklySeries shifts overlaid weeks onto the current week's time axis so
they overlay inside the plot instead of overflowing left; oldest weeks
paint first, current week on top
- Legend moved inside the visits chart's top right: current ISO week in
accent, past weeks as a single muted "Week M" / "Week M–N" specimen
- Past week curves use the muted color instead of faded accent
- Chart height reduced ~30% (180 -> 126)
- Charts render as single SVGs with axis labels inside the viewBox,
replacing the stretched plot + HTML overlay labels
- Rolling ranges end at now, t0 aligned to UTC day; bucket size follows
the window (6h up to 31 days) so "all" at its 30-day minimum renders
identically to "month"
- X labels always centered on their true position; no edge-align shifting
- rangeWindow simplified to rolling spans ending at now
- TransitionGraph "all" visual scale floored at the 30-day plot minimum
- keep visitor charts y-axis minimum range at 10
- keep 'all' chart x-axis minimum span at 30 days
- group crawler hits by (ip, ua) and list top pages visited, show crawler page load counts as N× prefix
- store and display geoip city, keep geoip country overwrite
- stream live updates over WebSocket /_api/ws/analytics
- include family ring arcs in transition map crop bounds
- remove top UA summary, limit crawlers to 10 and visits to 20
- human-readable relative timestamps with UTC tooltip
- Edge widths grow logarithmically with the connection count (~1 px at
a single count, uncapped); connections below 1% of total traffic are
pruned, bounding the graph to ~100 edges.
- Beads: per-direction flows emitted at a rate linear in the count,
each bead simulated independently in JS (no in-flight limit), offset
onto right-hand lanes so opposing flows don't collide, running under
the node circles with a glow.
- External links: referer origins as a node row above the map, exit
origins fanned outwards from their source page.
- Transitions are now stored per 5-minute bucket (sparse
from -> to -> bucket -> count) so the graph filters by time range
like the other series; legacy analytics files are discarded.
Add server-side visit analytics collection, a public-page ping endpoint,
and a full-screen AnalyticsView for admins.
Backend:
- Add pagerite/analytics.py: Analytics/Visit model, Store, and persistence
- Wire /_a ping endpoint and GET /_api/analytics into pagerite/app.py
Frontend:
- Add full-screen AnalyticsView with visitor charts and transition map
- Add VisitorCharts and TransitionGraph subcomponents
- Add analytics JS helpers in frontend/src/analytics/
- Send navigation pings from frontend/src/pagerite.js
- Mount AnalyticsView from frontend/src/main.js
- Document the feature in docs/analytics.md and update AGENTS.md