Repository navigation
Turn aadhar.sh into a Windows-XP desktop OS (window shell, popovers, frontier features) + repo cleanup - #1
Merged
Merged
Conversation
…tons Restructure the XP taskbar to match the real Quick Launch layout: - profiles (Twitter/Instagram/Curius/Beli/Spotify) move into a compact Quick Launch strip right of Start, drawn as stylized CSS glyphs + brand colours (generic camera, @, music note, lettermarks — no trademark repros, zero image bytes), with an engraved separator. - first-level subpages (garage, writing, serendipity, around, whoareyou, music, coffee) take over the labeled .axp-pin app-button treatment; internal nav so they ride the window View Transition. - SW bumped v49 -> v50 to sweep the cached nav.js. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Desktop philosophy: taskbar = runnable apps only. Profiles move off the Quick Launch onto the wallpaper as desktop shortcuts, alongside new My Pictures + Notepad icons. Icons sit in the wallpaper gutter (gated to >=1024px so they never overlap the centred window), VT-frozen. - Run dialog: mouse hover now hot-tracks the list (selection follows the cursor), matching XP list controls — OK/Enter act on the hovered row. - Temporal: taskbar clock (nav.js) + Notepad F5 stamp (notepad.js) use Temporal.Now when present, Date as fallback. - base-select: the garage/horizon demo's drop-arrow is now a real raised XP combobox button that depresses when open; field gets a sunken inner shadow. @supports/native fallback unchanged. - SW v50 -> v51 to sweep cached nav.js + notepad.js. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nside) The page used to be the scroller, so windows slid up under the taskbar. Flip it: body becomes a clipping flex "desktop" (overflow:hidden), the window frame is pinned and bounded to the desktop, and its inner region scrolls internally — like a real OS window. Generalizes the model Notepad already used. - body:has(.window|.np-window|.wrap) → flex column, centred, overflow hidden. :has gates it so windowless pages (raw dir index) still scroll; unsupported browsers fall back to the scrolling body. - .window → flex column, max-height:100% (sized to content, never taller than the desktop); .window>.content / .window>.body scroll internally. - serendipity's .wrap made a transparent flex pass-through. - skeuomorphic XP scrollbar inside the scroll regions. - window now can't flow under the taskbar (hard floor) and stays static while you scroll; dragging is clipped to the desktop too. - SW v51 -> v52. Verified live across all three window families (homepage/garage/worker, Notepad, serendipity) at 1280x800: title bar stays fixed on scroll, window bottom never crosses the taskbar floor. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Strict windowing + window-management polish (all architecture-agnostic): - Custom XP scrollbar mounted on each window's right edge (native bar hidden via scrollbar-width:none + ::-webkit-scrollbar). Sunken track, raised thumb + arrow buttons; thumb drag, track paging, arrow stepping; ResizeObserver keeps it synced. Wheel/keyboard still scroll natively. - Drag clamp: the top is a hard wall (a window can't be dragged off the top, where it'd be unretrievable); the title bar also can't slide under the taskbar or fully off the sides. - Desktop icons are now freely draggable on the wallpaper, positions persisted in localStorage (#axp-icons became a pointer-events-pass- through full-desktop layer; icons absolutely positioned). - Resizable windows via a bottom-right grip (sets width/height, clamped to the desktop; scrollbar re-syncs through its ResizeObserver). - Removed the "My Pictures" shortcut (no Apache R2 bucket view to open). - SW v52 -> v53. NOT done: ripping the 2008 Twitter/Spotify logos — those are trademarked brand assets; kept the original stylized CSS glyphs instead. Verified live-equivalent in a real browser at 1280x800: scrollbar thumb tracks 0..max exactly, drag-up clamps at top=8, drag-down moves, icon drag persists, resize widens past the old 660 cap. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The /writing folder now inlines every note as a popover Notepad window
(notes are tiny .txt — cheap to inline, no fetch). Clicking a file opens
its note in the top layer, cascaded down-and-right of the folder so the
"selecting menu" stays visible alongside it — no navigation, same page.
- notepadWindow() gains a popId param → renders id + popover="auto" +
a close button that hides the popover.
- handleWritingIndex inlines all notes after the folder window; list <a>'s
keep their real /writing/<slug> href (no-JS / permalink fallback).
- notepad.js refactored single-window → per-window enhance(), plus folder
wiring: click → showPopover + pushState(/writing/<slug>); ✕/Esc/click-
away → history.back so the URL reverts and Back closes the note.
- CRITICAL fix: our .np-window{display:flex} beat the UA
[popover]:not(:popover-open){display:none}, leaking closed notes into
flow — added .np-note:not(:popover-open){display:none!important}.
- popover notes are .np-window, so they inherit drag, resize, and the
custom scrollbar for free.
- SW v53 -> v54.
Verified in a real browser at 1280x800: closed notes hidden, open note
cascades 38px right of the folder (menu peeks), URL pushes to the
permalink + reverts on close, scrollbar/resize present on the note.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…roll - Desktop icons now render BELOW the window (icon layer z-index 1, window frames get position:relative;z-index:2) — XP-correct; a window dragged over an icon covers it instead of the icon floating on top. - Photo thumbnails are fixed-size again: grid uses repeat(auto-fill,184px) + justify-content:center instead of repeat(3,1fr), so they never rescale when the window is resized (they reflow count) and the centered grid leaves a hover-safe gutter on each side. - Tooltips are suppressed while the content is actively scrolling (+220ms after it settles), so wheeling over the photo grid / tracklist no longer trips the cursor-follow tooltip. Returns on the next hover-move. - SW v54 -> v55. Verified at 1280x800: iconsZ=1 < winZ=2, grid cols fixed 184px centered, scroll dispatch raises no error + leaves the tooltip closed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Favicons: per-section CSS/SVG glyphs (colored tile + white pictogram) for garage/writing/serendipity/around/whoareyou/music/coffee. Used BOTH as each first-level route's tab favicon (nav.js setFavicon, exact-match only so garage sub-pages keep their own) AND its taskbar app-button icon — replacing the generic colored swatches. Favicons and the desktop shell now share one language. Cleanup (from the audit workflow's punch-list): - nav.js: removed orphan pinColor() (0 calls), dead .pic CSS + "pics" ternary (My Pictures leftovers), and now-unused SUBPAGES color fields. - sitemap.xml: rebuilt — was 4 stale URLs, now 14 (writing + 2 posts + all garage pages + serendipity), current lastmods, /coffee excluded by design. - llms.txt: writing/photos no longer "planned" (they're live); homepage no longer called a "holding page"; added /writing + /serendipity to Pages. - index.md: photo pool ~120 -> ~146. - README.md: rewrote the obsolete "under construction placeholder / swap to site/" text to describe the actual live site. - colophon.txt + horizon.html (x2): dropped references to the removed /source viewer. - CLAUDE.md: nav.js + notepad.js rows rewritten for the desktop shell / OS- window / popover notes; THUMB_VERSION 10->18; CACHE_VERSION -> v56; photo counts 120/131 -> 146; JPG+WebP -> AVIF+JPG; /source removed; design/ tokens.css -> design/tokens/. - SW v55 -> v56. Verified live: nav.js section icons + setFavicon present, 0 orphan refs, sitemap 14 routes, llms.txt updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Display labels are now Photos / Music. Kept the real destinations,
brand-coloured tiles, and glyphs (camera / music note) via a new `icon`
key on PROFILES that the tile-colour + glyph lookups read instead of the
label. `hint` carries the brand ("Instagram"/"Spotify") so the Run palette
still finds them by brand and the tooltip shows where they go.
- buildIcons uses qlColor/qlGlyph(it.icon || it.label).
- SW v56 -> v57.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Notes were popover="auto", which light-dismisses — opening a second note closed the first. Switched the inlined notes to popover="manual" so they stack like real windows: opening one no longer closes the others. - _worker.js notepadWindow: popover auto -> manual for popId notes. - notepad.js openNote: cascade each new note 26px down-right (offset by how many are already open), clamped to the desktop; re-open of an already-open note raises + focuses it. Dropped the single-note pushState/popstate/Back coupling (a single URL can't represent several open windows; the standalone /writing/<slug> permalink pages still work for deep links). Esc closes the topmost note (manual popovers don't light-dismiss); ✕ closes one. - SW v57 -> v58. Verified in a real browser: both notes open simultaneously + cascade, folder stays visible, closing one via ✕ leaves the other open. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- /writing resize: the folder window is a fixed-content listing — excluded it from initResize (it hugs content instead of ballooning into empty space). Note windows still resize, and .np-text now flex:1 so resizing a note grows the textarea rather than leaving a blank gap below it. - serendipity sidebar: the OS-window model scrolled the whole .body (sidebar + main) together, so the 200px sidebar ended mid-scroll with white below it. Override so .window>.body doesn't scroll and the main .content scrolls internally — the sidebar now fills full window height. - tooltips: lowered the post-scroll suppression cooldown 220ms -> 120ms so they feel snappy again right after scrolling. - SW v58 -> v59; serendipity worker redeployed. Verified in a real browser: serendipity pane fills body height + content scrolls; folder has no grip; note textarea flex-grow:1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Image metadata date (index.html fmtDate): EXIF capture time is a wall-clock
with no zone, upload timestamps are instants. Now uses Temporal.PlainDateTime
for the wall-clock (shown exactly as recorded) and Temporal.Instant→local for
instants; the no-Temporal fallback parses parts directly (no Date local
reinterpretation). Verified: "2026:05:23 08:41:43" -> 05-23-2026, 08:41;
ISO Z -> viewer-local.
- serendipity relativeTime + fmtDateTime: Temporal when present, Date fallback.
Runtime note: the Workers runtime has NO Temporal yet — there is no compat flag
for it ("No such compatibility flag: enable_temporal"), so serendipity runs the
Date fallback today and auto-upgrades if/when Cloudflare ships it. Browsers that
already ship Temporal use it client-side now.
Left as Date on purpose: HTTP Last-Modified (RFC-1123 toUTCString, which Temporal
can't emit), perf timers / cache-busters / epoch values (not calendar dates), and
machine ISO timestamps (Temporal.Instant.toString() is byte-identical — no gain).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ale dates) The homepage footer date was hand-hardcoded (stuck at May 18). Pages assets are content-addressed (ETag, no Last-Modified) and there's no build step, so there's no deploy timestamp to read — but the photo manifest carries per-photo `uploaded` times. The worker now rewrites the footer <time> to the most recently added photo (a real, datable content change; the pool grows often), floored by the hardcoded date in index.html so a copy-only edit can still bump it by hand. Bumped that floor to 2026-06-07. So it auto-advances whenever the photo pool grows — no more hand-stale footer. Verified live: footer reads "June 7, 2026". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e text-wrap Three "high-octane" features, all with clean fallbacks: - Speculation Rules, shell-wide: nav.js injects a prerender ruleset (eagerness "moderate" = hover-intent) on every page, so opening a window (garage/writing/ serendipity…) is near-instant and the View Transition plays on already-loaded content. Allowlist-by-exclusion: skips / (counter), /around (live crawl), /whoareyou, /rn (redirect), /coffee, /images*, raw text. Guarded so it doesn't duplicate the homepage's existing inline ruleset. Feature-detected via HTMLScriptElement.supports; unsupported browsers just navigate normally. - Container queries: windows are resizable, so serendipity's master-detail now stacks on WINDOW width (container-type on .window + @container 560px), not just viewport. The existing @media stays as the viewport / no-container fallback. Verified: .body is row at full width, flips to column when the window is dragged to 460px with the viewport still 1280. - text-wrap: pretty site-wide via nav.js (headings keep their own balance). - SW v59 -> v60; serendipity redeployed. Verified live: garage page gets exactly one injected speculation script (eagerness moderate, no dup); serendipity container query stacks on window width. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The cross-document View Transition freezes the root group, so it snaps to the destination's <html> immediately — and html was white on every page (only body carried the desktop tone), giving a frame of white before deferred nav.js paints the real wallpaper. nav.js can't fix first paint (it's deferred), so the fix is per-page first-paint CSS on the root: - index.html: Bliss gradient on html (was body-only). - 8 garage pages: html background added (sed; body already had it). - worker pages (around/whoareyou/bot/+1): xpChromeCss now paints html+body the Bliss gradient instead of the old light blue-white one — single point for all. - writing Notepad pages (NOTEPAD_CSS): html + body -> Bliss. - serendipity: Bliss on html. So the frozen root group shows the desktop colour, not white. Verified live: /garage/, /garage/encoding, /whoareyou, /around all paint the Bliss root. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The box-morph was riding the UA default (~0.25s) and the content cross-fade was 0.2/0.24s — a touch slow next to the prerender. Tightened: - ::view-transition-group(axp-window) geometry morph retimed to .16s ease-out (was the UA ~0.25s default). - old/new content fades .12s / .16s ease-out (was .2s / .24s ease). - less zoom: scale .992 / 1.008 (was .985 / 1.015). SW v60 -> v61. Verified live in nav.js. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Was a center morph + in-place cross-zoom (modern-app feel). Remapped to XP's "animate windows when minimizing/maximizing": the outgoing window zooms DOWN toward the taskbar and the incoming one zooms UP out of it. - transform-origin 50% 130% (below-centre, toward the taskbar at the bottom). - old: scale .66 + fade, ease-in (cubic-bezier(.4,0,1,1)), .18s — minimize. - new: scale .66 + fade, ease-out (cubic-bezier(0,0,.2,1)), .2s — restore. - group geometry morph kept but fast (.2s) so it doesn't fight the zoom. SW v61 -> v62. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Preview-server launch config + Claude Code worktrees are local-only; keep them out of the repo alongside the existing .claude/projects/ ignore. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ze grip The custom scrollbar sat at right:2px and ran the full content height, so its raised edge abutted the window's inner bevel on the right and piled onto the bottom bevel + the resize grip in the corner — reading as overlapping layers at the window boundary. Inset it: right 2->3px, top +1px, and stop 15px above the bottom so the bottom-right corner stays clear for the sizing grip (like XP, where the corner is the gripper). SW v62 -> v63. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New /garage/safari27 — WWDC26's Safari 27 (macOS 27 Golden Gate) through the lens of what this site already ships, not the full changelog. Leads with the features that intersect our work: - appearance: base-select going stable (we already ship a Luna one) - transform-aware anchor positioning + position-anchor normal/none (our tooltips) - scroll anchoring / overflow-anchor (our internally-scrolling windows) - :heading, stretch sizing, revert-rule, contain: style, linear color spaces - top-level await, Wasm JSPI, Service Worker static routing, transferable streams - <model> going cross-platform Each card has a "Safari 27 / now stable" tag + an "on this site" note, and the top strip runs live CSS.supports()/interface probes so every visitor sees what THEIR browser supports (octane + graceful fallback). Luna-styled like the rest of the garage; original compass favicon (not Apple's mark); features paraphrased from the WebKit WWDC26 post, which is linked as the source. Wired into the garage index, Run palette (PAGES), and sitemap. SW v63 -> v64. Verified live + in a real browser (7 cards, 11 chips, styled select renders). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Added a "The talks" section linking WebKit's sessions post + the six session videos, with the two that land on this site flagged: - Grid Lanes (CSS-only masonry) — horizon flagged masonry as not-shipped mid-2026; this is it shipping. Direct fit for the photo grid. - Customizable Select — the talk behind the base-select card above. Plus the Safari 27 overview, <model>, immersive environments, Web Extensions. Original tie-ins; session titles/URLs factual, WebKit roundup linked as source. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New /garage/masonry — a ragged-height contact sheet via display: grid-lanes (the masonry layout Safari 27 shipped), with the homepage's fixed-1:1-square grid as the @supports fallback. Same markup both ways. - columns: repeat(auto-fill, minmax(140px,1fr)); rows stack masonry where grid-lanes is supported, else fixed squares + object-fit:cover. - per-photo aspect-ratio reserved from metadata.json EXIF width/height (set as --ar), so masonry lays out correctly with zero layout shift as images load. - live "display: grid-lanes in your browser" chip + a Masonry/Fixed-squares toggle to A/B; toggle is disabled (and labelled) where grid-lanes is absent. - photos lazy-loaded from the worker manifest (same pattern as the homepage), avif<picture>+jpg. Wired into the garage index (newest), Run palette, sitemap. SW v64 -> v65. Verified in a real browser: 40 photos load, distinct aspect ratios reserved, fallback renders 1:1 squares (this engine lacks grid-lanes); live page 200 and the worker manifest route still serves (temp local manifest used only for the preview test, deleted before deploy). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Overlap fix (couldn't repro in Chromium at any width incl. 300px; Safari-only): - base-select demos had a hard min-width:220px that overflows narrow windows -> min-width:0; max-width:100% on safari27 .s27-sel + horizon .bsel-field. - .demo > h2 children get min-width:0 + overflow-wrap:anywhere so the title wraps below the stat badge instead of colliding on narrow widths. Voice pass (writing-style skill) on safari27 + masonry copy: - killed every em dash in the prose (per LRS), recast as commas/periods/colons. - straightened all curly quotes to straight quotes. - tightened ledes/notes into the voice; no banned phrases (none were present). - left title-bar / <title> "X — aadhar.sh" separators (site-wide convention) and code comments; flagged the separator for a separate call. Queued: garage index + the 7 older demo pages (chunks, cloudflare, encoding, horizon[219 em dashes], pretext, scroll, tooltips) for the same voice pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New /writing/education-in-tech note (BSD = Berkeley, Sun = Stanford), formatted like the others (title + rule + body). Registered in posts.json as the newest entry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Performance / quota: - SW: stop mirroring /images/full/* (R2 originals) into Cache Storage — the browser HTTP cache already holds them immutable, so it was double disk + a background re-fetch per hit for zero hit-rate gain. - tooltip EXIF moves to per-photo /images/meta/<stem>.json (lazy on hover), histograms computed client-side — drops the metadata.json blob from the hot path + the Pillow dep. worker guards the route so a miss can't SPA-fall-back to index.html and get stamped 1yr-immutable (poisoning a tooltip). - /index.html → 301 /, so the alias can't heuristically cache the no-store, re-randomizing, counter-ticking homepage. - homepage reads (asset, tracks, manifest, counter, altMap) fire concurrently; playlist-id module-cached so warm isolates do one KV read for tracks. Luna fidelity: - drop the soft blurred drop shadow on homepage + serendipity windows for the hard 4px offset shadow (soft blur is the SaaS tell DESIGN.md bans). - worker-page headings: Franklin Gothic -> Trebuchet MS (an approved stack; Franklin Gothic Medium isn't installed everywhere + wasn't in the 3 stacks). - speculation rules: also exclude /coffee* and /images*. - kept push-button border-radius at 3px (Luna), not squared. SW v66 -> v67. Verified: 146 meta/<stem>.json serve 200 JSON; buttons 3px live. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Decisions from the diff-session review: - corners: title-bar top-right 7px -> 8px everywhere (index.html, _worker.js chromes, all garage pages, masonry/safari27) so it matches the symmetric 8/8 window body. it was a typo, not a deliberate asymmetry. - amber: "pages win" -- the shipped pages use hue 47.10 (warn amber) and 95.44 (row hover), but design/tokens/colors.css had --hue-warn:75 feeding BOTH. split into --hue-warn:47.10 + new --hue-rowhover:95.44 and decoupled --row-hover. tokens-only, reference surface -- no live page change. Mechanical batch (landing the diff session's uncommitted sweep): - nav.js Run dialog: 5px -> 3px title/icon radii, soft float shadow now matches the --shadow-float token value (navy, single blur, was black double-blur), Geneva added to font fallbacks. - caption-button filter .1s -> 60ms ease-out everywhere. - font stacks: Geneva fallback restored across worker/nav/serendipity. - _worker.js getImagesManifest: two-key stale-while-revalidate (persistent manifest + 1h freshness sentinel) so only the true cold start pays the inline R2 list(); lapsed-freshness visitors get the stale manifest and the rebuild rides ctx.waitUntil. - index.html: relocated the ~27KB cursor-tooltip IIFE to just before </body> so the tracklist + photo grid paint before it parses. - cal/templates.js: palette re-sync to the canonical 5-stop title gradient, gel caption buttons, on-token ButtonFace + h1 navy; stale text-glyph comments updated to describe the gel controls. sw.js CACHE_VERSION -> aadhar-v68-luna-polish (nav.js changed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ran the copy on garage/index, chunks, cloudflare, encoding, horizon, pretext, scroll, tooltips through the LRS writing rules (safari27 + masonry were already done). Mechanical, no meaning or technical-accuracy changes: - killed every em dash in prose + author-voice comments (~320 total, horizon alone ~217), repunctuated by sense -- colon for a setup, comma for a light pause, period for a hard stop, parens for a true aside. never a blanket swap. - straightened all curly quotes/apostrophes to ASCII (0 remain anywhere). - rewrote the handful of "Not X, but Y" constructions affirmatively. - no banned words present (leverage/utilize/robust/delve/in order to/ furthermore) -- confirmed by sweep. Intentionally left (non-prose / site conventions): - <title> + og:title " — aadhar.sh" separators (sitewide convention, decided separately). - tooltips tracklist <span class="sep">—</span> + the JS "no data" placeholder glyph -- UI separators that mirror the homepage np-sep. - cloudflare .cf-out empty-state — placeholders (overwritten by JS). - horizon's two em dashes inside <pre> CSS code samples (literal code). Verified: all <div> balanced, all inline scripts parse, horizon + pretext render clean with 0 em dashes in visible text. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Second-layer revision across all 10 garage pages, applying the real LRS
sentence-level method (Williams & Colomb) on top of the surface cleanup:
put a concrete character in the subject slot, its action in the verb;
kill nominalizations (-ion/-ment/-ance) back into verbs; drop bleached
"X is", "there is", "this is" cores; prefer active where the passive hid
the doer; tighten cores; old-info-first, new-info-in-the-stress-position.
Examples of the move:
- "where a tool gets validated under real load" -> "where I run a tool
under real load"
- "Good cars get learned on mules first" -> "Carmakers learn the good
cars on mules first"
- "aspect ratios are reserved from EXIF" -> "EXIF pins each photo's
aspect ratio up front, so nothing reflows"
- "Three of them are wired to live demos" -> "I wired three of them to
live demos"
- "the cut points are decided by the data" -> "the cut points follow
the data"
Restrained on purpose: deliberate 2003 fragments ("Same spirit here,
fewer cones.") and definitional "is" cores left as voice. Surface rules
held (no em dashes/curly quotes reintroduced); div balance + all inline
scripts verified; index + horizon + pretext render clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Swap the ' — aadhar.sh' title separator for a root-first filepath form joined by ' / ', so every title reads like a real path: - aadhar.sh / garage - aadhar.sh / garage / safari 27 - aadhar.sh / writing / <slug>.txt - aadhar.sh / around · / bot · / whoareyou - aadhar.sh / serendipity · aadhar.sh / coffee / <page> Applied to browser <title>, og:title, and the on-screen window title-bar labels across the garage pages, the worker-rendered pages (writing index + notes, around, bot, whoareyou, rn/set), serendipity, and cal. Root-first on purpose: it labels "where you are" as an absolute path (the canonical reading), which is what "reads like a filepath" actually means; the relative tokens . / .. are navigation, not location, so they don't belong in a location label. Side benefit: removes every remaining em dash from title surfaces (the one separator class the earlier voice pass had deliberately left as a pending decision). Homepage title (the person's name) + the authentic "file - Notepad" window convention left as-is. JS parses; live titles verified. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collapse ' / ' -> '/' in the filepath titles so they read like a real
path: aadhar.sh/garage/safari 27, aadhar.sh/writing/<slug>.txt,
aadhar.sh/coffee/<page>, etc. Leaf names keep their internal spaces
("safari 27"); only the separators tighten.
Targeted full-string replacements, not a blind ' / ' sweep -- prose
slashes (encoding's "AVIF / WebP / JPEG"-style comparisons, table values
like "3 / 0") are left intact. JS parses; live titles verified.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The /writing folder listing now shows each note's size before you open
it ("Text Document · 1,388 characters · 2026-06-06"), and the folder
status bar totals them in a second beveled cell ("3 documents · 2,264
characters") -- the authentic XP folder details/status-bar layout.
No extra fetches: handleWritingIndex already pulls each .txt to inline the
popover Notepad windows, so the same text now also feeds the char count
(text.length). Refactored the two passes into one Promise.all.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Re-checked the volatile features against the page's own sources (caniuse / MDN baseline / webkit + mozilla standands-positions / chromestatus). Five had drifted; the rest were accurate: - contrast-color(): was "Safari + Firefox, Chrome lagging ~m147, needs fallback" → now Baseline (Chrome 147, Safari 26, Firefox 146; Apr 2026). Badge + "on the site" note rewritten — no fallback caveat. - command / commandfor (invoker): + Firefox 144 → Baseline across all three. - Temporal: Chrome 144 shipped (Jan 2026), joins Firefox 139 → 2 engines, badge promoted trial→cross. - field-sizing: + Firefox 152 (shipped Jun 2026) → all three. - scroll-driven animations: corrected the optimistic "Firefox" → "FF flag" (still behind layout.css.scroll-driven-animations.enabled; stable in Chrome + Safari only). No homepage adoption: contrast-color() is now Baseline, but the title-bar blue (oklch ~51%) sits right at the black/white contrast crossover, so contrast-color could resolve to BLACK and flip the XP title text — the hand-picked white is deliberate. Not worth gambling the look for a no-op on a static surface. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pushes the tracker out to the absolute edge of the platform, integrated or not, and finally represents the networking layer (a static site usually never touches it). New cards, each with a verified June-2026 cross-engine badge + an "on the site" hook, several with live demos: newer platform: - Element.moveBefore() — state-preserving reparenting (Chrome 133; FF/Safari positive). Live demo: an iframe uptime counter survives moveBefore() but resets on the appendChild fallback — exactly what the window shell's reparenting needs. - cross-document View Transitions (@view-transition) — Chrome 126 · Safari 18.2. - shape() responsive clip-path — Baseline (Chrome 119 · Safari 18.2 · FF 136); live pennant demo. - text-wrap: pretty — Chrome 117 · Safari 26; before/after demo. - @function custom CSS functions — Chrome 139. networking: - WebTransport (HTTP/3 + QUIC) — now Baseline (incl. Safari 26.4). - Service Worker static routing (InstallEvent.addRoutes) — Chrome; relevant to this site's own sw.js. - 103 Early Hints — server-side (Cloudflare-emittable); no chip since it's not a JS capability. far edge: - Local Font Access (queryLocalFonts) — Chrome-only + Safari/FF oppose; live enumeration demo, on-the-nose for a zero-web-font site. - WebGPU — now multi-engine (Chrome · Safari 26 · Firefox 141). cap-strip checks + labels added for the 9 client-detectable ones. Verified in preview: all cards render, chips light up, moveBefore demo preserves node state. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tic routing Two horizon features graduated to production. Cross-document View Transitions: a reduced-motion-safe @view-transition opt-in inlined into every same-origin surface (homepage, 12 garage articles, the worker-rendered pages, serendipity) — placed in parsed CSS, not JS-injected, so the INCOMING page has opted in by the time pagereveal fires (a deferred nav.js inject would be too late). Real page→page navigations now crossfade natively at a fast 140ms; the persistent shell (wallpaper/taskbar) is identical across pages so visually only the changing window content fades. Wrapped in prefers-reduced-motion: no-preference, and progressive (Chrome 126 + Safari 18.2 get it; Firefox ignores it → today's instant cut). Service Worker static routing (sw.js): declare the always-network paths (/, /around*, /whoareyou*, /rn*, /images/*.json, /images/meta/*) via InstallEvent.addRoutes so the browser serves them WITHOUT cold-booting the worker — these are network-only in the fetch handler anyway, so they were waking the SW just to no-op (notably `/`, no-store + every visit). SWR + cache-first paths stay with the handler (they need logic). Guarded by "addRoutes" in event + try/catch; unsupported browsers behave exactly as before. CACHE_VERSION → aadhar-v74-swroute. Verified: opt-in served on all four page types; sw.js parses + ships addRoutes; _worker.js / serendipity.js still parse with the inlined CSS rule. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…+ add Speculation Rules card Make it obvious which horizon features aren't just feasible but actually LIVE on the real site. A hand-kept list (audited against production usage in index.html / nav.js / _worker.js / sw.js / serendipity) gets a .shipped class via the cap-strip script: translucent green fill, a green inset edge, and a labeled "✓ shipped on the site" pill (so it reads without relying on colour alone). 12 shipped: text-box-trim, anchor-positioning, view-transitions, cross-doc-vt, relative-color, @Property, @starting-style, Temporal, speculation-rules, sw-static-routing, hidden-until-found, contrast-color. Also: - Added the missing Speculation Rules card (it was pitched + is shipped — the prerender — but never got a card). + cap-strip check/label. - Fixed three now-inaccurate "on the site" notes the green tag exposed: · contrast-color — it's serendipity avatar text (NOT the homepage title bars, which we reverted at the b/w crossover), corrected to say so. · cross-doc-vt + sw-static-routing — flipped "would" → "live now" since they shipped this session. Verified in preview: 12 cards green + tagged, non-shipped (e.g. moveBefore) stay white. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…i + bounce) The site's Run palette (⌘K) is itself a command palette, so having it fire a Raycast command is a fitting meta-joke. Two built-in Raycast commands (every Raycast user has them) as palette entries: · confetti 🎉 → raycast://extensions/raycast/raycast/confetti · toggle bounce → raycast://extensions/raycast/raycast/toggle-bounce-animation New kind "raycast": go() does location.href to the protocol URL (the OS hands it to Raycast, the page stays put); rendered with a "↗ raycast" tag and an explicit "needs Raycast installed" hint so non-Raycast folks aren't surprised by the OS "open?" prompt. Naturally excluded from the prerender ruleset (href isn't a "/*" path) and from the taskbar/desktop (Run-palette pool only). PAGES tagger now preserves a pre-set kind. CACHE_VERSION → aadhar-v75-confetti. Built-ins only — the personal installed extensions (brew, slack, etc.) wouldn't resolve on a visitor's machine and would leak tooling, so they're excluded. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rt horizon card
Skip rendering off-screen repeated content where there's enough of it to matter:
- garage/horizon (48 demo cards, several with their own demos/animations) — the
page I just made heavy; `.demo { content-visibility: auto; contain-intrinsic-size:
auto 360px }`. The intrinsic-size reserves a placeholder so the scrollbar stays
stable, and the `auto` keyword remembers each card's real height after first
render → no shift on re-scroll.
- garage/safari27 (8 demo cards) — same treatment.
- serendipity event list (.ev) — long list (120+ events incl. the browsed tier);
contain-intrinsic-size: auto 66px per row.
Not applied to the homepage (above-fold dynamic content — the CLS reason it was
removed there before) or the short single-demo garage pages (nothing meaningful
off-screen; bespoke selectors, no gain). content-visibility is Baseline 2026
(Chrome 85 / Firefox 125 / Safari 18); degrades to normal rendering elsewhere.
Also added the Compression Dictionary Transport card to horizon (dcb/dcz —
Chrome 130, Cloudflare open beta Apr 2026), honest that its payoff here is
marginal given the site's already-tiny assets. Chip-less (transport-level, not a
JS capability), like 103 Early Hints.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The OS model scrolls .content internally (overflow:auto behind the custom scrollbar), which browsers don't reliably restore on reload — the no-store homepage re-renders, and the scroller is JS-geometry-aware. So persist the primary window's scrollTop per-path in sessionStorage (per tab, survives reload, clears on close) and restore it. Scoped tightly: restore ONLY when navType === "reload" — fresh navigations start at top, and back/forward is left to the browser's bfcache. Write is cheap (scrollend where supported, else a 200ms throttle; plus a pagehide flush). Restore re-asserts on `load` until it sticks, so a not-yet-laid-out scroller (images/fonts settling, or a 0-height first frame) doesn't clamp it to 0. Verified in preview: scroll → reload restores exactly; a fresh navigation does not. nav.js change → CACHE_VERSION aadhar-v76-scroll. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… found by it
Local-only test suite for the (undeployed) coffee-booking worker — never bundles
into a deploy (wrangler only bundles src/index.js + its imports; tests live in
test/, devDeps in package.json, node_modules gitignored). 40 tests across the
real workerd runtime (@cloudflare/vitest-pool-workers, vitest 4):
· sign.test.js HMAC approve/decline link auth (tamper, rotation, malformed)
· availability.test.js generateSlots() invariants, busy-overlap, daily limit
· booking.test.js KV lifecycle + the cron expire sweep
· email.test.js Resend payloads + .ics generation (fetch stubbed)
· index.test.js full request flow via worker.fetch(): routing, validation,
honeypot, BASE_PATH, book→approve / book→decline cycles
Writing the email tests surfaced two real .ics bugs (src/email.js), now fixed:
1. attachment used btoa(ics) — throws on any code point >255, so an emoji/
accent in a booking topic/name made sendInvite throw inside ctx.waitUntil →
the confirmation email silently never sent. Now UTF-8 → base64.
2. DESCRIPTION newlines were double-escaped (\\n then escICS'd) → rendered as
literal "\n" in calendars. Fixed to single ICS escapes. Plus octet/codepoint-
aware line folding so an emoji near the 75-octet fold boundary isn't split.
Run: cd cal && npm test. Nothing here deploys.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…KV id)
Prep for going live as a native worker (replacing the Cal.com setup):
- cal.aadhar.sh route: custom_domain can't carry a path/wildcard — "cal.aadhar.sh/*"
was rejected ("Wildcard operators are not allowed in Custom Domains"). Changed
to the bare host "cal.aadhar.sh" (CF auto-provisions DNS + cert, routes the whole
hostname). The canonical aadhar.sh/coffee + /coffee/* routes are unchanged.
- BOOKINGS KV: created the namespace + wired its real id (was a placeholder).
Dry-run now clean (44 KiB worker, KV bound). SIGNING_SECRET is set on the worker
(generated random). Still gated on two operator-only secrets before deploy:
ICAL_URL + RESEND_API_KEY.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deploying aadhar-sh with
|
| Latest commit: |
ce33c09
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://87a20acf.aadhar-sh.pages.dev |
oddharsh
added a commit
that referenced
this pull request
Jul 27, 2026
Drove an actual webmention through the deployed receiver from a public source page. It verified and stored correctly, but the moderation email was 280 characters of SVG path geometry: 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018…"> A throwaway source page. It exists to drive one real w Two causes, either of which is enough on its own. <svg> was not stripped alongside <script> and <style>, and a page like a GitHub gist is full of inline icons. On its own that is survivable, since the tag stripper removes complete tags with their attributes. What made it escape is the excerpt window. It is a fixed 400 characters either side of the target, cut at a byte offset, and the target lives inside an href — so centring on a link GUARANTEES the cut lands mid-tag, and often mid-attribute. A leading partial tag is not a complete tag, so the stripper leaves it as text. That is the stray "> in the middle. Now strips <svg>/<noscript> and drops the partial tags at both edges, detecting a mid-tag cut by whether > precedes < in the slice. Same page now yields the actual sentence around the link. Also fixes two things found alongside it: 202 no longer carries a Location header. The spec ties Location to 201, where it names a status URL the sender can poll; webmention.rocks receiver test #1 fails a 202 that sends one. /inbox was never a status URL for a pending mention anyway, since a pending mention deliberately does not appear there. Citations now skip anchors that exist to open a hover card: interestfor on the garage pages, class="car-link" on the homepage. Every one of them points at a Google SEARCH url, so telling google.com/search?q=Singer it had been cited is noise, not credit. Live citation targets drop 56 -> 53, and all three removed are search pages. Verified the excerpt test fails with both fixes removed, reproducing the exact garbage above. Each fix alone is sufficient, so it takes removing both to break it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
oddharsh
added a commit
that referenced
this pull request
Jul 28, 2026
* fix(social): endpoint discovery was taking three of the decoys Ran the site's sender against the IndieWeb conformance suite at webmention.rocks, which plants deliberate traps in its 23 discovery pages. Three of them caught us. All three were the same shape: the parser was matching on text rather than on structure. #12 rel="not-webmention" took the decoy #13 decoy inside <!-- --> took the decoy #20 <link rel="webmention"> gave up instead of reading on 12 is the sharp one. The old test was /\bwebmention\b/, and "-" is a word boundary, so "not-webmention" matched. rel is a space-separated token list, so the check is now tokenize-and-compare, in the header path too. 13 needed comments stripped before the scan (escaped HTML in #14 was already safe, since these patterns want a literal "<"). 20 was treating the first rel=webmention tag as the answer even with no href, so a bare <link> shadowed the real <a> further down; it now skips candidates without an href and keeps looking. href="" is deliberately NOT that case: it is a legal self-reference, and #15 asserts it. Kept the single document-order scan. Tempting to look at every <link> before any <a>, but #16 exists precisely to punish that: it puts the real endpoint in an <a> and a decoy in a later <link>. 23/23 against the live suite now, up from 20. Frozen as fixtures rather than live fetches, because this file is deliberately network-free. Confirmed the fixtures fail against the old parser. discoverEndpoint is exported to make the suite runnable, matching the other helpers here that are already exported for tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(social): a real mention arrived reading like spam, twice over Drove an actual webmention through the deployed receiver from a public source page. It verified and stored correctly, but the moderation email was 280 characters of SVG path geometry: 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018…"> A throwaway source page. It exists to drive one real w Two causes, either of which is enough on its own. <svg> was not stripped alongside <script> and <style>, and a page like a GitHub gist is full of inline icons. On its own that is survivable, since the tag stripper removes complete tags with their attributes. What made it escape is the excerpt window. It is a fixed 400 characters either side of the target, cut at a byte offset, and the target lives inside an href — so centring on a link GUARANTEES the cut lands mid-tag, and often mid-attribute. A leading partial tag is not a complete tag, so the stripper leaves it as text. That is the stray "> in the middle. Now strips <svg>/<noscript> and drops the partial tags at both edges, detecting a mid-tag cut by whether > precedes < in the slice. Same page now yields the actual sentence around the link. Also fixes two things found alongside it: 202 no longer carries a Location header. The spec ties Location to 201, where it names a status URL the sender can poll; webmention.rocks receiver test #1 fails a 202 that sends one. /inbox was never a status URL for a pending mention anyway, since a pending mention deliberately does not appear there. Citations now skip anchors that exist to open a hover card: interestfor on the garage pages, class="car-link" on the homepage. Every one of them points at a Google SEARCH url, so telling google.com/search?q=Singer it had been cited is noise, not credit. Live citation targets drop 56 -> 53, and all three removed are search pages. Verified the excerpt test fails with both fixes removed, reproducing the exact garbage above. Each fix alone is sufficient, so it takes removing both to break it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Aadharsh Pannirselvam <19518661+oddharsh@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
oddharsh
added a commit
that referenced
this pull request
Aug 12, 2026
Renaming or moving a page turned every page LINKING to it into a 404, and nothing in this repo noticed. routes:check sweeps the routes it is TOLD about, which is the forward direction. Invariant #1 asserts the Worker's routes reach run_worker_first. Neither one reads an href. scripts/lib/link-integrity.mjs is the resolver, run as a build invariant over the MINIFIED tree, since those are the bytes a visitor gets and the minifier rewrites asset refs to their hashed /a/ form. COMPLETE rather than scoped to the diff, because it costs nothing to be: 2645 refs across 48 documents in ~45ms. A diff-scoped version would be more code and would miss the case that matters most, where the moved page is not in the diff and its dependents are. No second copy of the route table. The inputs are exactly what invariant #1 already extracts (the ROUTES keys, run_worker_first's globs) plus the surface registry and the staged tree, so a route added anywhere is understood here with no matching edit. Two findings from building it, both now load-bearing: run_worker_first cannot be the resolver on its own. It answers whether the Worker SEES a request, not whether a path SERVES a page, and it carries /garage/* and /lwe/* — the namespaces holding most of the site's pages. Measured on a deliberately broken ref, a glob-only resolver called /garage/renamed-page fine. So a namespace that already holds registered surfaces is GOVERNED by the registry and the glob buys it nothing there. The governed set is derived from site-manifest.json, so adding a section governs it with no edit. The scanner has to be quote-aware. minify-html unquotes attributes, so the served bytes carry href=/coffee far more often than href="/coffee", and the first draft read 33 refs where there are 2645 and passed with a straight face. Third naive scanner this repo's minified output has caught. Tested for teeth rather than agreement, since the tree currently has zero dangling refs and a test asserting only "the real site passes" would survive the resolver being reduced to () => true. Three unit tests cover accept, reject and the scanner; end to end, injecting <a href=/garage/renamed-page> into www/index.html fails the build with the path and the referring document named. Non-goals recorded in CLAUDE.md: prose mentions of a path (this repo discusses deliberately deleted routes, so that would false-fire on its own history), the Run palette and nav fences (generated from the registry, asserted by invariant #8), and off-origin links. 247 tests, build, pages:check, typecheck and 130 routes all pass.
oddharsh
added a commit
that referenced
this pull request
Aug 12, 2026
…#351) * ci: ramp production from Actions, and stop trusting an empty variable Four release steps ran on a workstation or not at all. Three of them move to the repo here. RAMP. `.github/workflows/ramp.yml` is new, and CLAUDE.md has described it since 2026-08-06 without it ever existing: no such workflow in any commit, environments `copilot`/`production` rather than the canary/full pair, and no write token. What landed that day was the `release-guard.mjs` rewrite alone, which asks whether a process can authenticate instead of whether it is CI. That is what makes this possible; the pipeline it cleared the way for is what was missing. It splits at the human rather than removing them: canary 10% production-canary, no reviewer, runs on its own full 50% -> 100% production-full, REQUIRED REVIEWER, waits for a click verify none no environment, no credential, runs dcz:check A merge reaches a tenth of traffic by itself and stops. Approving `full` is the same decision as before, minus the terminal. Three parts of it are load-bearing: - It checks out `production`, never `main`. The ramp writes the D1 changelog by diffing the checked-out checkpoints.json against D1, so the tree has to be the one that was built. This is gotcha 24 solved rather than moved: CI cannot ramp from a stale tree, nor from one running ahead of what is serving. - `full` refuses a version the canary never saw. Approval is asynchronous and Workers Builds uploads a version for every push, so re-resolving the newest build after an approval could put traffic on something nothing canaried, silently, with every downstream check passing because a version that built returns 200s. - Every step runs through `pnpm run`. deploy-promote.mjs shells out to `npx wrangler`, and npx resolves whatever it finds: measured in a tree with no node_modules, `npx wrangler --version` answered 4.105.0 from its own cache while this repo pins 4.120.0. Both environments restrict deployments to `main`, which is not tidiness: `workflow_dispatch` can target any branch carrying the workflow file, so without a branch policy anyone able to push a branch could ship a modified ramp.yml and read the token out of it. CHECKPOINTS. `checkpoints:check` was deliberately unwired ("NOT wired into the PR job by default", its own header) because it needs a D1 read. It needs no NEW credential: D1:Read is already one of the six scopes on CLOUDFLARE_API_TOKEN. Fatal on drift, which is safe because the script already tells a STAGED release apart from a rewrite, so a PR staging v187 passes. Tiered on the token the way infra:check is, since failing a fork PR for not holding a secret is a worse check than no check. OG CARDS. `og-cards.yml` bakes the cards on a runner and opens a PR. Dispatch-only, never scheduled: the cards capture production so data-driven demos render populated, and the photo grid, live counters and routing prober are not deterministic between runs, so a nightly job would propose a PR of changed PNGs every night that meant nothing. dictionary-roll.yml can be nightly because its output is a pure function of what production serves; this is not. AN EMPTY VARIABLE, found on the way. ci.yml passed `CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}` to infra:check and NO repository variable of that name has ever existed (`gh variable list` returns nothing), so it expanded to "" on every run. It reads scripts/print-account-id.mjs now, which cannot be silently empty and cannot drift from the account the deploy targets, because it IS that string. This is HARDENING, not a repair, and the difference is worth stating because the first version of this message got it wrong. checkApi() falls back to GET /accounts and only degrades when the count is not exactly 1; the scoped CI token sees exactly one, so the account tier was asserting normally. Measured: `infra ok: 44 checks passed, 1 skipped or advisory` both before and after, with the one advisory being the documented CodeQL default-setup note and no `token sees N accounts` warning in either run. The two-account fact belongs to the owner's interactive wrangler login, which is a DIFFERENT credential from the CI token, and conflating them is what produced the wrong claim. What the helper is genuinely required for is the ramp: release-guard.mjs returns an error unless CLOUDFLARE_ACCOUNT_ID is set in CI, unconditionally, so the canary and full jobs cannot authenticate without it. What did NOT need building, checked rather than assumed: md twins are build output regenerated every build, with checkTwinFacts pinning the hand-written ones in both directions; photos already have photo-pipeline.yml plus photos:check in CI; dictionaries already have dictionary-roll.yml. Environments are created but NOT declared in infra.json, which leaves the required reviewer on the honour system. Asserting them in check-infra.mjs is the follow-up and is deliberately not bundled with the change that created them. 244 contract tests, typecheck, build and pages:check all pass; checkpoints:check passes against live D1 (122 released, newest v186). * ci: pass workflow values through env, never ${{ }} inside the script Four spots interpolated an expression directly into a `run:` block. A ${{ }} expansion is substituted into the script TEXT before bash parses it, so a value carrying shell metacharacters executes. `inputs.base` in og-cards.yml is the one that matters: a URL somebody types into the dispatch form. Dispatching needs write access, so this is hygiene rather than a hole, and the other three values were already constrained (a hex prefix the canary matched with a regex, and a literal true/false). Fixed all four anyway, because the safe shape should be the habit rather than a judgement call per value. Found by reading the diff, not by the security check: github-advanced-security failed on this PR for the eighth recorded time with `CAPIError: 400 The requested model is not supported`, so Copilot's agent crashed before reading anything (see gotcha 27). `Analyze (actions)` passed and flagged none of this. * docs: gotcha 27, the security check now fires on every push Three more occurrences on 2026-08-12, all the same CAPIError: 400, taking it to nine recorded. One landed on a commit whose entire diff moved ${{ }} expressions out of run: blocks and into env:, which strictly reduces attack surface — so a red mark here carries no information about the diff even in the direction it claims to. Those three cost three separate investigations in one session, which is the actual bill for a check that gates nothing. So the note now ends with the short loop: confirm the run id is new, grep for CAPIError, stop. Do not re-read the diff, and do not push to appease it. Names the settings-side fix for when the noise stops being worth it, and records that CodeQL and Analyze (actions) are the checks doing the real work. Also records what Analyze (actions) does NOT catch, since it passed on the ${{ inputs.base }} interpolation fixed in a76ea18: the CodeQL Actions analyzer treats workflow_dispatch inputs as trusted, so that injection class is on the author to read for. * refactor: pnpm exec for the eight npx call sites the sweep missed gotcha 29 says the repo moved off npx because npx FETCHES what it cannot resolve locally, which is a hole in a repo that pins wrangler exactly and enforces one version on every PR. It also says "all 30 call sites" were converted. Eight were not: scripts/check-checkpoints.mjs d1 execute scripts/deploy-promote.mjs the RAMP scripts/lens-webmcp.mjs browser create + close scripts/perf-budget.mjs deploy --dry-run, check startup scripts/perf-snapshot.mjs deploy --dry-run scripts/release-status.mjs versions list Each spawned execFile("npx", ["wrangler", ...]), so no grep for `npx wrangler` as a phrase could find them: the command is assembled from array elements and only the word "npx" appears as a token. Same blind spot the holding/ rename hit from the other direction, and the rule that generalises is to grep for "npx" quoted rather than npx <binary> as a phrase. deploy-promote.mjs was the one that mattered: the wrangler moving production traffic depended on how the ramp was invoked. Under `pnpm run` it got the pin; from a tree with no node_modules it got whatever npx had cached, measured at 4.105.0 against a pinned 4.120.0. pnpm exec, not pnpm dlx. dlx is npx's true twin and would fetch from the registry and ignore the pin, which is the hole rather than the fix; wrangler is a pinned devDependency, so exec is correct. The three npx strings in infra.json and its mirrors still stand on purpose: they RECORD dashboard values, and editing them alone fails infra:check on drift it invented itself. Exercised rather than assumed, since a bad spawn surfaces as a missing binary at the worst possible moment: checkpoints:check reads D1 (122 released, newest v186), perf-budget runs both wrangler calls and reports hard checks green, release-status lists versions. 244 tests and the build pass. Two comments I wrote hours ago went stale with this and are corrected: ramp.yml and CLAUDE.md both explained using `pnpm run` as a guardrail against npx, which is now consistency rather than protection. * docs: the ramp token needs five permissions, not two This repo has said `Workers Scripts:Edit` + `D1:Edit` "and nothing else" since 2026-08-06, and since no such token existed until now the list was never checked against a running ramp. Derived from what deploy-promote.mjs actually executes: Account Workers Scripts Edit versions list, deployments status, versions deploy Account D1 Edit SELECT shipped vnums, INSERT the changelog row Account Account Settings Read wrangler resolves the account User User Details Read wrangler identifies itself non-interactively User Memberships Read same The bottom three are wrangler's own identity calls rather than anything the ramp asks for, which is why a two-scope list read as complete. Narrower than the token Workers Builds generates for itself in one place worth recording: no Workers Routes, no KV, no R2. A ramp shifts traffic between versions that already exist and never uploads one. Also documents the control (CI=1 matters, or release-guard ignores the token and falls back to the interactive login, so the check tests the wrong credential) and the one failure that is silent: the changelog INSERT runs only after 100% and is caught rather than fatal, so a missing D1:Edit costs a changelog gap on a ramp that reports success. checkpoints:check is the backstop and goes red on the next PR. * docs: turning off Copilot Autofix does not stop the security check gotcha 27 said the fix for the noise was in GitHub's settings and named Copilot Autofix. Measured: the owner turned it off and the very next push failed identically, 12th recorded occurrence. The toggle did land, and the log shows it changed something real: COPILOT_AGENT_ONLINE_EVALUATION_DISABLED: true [skills] session=github/code-scanning enabled=false source=disabled and the job ran 29s instead of ~80s. It then crashed on the same CAPIError: 400 regardless. So the setting is not the off switch, and which setting is (if any) is now recorded as UNKNOWN rather than guessed at a second time. This is the second wrong claim I have had to walk back in this note, both from asserting a cause without measuring it. The note now says what is known: the check configures nothing in this repo, it gates nothing, CodeQL and Analyze (actions) do the real scanning and stay green. * docs: mark which ramp-token scopes are measured and which are guessed The scope table listed five permissions as though equally established. Two are not: User Details:Read and Memberships:Read were inferred from the token Workers Builds generates for itself, not from anything deploy-promote.mjs is known to call, and CLOUDFLARE_ACCOUNT_ID is passed explicitly so wrangler need not enumerate anything. Marked SPECULATIVE, with the guidance to grant the three Account rows and run the control instead of collecting all five. Also records why they look absent from the dashboard: User-scoped permissions exist only on USER-owned tokens (My Profile, API Tokens). An account-owned token created from Manage Account does not offer the category at all, which reads like a missing option and is a token-type difference. * docs: the ramp token needs three Account scopes, measured Verified with a real token: CI=1 deploy:promote --dry-run resolved a target, printed the current split, and skipped two non-production versions by alias, on Workers Scripts:Edit + D1:Edit + Account Settings:Read alone. So the two User rows come out. They were inferred from the token Workers Builds generates for itself rather than from anything the ramp calls, they are unnecessary because CLOUDFLARE_ACCOUNT_ID removes the account-enumeration call they would have served, and they are unavailable on an account-owned token — so listing them sent somebody hunting for a dashboard category that was never there. Records what the control does NOT prove, since it only reads: a token holding Workers Scripts:Read would satisfy it too. versions deploy and the D1 INSERT are first exercised on a real ramp, and the D1 one is caught rather than fatal. Third correction to this token note in one session, all three from asserting a cause before measuring it. * build: fail when a moved page leaves its inbound links behind Renaming or moving a page turned every page LINKING to it into a 404, and nothing in this repo noticed. routes:check sweeps the routes it is TOLD about, which is the forward direction. Invariant #1 asserts the Worker's routes reach run_worker_first. Neither one reads an href. scripts/lib/link-integrity.mjs is the resolver, run as a build invariant over the MINIFIED tree, since those are the bytes a visitor gets and the minifier rewrites asset refs to their hashed /a/ form. COMPLETE rather than scoped to the diff, because it costs nothing to be: 2645 refs across 48 documents in ~45ms. A diff-scoped version would be more code and would miss the case that matters most, where the moved page is not in the diff and its dependents are. No second copy of the route table. The inputs are exactly what invariant #1 already extracts (the ROUTES keys, run_worker_first's globs) plus the surface registry and the staged tree, so a route added anywhere is understood here with no matching edit. Two findings from building it, both now load-bearing: run_worker_first cannot be the resolver on its own. It answers whether the Worker SEES a request, not whether a path SERVES a page, and it carries /garage/* and /lwe/* — the namespaces holding most of the site's pages. Measured on a deliberately broken ref, a glob-only resolver called /garage/renamed-page fine. So a namespace that already holds registered surfaces is GOVERNED by the registry and the glob buys it nothing there. The governed set is derived from site-manifest.json, so adding a section governs it with no edit. The scanner has to be quote-aware. minify-html unquotes attributes, so the served bytes carry href=/coffee far more often than href="/coffee", and the first draft read 33 refs where there are 2645 and passed with a straight face. Third naive scanner this repo's minified output has caught. Tested for teeth rather than agreement, since the tree currently has zero dangling refs and a test asserting only "the real site passes" would survive the resolver being reduced to () => true. Three unit tests cover accept, reject and the scanner; end to end, injecting <a href=/garage/renamed-page> into www/index.html fails the build with the path and the referring document named. Non-goals recorded in CLAUDE.md: prose mentions of a path (this repo discusses deliberately deleted routes, so that would false-fire on its own history), the Run palette and nav fences (generated from the registry, asserted by invariant #8), and off-origin links. 247 tests, build, pages:check, typecheck and 130 routes all pass. --------- Co-authored-by: Aadharsh Pannirselvam <19518661+oddharsh@users.noreply.github.com>
This was referenced Aug 18, 2026
oddharsh
added a commit
that referenced
this pull request
Aug 19, 2026
…on any origin (#469) NLWeb is a thin convention: answer natural language at /ask, return schema.org objects, expose the same thing over MCP. This origin had the MCP half and a search index over its own corpus, so what was missing was the endpoint, its parameter contract, and a schema.org projection of the search records. The `ask` MCP tool calls the same function, since the protocol specifies both doors carrying the same arguments and the same answer. Streaming defaults to TRUE per the spec, and the dialect is selected by request SHAPE alone: a `query` arriving as an object is v0.55 and gets named SSE events, anything else is legacy and gets unnamed message_type frames. Three things are refused rather than faked, and each could have shipped as a 200 that looked right. mode=summarize and mode=generate need a language model this Worker has no binding for, so both answer 501 naming supported_modes. A `prev` with no decontextualized_query gets the raw query searched AND a _meta line saying so. Descriptions are extractive and _meta says so, because the spec annotates that field "generated by an llm" and ours are not. /lens/nlweb walks through the door the doors tier only knocks on, grading the answer PER FIELD. Opt-in, cached 1h per origin AND question, 4/min. Pointing it at the new endpoint found the door probe grading a REAL NLWeb server as absent, twice: the protocol streams by default, and `query` is required so a conforming server must refuse a bare knock. Both read `likely` now, with the earlier tightening intact. Three repairs found on the way: build invariant #1 had been vacuous since ROUTE_TABLE was extracted (0 of 85 routes checked), `pnpm run search:index` had been exiting ENOENT since the www/ split (three live pages invisible to search), and stripMarkup left character references in strings /ask now publishes inside schema.org objects.
oddharsh
added a commit
that referenced
this pull request
Aug 28, 2026
webmention.rocks receiver test 1, sent at production on 2026-08-28, verified and
stored correctly and named nobody. The row:
kind reply (correct: the source carries u-in-reply-to)
author "" <-
title Webmention Rocks!
status pending
Two bugs, both in the fallback chain, both visible in the source's markup:
<div class="left p-author h-card">
<a href="/"><img class="u-photo" alt="Webmention Rocks!"></a>
</div>
...
<h1 class="p-name"><a href="/receive/1">Receiver Test #1</a></h1>
The p-author pattern matched, then captured the WHITESPACE between that div's
">" and the "<a" on the next line. A whitespace string is truthy, so the `||`
chain stopped there and the documented hostname floor never ran; clean() then
reduced the winner to "". An h-card wrapping a linked photo is the ordinary
shape on a site that marks its author up properly, so this was never a quirk of
the test post: every reply from a real IndieWeb site would have arrived blank.
And the second fallback was worse than no fallback. In microformats2 a p-name
inside an h-card is the author's name, while a p-name inside an h-entry is the
POST's name, and a pattern that cannot see which parent it sits under reads the
title. On this very document that fallback calls the author "Receiver Test #1",
which is confidently wrong where the hostname would merely have been plain.
So the chain reads the p-author ELEMENT, bounded by counting its own tag's
nesting rather than by a character window, because the entry title sits a few
hundred characters past the h-card's end and a window swallows it. Inside that
element: nested p-name, then text, then the photo's alt, which is what mf2 says
carries the name for a p-* property. Every candidate has to survive clean() as a
non-empty string to win, so the hostname floor is reachable again.
Re-parsed against the saved source: author "Webmention Rocks!", and the id comes
back byte-identical to the row production stored, so a re-send still updates
that row rather than minting a second one.
The fixture in the tests is that markup verbatim rather than paraphrased, since
both bugs live in its exact shape. Control run: the old chain fails it with
actual "".
oddharsh
added a commit
that referenced
this pull request
Aug 28, 2026
…d so (#644) * fix(webmention): the outbound half never read a page, and nothing said so `fetchOwnPage` used a plain `fetch()` at our own origin. A Worker cannot do that: the request is blocked as recursion (error 1042), which perf-probe.ts and lens's SELF_FETCH each already document. It failed by RETURNING "", so every page hit `if (!html) continue`, every run wrote zero rows, and the job reported the same silence a working job reports. Measured 2026-08-28 against production: `webmentions_sent` held 0 rows and `webmentions` held 0 rows, while /around on the same daily tick had run that morning and written its KV snapshot at 05:41:06Z. The whole outbound half had never fetched one of its own pages since it shipped. Two fixes, because the first one alone would have hit the second. Reads go through the injected dispatcher. `withSelfFetch` is factored out of the fetch handler so `scheduled()` can build the same seam, and the cron gets it; `fetchOwnPage` takes SELF_FETCH, falls back to ASSETS, and has no plain-fetch arm, because that arm is the bug. Verified in local dev against the real scheduled handler: "5 pages read, 8 citations considered", where the same line read "0 pages read" before. Then the run is bounded by a subrequest budget. A full sweep is ~35 pages plus ~100 citations at 2-3 D1 statements each, and Workers Free allows 50 subrequests per invocation, so the loop could never have finished one — it would have died partway with an exception the `.catch(() => {})` swallows, which is the same silence again. One bulk read of prior sends replaces the per-citation SELECT (~100 subrequests back), the loop stops while it can still afford a whole target, and pages rotate by a self-pair marker row so a bounded run resumes somewhere new instead of re-sweeping the first five forever. Three runs in local dev swept 5, then 10, then 12 of 35 pages at 38/40, 40/40 and 36/40. The tests are the other half of the finding. The three that existed covered the pure filters; nothing covered the loop, which is exactly the seam that broke. Three new ones pin that a run reads its pages through the dispatcher and records what each probe learned, that a run with no dispatcher records nothing rather than looking like a page with no citations, and that a run stays inside the budget while advancing the rotation. Inbound needed no change and was re-verified on the wire: discovery advertised on every mention-enabled page, 202 with no Location, correct CORS, every validation path, and the anti-forgery link check scored 8/8 against real pages (including an href that exists only inside webmention.rocks' copy-paste textarea, which correctly does not count as a link). * feat(webmention): /inbox cites the IndieWeb wiki, which is the first target that listens Swept every citation this site makes: 0 of 97 across 39 pages advertise a webmention endpoint. Controls in the same run (aadhar.sh/garage/pqc, webmention.rocks/test/1, aaronpk.com) all resolved, so the prober works and the citation graph genuinely has nobody on the other end. It is Wikipedia, GitHub, vendor docs and research blogs, and none of them do webmention. So the outbound half was correct and structurally mute. /inbox is the one page here whose prose is about webmention, and it already carried a real external citation to the W3C REC, which advertises no endpoint. It now also cites the IndieWeb wiki, which does: indieweb.org/Webmention answers with webmention.io/indiewebcamp/webmention. That makes the page's flag true in both directions, which is the coherent reading for the inbox: the page that displays mentions accepts them, and the page that explains the protocol participates in it. Nothing about the citation is staged for the demo, which was the bar. adactio.com and tantek.com also listen and are not cited here, because there is no honest reason to. Confirmed against the local dev worker: /inbox is in the send list (36 pages), its two citations extract cleanly, and exactly one of them resolves an endpoint. It sits last in the rotation, so the daily cron reaches it in about a week. The advertisement contract test caught the flag on the way in, which is what it is for: a path can join WEBMENTION_PATHS with one manifest flag, and accepting a mention you never advertise is the same as not accepting it. It knew how to ask /writing and nothing else, so it now holds a table of the worker-rendered pages and asks each through its real handler. A path in neither the table nor _headers still fails outright. derive:check re-locked shell/desktop, whose input is the manifest. * test(identity): pin the rel=me chain, which is why IndieAuth needed no code Scoping IndieAuth for the webmention.rocks receiver suite turned up that the sign-in already works. The flow, measured 2026-08-28: GET webmention.rocks/auth/start?url=https://aadhar.sh/ -> 302 indielogin.com/auth?me=https%3A%2F%2Faadhar.sh%2F -> 302 github.com/login/oauth/authorize indielogin went straight to GitHub with no provider chooser, which it can only do after reading a rel=me link off this homepage AND finding the matching link back on that GitHub profile. Both halves are there: the shell stamps rel="noopener me external" on all six profile anchors, and github.com/oddharsh carries href="https://aadhar.sh". That is RelMeAuth, not IndieAuth, and the distinction is worth keeping straight. This site advertises no authorization_endpoint and no rel=indieauth-metadata, and its .well-known/oauth-authorization-server is the agent-auth document with an anonymous grant, so a client doing real IndieAuth discovery still finds nothing. What it has is the older mechanism, which is the one indielogin uses, so the receiver suite is reachable today without building an endpoint that would assert domain control and carry a real security surface for it. Pinned because the half that lives here is invisible and fails quietly. rel= "noopener external" renders and behaves exactly like rel="noopener me external", so losing the token costs nothing a reader could see and takes the domain's identity with it. The test walks the committed desktop chrome, requires "me" as a whole rel token on every PROFILES anchor, and carries a floor so an emptied PROFILES cannot pass by checking nothing. Control run: dropping the token from the GitHub anchor alone fails it by name. The other half is on github.com and no checkout can assert it. The comment says so rather than implying the test covers both. * fix(webmention): the first real mention arrived with a blank author webmention.rocks receiver test 1, sent at production on 2026-08-28, verified and stored correctly and named nobody. The row: kind reply (correct: the source carries u-in-reply-to) author "" <- title Webmention Rocks! status pending Two bugs, both in the fallback chain, both visible in the source's markup: <div class="left p-author h-card"> <a href="/"><img class="u-photo" alt="Webmention Rocks!"></a> </div> ... <h1 class="p-name"><a href="/receive/1">Receiver Test #1</a></h1> The p-author pattern matched, then captured the WHITESPACE between that div's ">" and the "<a" on the next line. A whitespace string is truthy, so the `||` chain stopped there and the documented hostname floor never ran; clean() then reduced the winner to "". An h-card wrapping a linked photo is the ordinary shape on a site that marks its author up properly, so this was never a quirk of the test post: every reply from a real IndieWeb site would have arrived blank. And the second fallback was worse than no fallback. In microformats2 a p-name inside an h-card is the author's name, while a p-name inside an h-entry is the POST's name, and a pattern that cannot see which parent it sits under reads the title. On this very document that fallback calls the author "Receiver Test #1", which is confidently wrong where the hostname would merely have been plain. So the chain reads the p-author ELEMENT, bounded by counting its own tag's nesting rather than by a character window, because the entry title sits a few hundred characters past the h-card's end and a window swallows it. Inside that element: nested p-name, then text, then the photo's alt, which is what mf2 says carries the name for a p-* property. Every candidate has to survive clean() as a non-empty string to win, so the hostname floor is reachable again. Re-parsed against the saved source: author "Webmention Rocks!", and the id comes back byte-identical to the row production stored, so a re-send still updates that row rather than minting a second one. The fixture in the tests is that markup verbatim rather than paraphrased, since both bugs live in its exact shape. Control run: the old chain fails it with actual "". * fix(webmention): a relative source was accepted, and turned into a fetch at an invented host webmention.rocks receiver test 2 grades one thing: HTTP 400 for every invalid parameter. Running its contract against production on 2026-08-28, twenty of twenty-one shapes passed and one did not. 400 source == target, missing either, javascript:, ftp:, file:, localhost, 169.254.169.254, same-origin source, unparseable source, relative target, foreign target, 404 target, non-mentionable target 202 source=/some/path <- `validateLensTarget` prepends https:// to a schemeless string. That is right where it lives: the /lens box is a place a visitor TYPES "example.com" and means a website. An endpoint machines POST to is the opposite case, where the spec's `source` is a URL and guessing at a scheme accepts something that never was one. The lens guard is untouched; both surfaces are right about their own callers, and the difference is who is doing the typing. The mention then died in verification, so the only visible symptom was a conformance test failing. What the fetch was AIMED at is the part worth reading twice, and the first version of this fix got it wrong. WHATWG parsing is forgiving about the slashes after a scheme, so "https:///some/path" does not come back host-less the way it reads: it comes back with host `some` and path `/path`. So `/some/path` was not merely accepted, it was an instruction to fetch a hostname the sender never wrote. That is why absoluteHttpUrl checks the SHAPE before the parse. A parse-only version accepts `https:///no-host` and was caught by the suite's network tripwire firing at https://no-host/, which is the check earning its keep against a bug written while fixing the bug it is about. Nothing reached a private address either way: privateHostBlocked still runs, and a single-label host resolves to nothing. What it cost was a 202 the spec says must be a 400, and an outbound request with a host nobody chose. --------- Co-authored-by: Aadharsh Pannirselvam <19518661+oddharsh@users.noreply.github.com>
oddharsh
added a commit
that referenced
this pull request
Sep 22, 2026
zenc square oriented a full-frame copy (311 MB of f32 for a 26 MP photo) and then resampled it three times. It now hands the stored frame to halflight's new resample_oriented, which reads it upright inside the horizontal pass and is bitwise equal to orienting first. Pins halflight at 871487e (its resample-oriented branch, to be merged with a merge commit as #1 was). Same-file decode-to-resample floor, 6240x4160 JPEG, interleaved: orientation 1 491 ms (unchanged), 3 568 -> 491, 6 582 -> 445, 8 592 -> 461. Rotation now costs nothing, and the axis-swapping cases beat upright because each tap streams one whole stored row. zenc:bench against the tiled-orient branch: identical on all 24 outputs and 258 histograms; rotated sources 1.06-1.07x wall and 2.3-3.8% less CPU, each at p <= 0.0012; upright sources not called. images/histograms regenerated from this build (byte-identical) and re-locked, since Cargo.lock and pixels.rs are its inputs. Co-authored-by: Aadharsh Pannirselvam <19518661+oddharsh@users.noreply.github.com>
oddharsh
added a commit
that referenced
this pull request
Sep 25, 2026
) /garage/dyno rendered per request, and a cold KV miss held the whole document behind a fetch to raw.githubusercontent.com with a 3 s budget. It also shipped at the edge's q4 brotli with no dcz delta, no ETag, and the loose 'unsafe-inline' script policy, for a series that moves once a night. build.ts step 5b now bakes the shell, like /whoareyou (#924). The chart, callout and recent-pulls table arrive from /garage/dyno/pulls.html through lib/island.ts. The placeholder is the same renderer fed a 14-row model with no dates or numbers, so the swap adds no rows, and the layout-shift lab reads the page clean on desktop and phone. Unlike /whoareyou's values, the fragment is the same for every visitor, so it takes the JSON endpoint's 5-minute cache instead of no-store. Measured against the built Worker: shell 4,288 B at q11, 2,342 B as a family dcz delta. First visit is even with today (8,158 B across two requests against 8,060 B in one). A return visit revalidates the shell to a 304 and pays only the fragment, about 3.9 KB. Also: - Accept: text/markdown on /garage/dyno returned HTML, because the exact route never negotiated. It serves the hand twin now, and the route oracle has a row for it. - explorerDetails were passed as strings rather than {term, value}, so "N pulls recorded" never rendered. Dropped: a baked shell cannot know the count. dyno.ts goes from 3 type errors to 1. - link-integrity's route scan had matched `new Map([...])` since the table became ROUTE_TABLE on 2026-08-19, so its routeKeys set was empty. It uses invariant #1's pattern and a 60-key floor now. It surfaced because /garage/dyno.json sits under the governed /garage prefix and this is the first built page to link it. Co-authored-by: Aadharsh Pannirselvam <19518661+oddharsh@users.noreply.github.com>
oddharsh
added a commit
that referenced
this pull request
Oct 2, 2026
…1102) A route lived in five hand-kept lists: ROUTE_TABLE and PREFIX in index.ts, the 96-row run_worker_first allowlist, WORKERS_CACHEABLE_PATHS plus three prefixes hardcoded in lib/cache.ts, and the GET-write list in lib/preview.ts. build.ts and seven tests joined them by regex over index.ts, and that regex matched nothing twice, once for five weeks. src/worker/routes.ts is now the one list. It is data only: each route's path or label and pattern, the run_worker_first rows it claims (a fold names a wildcard and carries the reason it is safe), and whether it is cacheable or writes. cloudflare.config.ts, Workers Cache, the preview and early-data guards, build invariant #1 and the link resolver all derive from it by import. index.ts binds handlers in maps typed Record<ExactPath | PrefixLabel, RouteHandler>, so a route with no handler and a handler with no route are compile errors. It stays data because index.ts and cal's Worker cannot load outside workerd (gotcha 16). The seven island URLs moved into routes.ts too, and the feature modules import them. No behaviour change, measured against a baseline taken before the edit: run_worker_first set-equal (96 rows), 104 exact paths and 20 prefix labels in the same order, 28 cacheable paths, 6 GET-writes; the route oracle passes 325 rows with 0 hard failures; the suite passes 1148. Co-authored-by: Aadharsh Pannirselvam <19518661+oddharsh@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two days of work turning the homepage + subpages into a cohesive Windows-XP "desktop OS," plus a cleanup pass and a set of progressively-enhanced frontier web features. Everything degrades gracefully — the analogy the owner used is high-octane fuel: more capable when the platform supports it, still runs on regular when it doesn't.
All changes are deployed (Cloudflare Pages
holding/+ theserendipityWorker) and were verified in a real browser via preview before each commit.Desktop shell (nav.js)
bodyis a clipping flex desktop, each.window/.np-windowis pinned with its content scrolling internally behind a custom XP scrollbar; windows are draggable (top is a hard boundary) and resizable (bottom-right grip).:has()-gated so windowless pages keep normal scrolling./writingnotes open as stacking popovers (popover="manual") that composite over the folder index and cascade.Frontier features (octane + fallback)
Datefallback) for the taskbar clock, Notepad F5 stamp, image-EXIF dates, and serendipity relative times.appearance: base-selectLuna combobox,text-wrap: prettysite-wide.Fixes
htmlroot with the Bliss tone (the VT freezes the root group); fixed-size photo thumbnails; tooltips suppressed while scrolling; serendipity sidebar fills full height; writing-folder no longer ballooned by resize.Repo cleanup ("clean engine bay")
Ran a multi-agent audit and applied it: removed orphan code/CSS (
pinColor, deadpics/My-Pictures bits), rebuilt the sitemap (4 → 14 routes), refreshed llms.txt / index.md / README / CLAUDE.md (constants, photo counts 120→146, JPG+WebP→AVIF+JPG), and dropped stale/sourcereferences.Reviewer notes
Last-Modifiedheaders + perf timers were deliberately left asDate(Temporal can't emit RFC-1123; the rest aren't calendar dates).SW CACHE_VERSIONbumped across the range (nowv62);cf-garage/calnot touched.🤖 Generated with Claude Code