Loading…
a327ex.com

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.db OUTSIDE 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_PORT for 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: view is 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_devices VIEW 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 (Anchor2 a80f833, 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.