Anchor Website 17
Fable 5
Opus 4.8
Summary
Marathon session that completed the Anchor Website's remaining feature arc: homepage right-click fix, tweet-media context menus, full text selection (article + homepage), find-in-page (desktop + mobile), mobile chat end-to-end (four attempts, ending in the DOM-overlay architecture), a shared multi-line text-edit widget, Korean font support, console QoL, nginx hardening/footgun removal, and an LLM-friendly content package. Site is now functionally complete; sealed as the design continues privately.
Homepage right/middle-click fix (latch edge globals):
- Root-caused by code reading without needing the diag HUD: the engine runs update() at 120Hz fixed timestep = 2x per 60Hz draw;
ui_rmb_pressed = input_pressed('rmb')re-assigned per update erased the edge before homepage_draw's draw-time dispatch saw it. Article rmb worked (consumed in update); homepage left-click worked (ui_mouse_pressedlatched in ui_capture_input/ui_end). - Fix: rmb/mmb latched like ui_mouse_pressed (set on edge, cleared in ui_end); article rmb + link-mmb sites consume the latch (double-fire guard — mmb would have opened two tabs). REUSABLE: any update-captured EDGE global read at draw time MUST be latched.
Tweet media + gallery context menu:
- canvas_media_at only walked el.children; tweet media lives outside it (media_imgs/q_media_imgs grids + synthetic video_el/q_video_el), and gallery hits returned the container el the menu ignores.
- Fix: tweet video els get type='video'; draw_tweet_element stamps grid origins doc-space (media_x/media_y, q_media_x/q_media_y); canvas_media_at resolves tweet media + gallery hits to the CELL record (type='image', path). Tweet-body links remain invisible to canvas_link_at (body paras outside children) — noted, deferred.
Admin-token security Q&A + referrer-lure investigation:
- Assessed "can a lure website steal my admin token / own my PC": localStorage origin isolation + no CORS headers + header-carried tokens = no read/use path without a browser 0-day; realistic vector = reverse tabnabbing (window.opener) + token phishing. Found SDL_OpenURL = bare window.open (no noopener) in the built engine; flagged, owner deferred.
- Investigated dysphoricvarld.neocities.org (a referrer on the analytics board) via server-side fetch: harmless day-old personal Neocities page linking a327ex.com as "Rustic beatery"; the alarming link was a rickroll-style gag (earbuds review). Lesson embedded: referrer strings are attacker-chosen input; fetch suspicious ones server-side.
Text selection v1 — article view:
- selection.lua: doc STREAM of text-bearing pieces (text els incl. tweet name_el/body_paras/q_paras + code els; open details only); endpoints {si,li,ti,bi} over the SAME cached layout the draw uses (el.lines). Layout lines stamp
brk('hard'/'soft'/'force'/'end') so copy rejoins soft wraps with a space; emoji reconstruct from twemoji stems via utf8.char. - Highlight = per-line rect under glyphs in draw_text_element/draw_code_element (theme color
selectionx3 themes); gen-stamps prevent cached-doc ghost highlighting. - Browser click semantics:
ui_mouse_clicked= release <=4px without selection drag (latched, cleared in ui_end); article links/crumb/tweet-frame act on it. Code blocks: plain drag selects; pan moved to SHIFT+drag. Ctrl+C copy, Ctrl+A select-all, right-click 'Copy' menu item, I-beam cursor, drag-autoscroll.
Homepage selection:
- 'home' mode stream from homepage_sel_items(): imperative rows (crumb/email/ARTIFACTS/banner/link rows/date rows) become PSEUDO text els (one fabricated line of word tokens; positions stamped per frame; WYSIWYG truncated titles); message docs walk like articles with f._sel_uy feed-y stamps.
- Block-stable si: feed item ii owns block ii*1000+1..+999, msg date row pinned at +999 — a lazily built doc slots entries INSIDE its block; stream rebuilds WITHOUT dropping selection (browser-reflow behavior). Cross-message selection works; homepage clicks all converted to release-clicks; scrollbar/chat presses KEEP selection.
NDA Project 15 invisible on the site (deploy lesson):
- Another instance committed the log but deferred the content deploy (correct behavior — this session was mid-deploy). Committing shows logs to CRAWLERS (Lua SEO server restart) but NOT the engine site: renderer/data/index.lua is a GITIGNORED build artifact baked into the bundle. Fix = convert.lua --all + deploy.sh --data. Saved as memory reference_log_needs_deploy.
Find-in-page (Ctrl+F):
- find.lua reuses the selection stream: matches = (a,b) ranges in {si,li,ti,bi} space within one element, located via logical text + segments. Amber highlights + strong-orange current (find/find_cur x3 themes), Enter/Shift+Enter cycle w/ scroll-to (~30% viewport), Esc closes. Case-insensitive ASCII fold.
- Homepage find FORCE-BUILDS the whole feed (homepage_build_all) for browser-complete results.
- Perf fix after owner report (homepage per-keystroke lag): rebuild() was re-flattening the entire feed per keystroke into per-CHARACTER cells. Fix = index cached by sel_gen (built once at fi_open) + per-TOKEN segments (sel_entry_segments replaces sel_entry_cells); byte offsets map back via binary search only for actual matches. Blinking caret added.
- Typed-text plumbing: engine_get_typed_text DRAINS on read → chat drains only while focused; chat_blur() on find open (one text consumer per frame).
- Shared key-repeat standard: TEXTINPUT_REPEAT_DELAY/RATE (0.2/0.01) + textinput_repeat() helper; chat backspace + find backspace unified.
Chat fixes:
- Transparent panel (owner phone screenshot): panel fill was off_bg (4% white) — page showed through. Fix: opaque bg() base + off_bg wash.
- Presence wording: 'a327ex is online' / 'a327ex is away'.
Mobile chat typing — the four-attempt saga (the session's core lesson):
- DOM input overlaid on the engine's input rect: FAILED — engine px<->CSS conversion broken (canvas backing DPR capped 2.0, px() scale ~2.6).
- Full-width bar + kbFrac panel lift + interactive-widget=resizes-visual (which DID fix the canvas distortion): typing worked but browser zoom-on-focus panned the canvas-drawn panel offscreen-top.
- maximum-scale=1/user-scalable=no: ignored by the owner's browser (force-zoom class behavior); reverted.
- FINAL (first-principles rewrite as Fable): while open, the ENTIRE panel is real DOM tracking the visualViewport RECT every rAF — keyboard shrinks it, pan follows, zoom still fills what you see; no viewport state can hide it, BY CONSTRUCTION. Engine keeps all logic (identity/poll/send/read-acks); publishes chatOpen per frame + chatState JSON on content-signature change; overlay mirrors chatText + chatSubmit/chatCloseReq/chatRenameReq counters; textContent ONLY (XSS rule holds). MOBILE_CHAT kill switch.
- Engine adds: web_set_param (Lua->Module.__
, reverse of web_boot_param, 73894dd); web_boot_param buffer 1KB->8KB (1031ba2). - Residual zoom aftermath: unzoom watchdog (meta re-clamp; ignored on the owner's browser, kept for default ones). Pinch passthrough attempt FAILED STRUCTURALLY — a pinch starts as ONE finger; SDL preventDefaults that opening touchstart; a canceled sequence never gets native gestures (don't retry). FINAL zoom fix: canvas + engine layout track the visualViewport rect (fit() positions/sizes to it; engine web_css_size reads visualViewport, 59296cd) — zoom becomes IRRELEVANT; owner confirmed working. THE MOBILE-WEB LESSON: you don't control the viewport; you SUBSCRIBE to it.
- Owner cleared browsing data while testing → phone devices 570/573/577 flagged owner=1 in the DB. Lesson: loaders are no-cache + bundles content-hashed → a PLAIN RELOAD always updates the site; clearing data wipes device/admin tokens.
Batch session (owner: "do 1-7 except 3, without stopping"):
- textedit.lua — shared multi-line canvas text-edit widget: greedy wrap with the box GROWING in height (cap 6 lines then follows caret), blinking caret (530ms), arrows/Shift+arrows selection/Home/End/Ctrl+word-jumps/Ctrl+A-C-X-V, selection-aware Backspace/Delete w/ shared repeat, click-to-place-caret. Wired into site chat desktop input + console reply box (console dofiles ../renderer/textedit.lua — ONE source). Mobile chat input became an auto-growing
- Mobile find: topbar magnifier (article+home) -> DOM bar top-of-visual-viewport w/ count + prev/next/close; find.lua mobile mode mirrors __findText + counters.
- Korean/Hangul: Yu Gothic lacks Hangul (ko subs = boxes). Malgun Gothic subsets (MalgunR/B.ttf, 738 codepoints, ~570KB) via subset_fonts.py is_hangul jobs; prose.kr[role] tier; is_hangul BEFORE is_cjk in tokenizer + mixed_segments (subtitles, video titles).
- Console QoL: notification ding (synthesized assets/notify.wav; cfg.sound), start_minimized (new engine window_minimize, 41d463f), Startup-folder autostart shortcut, run.bat self-swaps staged anchor-new.exe (running exe is locked during deploys).
- nginx: post-receive's auto-apply of deploy/nginx-a327ex.conf REMOVED on the VPS (backup post-receive.bak-20260705) — editing that file in git would have silently REVERTED the site to plain-Lua on push. Fallback conf got security-header + rate-limited /api/ parity (CSP w/ looser connect-src for tweet widgets). Verified: pushing the conf did NOT flip the site.
- Web-video polish skipped (no spec anywhere); per-post fidelity bucket owner-skipped; rotation re-fit doesn't work but owner: "absolutely fine, not necessary."
LLM content package (llms.txt convention):
- /llms.txt discovery file + /a327ex-content.zip (~25MB: all section .md files + MANIFEST.md w/ titles+dates) + concatenated /llms-{posts,notes,messages,pages}.txt + /llms-logs.txt INDEX (211 logs, 62MB raw — linked individually) + per-doc /raw/
/ .md. - deploy/build_llms_package.py runs from the post-receive hook on EVERY push — new content updates the package automatically at zero owner cost (verified on its own deploy push).
- nginx /raw/ alias uses EXPLICIT section alternation (messages|logs|notes|posts|pages) — vault/ structurally unreachable (verified direct 404 + traversal 404).
Engine (Anchor2) commits this session: web_set_param 73894dd; web_boot_param buf 8KB 1031ba2; web_css_size reads visualViewport 59296cd; window_minimize 41d463f. Site work spans ~25 commits ending a853d8d
LLM content package (added post-seal, same conversation): covered above in the summary intro — /llms.txt convention + zip + raw/ + hook auto-rebuild.
THE GREAT MERGE (post-seal, same conversation; owner: "I'd like the URL to be correct too"):
- Taxonomy: notes/ + messages/ merged INTO posts/ (99 messages stamped
Kind: message— feed inline/no-title behavior keys on that, not section); It Follows -> stories/ withTheme: oldstamped on all 41 old-blog-era files (the old-Blot skin is PER-FILE now, doc.theme_old — section=='posts' checks eliminated everywhere). games/ arrives when a game is hosted. - NO DEAD LINKS: engine nav already slug-based (section ignored); Lua server 301s /<old-sec>/
to the slug's current home via content.find_slug — verified live: /notes/x -> /posts/x, /messages/x -> /posts/x, /posts/it_follows -> /stories/it_follows, all 301->200. Internal links rewritten in 9 authored files; logs left verbatim (records; redirects cover). - It Follows feed row reads 'Dec 30, 2025 · STORY'; tags POST/STORY/AI LOG; ARTIFACTS banner -> /stories/it_follows.
- Touched: server content/server/templates/search (theme_old/kind exposure, choose(theme_old), sidebar data-kind/data-theme, per-file new-tab), static/new-theme.js (feed builder kinds, full-nav for /posts/+/stories/), renderer convert.lua (sections {posts,stories,logs}, Kind-based previews, theme_old+kind in data/index), main.lua (3 theme_old checks), homepage.lua, sidebar.lua {logs,posts,stories}, llms package sections, nginx /raw/ alternation, /msg skill (mirror -> posts/ + Kind; media dirs STAY media/messages/).
- ⚠ Caught live post-deploy: build-web.sh staged boot message bodies by grepping section="messages" -> ZERO staged (blank feed messages at boot); fixed to select by kind="message".
- ⚠ TRAP #2 (owner-caught): opening OLD-THEME docs errored — registering the second theme's font set (+ the new Hangul siblings) blew the engine's MAX_FONTS=64 cap mid-navigation; font_load failed. Fix: cap -> 160 (Anchor2 2f56b1f, engine anchor-c81fbffba0.wasm). The CSP console spam reported alongside was tweet-widget iframes + MetaMask extension noise, NOT our header (verified live).
- llms zip verified post-merge: posts 173 / stories 1 / logs 212 / pages 3, no notes/messages dirs — rebuilt automatically by the hook on the merge push.
🔒 Only the summary of this log is public. Private because it contains too many website internal details.