Two analytics corrections verified against the production capture:
- pagerite.js suppressed pings with fr == '/_a', but fetch-navigation
away from the analytics page had already GET-ed the target without the
preload header; the orphaned pending hit then flushed to the crawler
list, classifying a real user as a crawler. Navigations away from /_a
now ping normally (the server rejects /_a as a target regardless, and
admin noise is already handled by hide=1).
- _remove_visit only reversed the visit's creation counts, leaving
views/transitions from later pings behind as orphans on the graph with
no matching row in the visitor table. An in-memory per-visit count log
now tracks every count event, so an admin hide=1 scrub reverses the
visit completely.
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
JS-running crawlers (Googlebot, GoogleOther, Applebot) execute pagerite.js
and send navigation pings, registering as visitors. Pings whose User-Agent
matches _is_bot_ua (any "bot" token plus listed exceptions) are now
ignored, so their document GETs flush to the crawler list as intended. No
source verification: a spoofed bot UA merely lands in the crawler stats,
and path-based abuse classification catches scanners regardless.
Idle-time link preloads from pagerite.js were queued as pending crawler
hits and flushed to the crawler list whenever the user navigated more than
10s later, so real visitors' subpage loads showed up as crawler hits.
Preload fetches now carry an x-pagerite-preload header and the document
GET handler skips tracking for them; the ping sent on actual navigation
does the counting.
load() queried the live document for the pagerite:analytics-src meta,
but the swap never touches <head> — the meta only exists in the fetched
doc, so the app never mounted unless /_a was loaded directly. Also cache
the fetched HTML so the post-swap preload doesn't re-GET the page we
just navigated to.
- 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
- Fix banner artwork sizing: explicit 100% grid track so children
stretch instead of resolving height:100% against a content-sized row
(SVG intrinsic ratio bloated the row, cropping the artwork's bottom).
Bottom-anchor via object-position, transform-origin and YMax slice.
- pagerite.js: in-memory page cache — preload every visible internal
link once, serve navigation from memory without fetching; editors'
loadPlain keeps the cache in sync (pagerite:page-fetched).
- Structure editor: delete pages directly, no two-step confirmation.
- New 'stars' banner design (drifting starfield) alongside 'eyes'.
- Rewrite seed content: welcome page, three-level docs section covering
all Markdown features (source + rendered), showcase hierarchy with
image positioning and a simple leaf-page banner example.
Paskia does not support being iframed; the login/profile buttons are now
plain anchors, and a pageshow handler re-probes auth when history
navigation restores a cached page. Drops the paskia JS dependency.
CodeMirror measures text per character; Fira Code's ligature glyphs
render wider than the measured sum of their parts, corrupting cursor
and selection rendering. The font is set on .cm-content with ligatures
off, and inner spans inherit it for consistent metrics.
- Sidebar renders the section's whole subtree as nested lists (third
level and deeper, article-list-style markers); the one-item sidebar
rule yields when that item has children of its own; only the viewed
page is highlighted (navbar keeps ancestor highlighting); first_leaf
skips unpublished branches
- Banner design selector: inherit option names the resolved design and
its true source (backend banner_design_source excludes the node's own
setting; doc payload carries banner_design_inherited)
- Article editor: fenced code block on empty lines, table size picker
popup, borderless save icon disabled by saturate(0) alone
- While editing, window scroll is locked and only #main scrolls; the
panel exactly fills the available window height; editor scroll drives
the article
- One mounted shell with four kept-alive tabs; pens open/switch tabs,
closing only hides the shell so unsaved page-editor state survives
until a real reload; admin panels never reload the page, regions are
refreshed in place via the shared swapdoc.js helper
- SiteEditor split: site settings vs. the new StructureEditor tree tab;
BannerEditor split out of the old site editor
- Article editor format bar (bold/italic/code/link/table/image) with a
table size picker and Ctrl/Cmd-B/I/S bindings; fenced code block on an
empty line; icon-only save button disabled (saturate(0)) when clean
- Media uploads use icon buttons; favicon upload by clicking the preview
tile; placeholders reserved for actual defaults in effect
- Banner/site pens and auth buttons grouped in an .editor-pens container;
page pen no longer shown to anonymous visitors
- While editing, window scroll is locked, the panel exactly fills the
available window height, and only #main scrolls; editor scroll drives
the article
- Banner design inherit label names the design and its true source;
backend banner_design_source excludes the node's own setting and the
doc payload carries banner_design_inherited
Pages now declare width=device-width, so phones render at real device
width instead of a scaled-down 980px layout viewport. Below 48rem the
content becomes a single column with the sidebar lifted above the
article as a wrapping link strip; floated .left/.right figures fall back
to plain centered figures (explicit img widths still shrink-wrap) and
.wide keeps its centered full-viewport bleed.
Data.brand_html replaces the brand link entirely (rendered in a #brand
div on top of the banner, next to the nav); editable in the site editor
via a small HTML CodeMirror with image/video upload and paste, live
preview, and debounced save through /_api/settings.