Anchor Website 16
Fable 5
Opus 4.8
Summary
Built the entire chat + analytics system for a327ex.com's engine renderer (identity, analytics ingest + board, chat server + visitor panel + native desktop console), then a full security review + hardening pass, a per-page view counter, and a round of browser-affordance reclamation (link new-tab, hover-URL, right-click context menu, web clipboard/paste). Analytics-first ordering was chosen because analytics data is time-perishable while chat delayed loses nothing.
Direction + ordering (analytics-first):
- Owner's four goals: unique device IDs, per-ID tracking (visits/geo/duration/clicks), same IDs for chat history, later elevation to accounts (game testing, Patreon-like). Reordered analytics BEFORE chat because analytics is time-perishable and chat rides the proven identity layer.
- LGPD "good citizen" note explained: Brazil's GDPR-equivalent; anonymous first-party analytics is covered by legitimate interest, no cookie banner needed, just a transparency note. Deletion is self-service (clear site data) since nothing ties a token to a person.
Task 1 — server foundation (identity + analytics), LIVE e615e40:
- SQLite at
/srv/a327ex/data/a327ex.dbOUTSIDE the git work tree (deploys never touch it; WAL; ms timestamps).server/db.lua(lsqlite3, numbered migrations, named-param helpers → parameterized by construction),server/api.lua. - Schema:
users(empty until accounts),devices(token = 32-hex /dev/urandom secret),sessions(server-stamped CF geo/UA; ts_last advances per batch → durations),events(narrow, open type vocabulary, JSON data ≤2KB). - Endpoints: POST /api/identify (mint/refresh), POST /api/analytics/events (batched, all-or-nothing, session_start lifted via COALESCE), GET /api/analytics/summary (admin read token, constant-time compare, lockout). Security bones: tokens in headers/bodies never query strings, no-store, no IPs stored.
A327EX_PORTfor scratch instances. - VPS: lsqlite3 + dkjson rocks (Lua 5.4), nginx /api block (limit_req + 16k cap), backup.sh snapshots the DB.
Task 2 — client side (boot bridge + batcher), LIVE 2fb1119/b11d192:
- Engine (Anchor2
5c80556):web_boot_param(name)(reads Module.__) + web_http_post(fire-and-forget keepalive fetch). - engine-boot.js: identify in parallel with engine load (localStorage → Module.__deviceToken), per-boot session id + referrer, pagehide sendBeacon session_end (token in body).
- renderer/analytics.lua: queue+flush batcher (session_start w/ referrer/viewport/dpr/platform, pageview per nav, 45s heartbeat that self-gates to visible tabs via rAF freezing). Inert without the boot bridge.
- ⭐ TWO KEY BUGS: (1) build-web.sh preloads an EXPLICIT .lua list — a new root module (analytics.lua) MUST be added or the web boot dies on require (shipped broken ~13 min). (2) Ad blockers (uBlock/EasyPrivacy) kill any URL containing 'analytics'/'events' as ERR_BLOCKED_BY_CLIENT — the owner's own visit flushed and the POST died in-browser; renamed endpoints to bland
/api/sync+/api/admin/summary. Also fixed: landing on '/' queued no pageview. Verified end-to-end via the owner's browser (headless preview can't tick the SDL loop). - Added
__landing(raw arrival URL) to session_start so dead blot-era inbound links are captured (SPA flattens unresolvable paths to '/').
Task 3 — analytics board at /admin, LIVE 1cfd899:
- Engine (Anchor2
ca4599c):web_http_get/web_http_result— async GET, poll-style (the chat-polling primitive too). - Server: migration 2 (
devices.owner— board queries exclude owner=1), six /api/admin/* endpoints (overview + daily series, pages, sources incl. raw landings, sessions w/ path trails, visitors, visitor?device=N drill-down). /admin host page + /admin/setup token setter (plain-DOM input → localStorage; external /static/admin-setup.js because content blockers CSP-kill inline scripts). - Renderer board.lua: aggregate mode (numbers, visitors/day bar chart, pages, sources, recent sessions) + per-visitor mode (click → full history). Own JSON reader (unit-tested incl. surrogate pairs).
- ⭐ VIEW-SCOPING BUG:
viewis a main.lua LOCAL — board_enter() writing the global was a silent no-op ("board never opens"); the nav branch in main.lua sets it. Lesson recorded for chat too.
Bot filtering (owner: "only actual humans"):
bot_devicesVIEW recreated at boot: labeled crawlers (UA contains bot/spider/crawl/render/+http — Applebot ran 30 zero-second one-shot devices/day, Yandex/Baidu/Yeti self-label) + unlabeled-fleet net (UA shared by ≥5 devices, every session sub-3s one-shot, AND sessions==devices for identity-churn). Duration alone classifies nothing (a real sub-45s visit with a lost beacon also records 0s). Owner's returning-bouncer concern answered: keeps one ID, can't cluster. Day 1: ~100 IDs = 44 token-only crawlers + 40 bots + ~14 humans.- Owner devices flagged owner=1: #2 (Windows), #91 (Android phone), #102 (incognito one-shot). [REDACTED#1: personal] had zero genuine strangers — all three were the owner.
Board feedback pass: new/returning split (numbers + stacked chart), visitor list ranked by sessions then days, all times → Brazil (UTC-3, no DST; SQLite -3 hours). ⭐ Found a crawler session-id COLLISION: headless bots seed crypto.getRandomValues deterministically → identical session ids across crawls → since it was the sessions PK, the 2nd device's INSERT OR IGNORE was swallowed → orphan events, no UA, escaped the bot filter + broke the visitor count. Fix (35cf905): ingest prefixes the stored session id with device.id; ov_window derives visitors from the same set as the split so they reconcile.
VISITS grouping (30-min gap): a visit = a device's sessions collapsed while ≤30min apart, so a 2h continuous read or 7 permalinks over minutes = ONE visit. Shared VISITS_CTE (gaps-and-islands via window fns). #187's 7 sessions → 1 visit (7 pages, ~30min). Owner asked "2h = 1 or 4 visits?" → 1 (only splits after 30min inactivity). Overview/recent-visits/visitor-list/detail all use it.
Task 4 — chat COMPLETE:
- Server (migration 3): the conversation IS the device — messages(device_id, from_owner, body, ts, read_by_owner). Visitor: POST /api/chat/send (rate limits, control-char strip, MUTED = silently accepted), /api/chat/name (random adjective-noun names, never internal ids), GET /api/chat/poll. Owner endpoints behind the admin WRITE token (new secret, console-only; read token cannot touch chat — verified 401).
- Visitor panel (chat.lua): bubble + thread + input, device-token identity, poll 3s open/60s closed, optimistic echo replaced by server copy.
- Console (a327ex-site/console/, native Anchor 2): thread list + reply + mute on the write token (config.lua). Engine adds: WinHTTP http_get/post (Anchor2
e6e09b0), window_flash (FlashWindowEx), system_idle_seconds (GetLastInputInfo). Presence = console running AND owner not idle 10min. Read receipts (migration 5, read_ts): '· read' + hover-revealed timestamp both sides, seconds in timestamps. - Console polish: site_dark visual parity (verbatim theme values; colors must be PACKED
color()(); scale=1 — default 3 blew off-screen); duplicate-send fix (_local echo); async console HTTP (Anchor2a80f833, worker thread + poll — killed the poll hitches). - Chat fixes: space in the input (engine-boot preventDefault'd keyCode 32, killing SDL_TEXTINPUT); backspace key-repeat from the owner's FilterKeys values.
Privacy note + opt-out, LIVE d9b304e: notes/privacy.md written by me (attributed "Written by Claude (Fable 5)"), sidebar 'privacy' link. Settings "Anonymous analytics" switch: Off = engine-boot skips identify (reads persisted tracking=false pre-wasm; no token → analytics+chat+beacon inert) + immediate in-session disable. Settings now PERSIST on web (new engine web_store_get/set localStorage, Anchor2 ee76ef6).
Posted the chat-feature announcement message (owner's text on games as a "third space", MMO guilds, Discord "lame as hell").
⭐ SECURITY REVIEW + HARDENING (2c2dc13 + VPS firewall): 3 subagents + live tests found NO server-access vuln (no SQLi/auth-bypass/IDOR/path-traversal/XSS/SSRF/RCE/CSRF/token-leak; chat text is control-char+bidi-stripped, param-stored, glyph/native-rendered never DOM). Fixes: origin ufw-firewalled to Cloudflare (was directly reachable → CF-bypass + geo-spoof confirmed; :80/:443 allow only CF v4+v6, :22 open; applied with a 5-min systemd auto-revert safety). Rate-limit spoofing was NEVER possible (nginx overwrites X-Real-IP — verified 23/40 spoofed-IP mints still 429'd). Security headers (CSP frame-ancestors none + connect-src self+R2, X-Frame-Options DENY, nosniff, Referrer-Policy). DoS bounds: read_body by actual bytes, events per-token daily cap 5000 + global 2M + 120/min, search 200-char/20-term.
Per-page view counter (31b3ea4): top-right T/W/M/Y real-human (bot+owner-excluded) pageview counts, Brazil calendar boundaries (year=Jan 1, week=Mon). Owner-only (read-token gated → 401 without + UI only draws with it). Removed the "views" label per owner. Clarified it already counts new+returning (34 homepage views today = 28 new + 6 returning); it's per-page so it won't match the board's site-wide 40.
Browser affordances (0b5082b,373875d, clipboard 06f21eb): link new-tab (Ctrl/middle-click), hover-URL readout, right-click context menu (links/images/videos — we OWN every media file at a real URL so "open in new tab" = browser saves the actual mp4/png). Web clipboard FIXED: SDL clipboard is inert on emscripten (copy AND the code-copy button silently failed) → clipboard_set now uses navigator.clipboard.writeText; chat paste via the browser paste event → web_paste_drain. Media URLs normalized (stored without leading slash). Extended to the homepage feed.
⏳ OPEN (next session): (1) homepage right-click/middle-click DON'T work (hover-URL DOES — so the feed loop + hit-test + captured-globals-in-draw work; but ui_rmb_pressed/ui_mmb_pressed don't trigger the feed dispatch, though article-view right-click works). A TEMP owner-only diag HUD is LIVE (top-left) — read rmb# next session. (2) owner-test the async console. (3) text selection + find-in-page (the two deferred affordances). (4) mobile chat typing. Full handoff in renderer/PLAN.md.
🔒 Only the summary of this log is public. Private because it contains too many website internal details.