<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dhardingsea Developer</title>
    <description>The latest articles on DEV Community by Dhardingsea Developer (@dhseadev).</description>
    <link>https://dev.to/dhseadev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4061252%2F544eff56-3bdc-44e9-a8f5-9818e2a764a2.png</url>
      <title>DEV Community: Dhardingsea Developer</title>
      <link>https://dev.to/dhseadev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dhseadev"/>
    <language>en</language>
    <item>
      <title>Counting down to a date that does not exist</title>
      <dc:creator>Dhardingsea Developer</dc:creator>
      <pubDate>Wed, 19 Aug 2026 13:51:53 +0000</pubDate>
      <link>https://dev.to/dhseadev/counting-down-to-a-date-that-does-not-exist-378h</link>
      <guid>https://dev.to/dhseadev/counting-down-to-a-date-that-does-not-exist-378h</guid>
      <description>&lt;p&gt;I build small Chrome extensions under #DHSeaDev, and the newest one is a fan project: &lt;strong&gt;KH4 Companion&lt;/strong&gt;, an unofficial countdown to Kingdom Hearts IV — days-left toolbar badge, series anniversary milestones, news and trailer feeds, a lore compendium in original prose, and a three-lane rhythm minigame called Keyblade Strike. It's packaged at 0.2.0 and the Chrome Web Store listing is in preparation. Three design problems turned out to be interesting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The target is a window, not a date
&lt;/h2&gt;

&lt;p&gt;Kingdom Hearts IV has no release date. It has a window — Late 2027 — and a countdown app that pretends otherwise is lying with a number. So the countdown carries two modes on its face: window mode, which counts to a conservative end-of-window anchor and says so in its label, and date mode, which activates only when a real announced date is entered in settings. The storage listener flips the toolbar badge the moment the setting changes. The dishonest version of this feature is one line shorter, and I wrote it first.&lt;/p&gt;

&lt;h2&gt;
  
  
  A countdown you can unit test
&lt;/h2&gt;

&lt;p&gt;The countdown module never calls the clock. Time arrives as an argument — &lt;code&gt;computeRemaining(nowMs, target)&lt;/code&gt; — and the module has no DOM, no &lt;code&gt;chrome.*&lt;/code&gt;, and no fetch, so the same file runs in Node tests and in the extension unchanged. The service worker is relay-only: it holds no durable state, wakes on a six-hour alarm to re-render the badge text, and everything it knows it reads back out of storage. Milestones are the same shape — an annual month/day pair plus a pure next-occurrence function — so the anniversary list is testable across year boundaries without mocking anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feeds without a backend
&lt;/h2&gt;

&lt;p&gt;News and trailers come from public feeds — KHWiki, Reddit, Google News, YouTube — fetched directly from the popup with &lt;code&gt;credentials: 'omit'&lt;/code&gt;, so no cookie ever rides along. Responses cache in &lt;code&gt;chrome.storage.local&lt;/code&gt; for fifteen minutes, and every cache write goes through a per-key serialize queue, because two concurrent refreshes doing read-modify-write on the same key is exactly the storage race that has bitten this shelf of extensions before. When a feed is down, the panel serves the stale cache and says it's stale — which beats an empty panel every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  A fan project's honesty posture
&lt;/h2&gt;

&lt;p&gt;This is an extension about somebody else's story. So the project page carries a "what is ours, and what is not" section stating the line plainly: Kingdom Hearts belongs to Square Enix and Disney; series names appear in the extension as facts about the series; every line of compendium prose is original writing with a KHWiki source link; the art — in the extension and in the store assets — is original, with no game assets and no character likenesses anywhere. The store listing will say the same thing in the same words.&lt;/p&gt;

&lt;p&gt;The listing chip on the project page reads exactly what's true today: &lt;strong&gt;in preparation, not submitted&lt;/strong&gt;. When it clears review, the chip becomes a button.&lt;/p&gt;

&lt;p&gt;Project page (features + full privacy policy): &lt;a href="https://dhseadev.online/projects/kh4-companion/" rel="noopener noreferrer"&gt;https://dhseadev.online/projects/kh4-companion/&lt;/a&gt;&lt;br&gt;
The rest of the shelf: &lt;a href="https://dhseadev.online/projects/" rel="noopener noreferrer"&gt;https://dhseadev.online/projects/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>chrome</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I hand-built four things my tooling already shipped. The command I never ran was `catalog`.</title>
      <dc:creator>Dhardingsea Developer</dc:creator>
      <pubDate>Tue, 11 Aug 2026 13:25:55 +0000</pubDate>
      <link>https://dev.to/dhseadev/i-hand-built-four-things-my-tooling-already-shipped-the-command-i-never-ran-was-catalog-2obb</link>
      <guid>https://dev.to/dhseadev/i-hand-built-four-things-my-tooling-already-shipped-the-command-i-never-ran-was-catalog-2obb</guid>
      <description>&lt;p&gt;A build log about agent skills, HTML-to-MP4 video, and the specific cost of not reading the manual — with measured numbers."&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;The extension is &lt;a href="https://dhseadev.online/projects/dota-companion/" rel="noopener noreferrer"&gt;Dota Companion&lt;/a&gt; — a Dota 2&lt;br&gt;
stats popup that also prices your Steam inventory. I needed intro videos: a 30 and a 60 for&lt;br&gt;
YouTube, shorts for TikTok.&lt;/p&gt;

&lt;p&gt;The stack is &lt;a href="https://github.com/heygen-com/hyperframes" rel="noopener noreferrer"&gt;HyperFrames&lt;/a&gt; (Apache 2.0) — HTML and&lt;br&gt;
CSS in, MP4 out, no bundler. You write a composition as a plain HTML file, declare timing with&lt;br&gt;
&lt;code&gt;data-*&lt;/code&gt; attributes, register one paused GSAP timeline, and a headless Chrome + FFmpeg pipeline&lt;br&gt;
renders it. It's genuinely good for agent work because the artifact is a file an agent can read&lt;br&gt;
and edit, not a React tree it has to reason about.&lt;/p&gt;

&lt;p&gt;I also keep a personal "skill" file for it — a markdown doc the agent loads that carries facts&lt;br&gt;
about &lt;em&gt;my&lt;/em&gt; environment, as opposed to facts about the framework. The framework has its own docs.&lt;br&gt;
Mine holds things like "this install flag hangs in a non-TTY shell."&lt;/p&gt;
&lt;h2&gt;
  
  
  Round one was bad in an instructive way
&lt;/h2&gt;

&lt;p&gt;First pass: screenshots on a gradient, an animated outline ring around whatever the narration&lt;br&gt;
was naming, a caption bar, a slow zoom.&lt;/p&gt;

&lt;p&gt;Two problems, both visible in the first render:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The ring sat on the text.&lt;/strong&gt; At video scale a 3px border plus a glow lands on the glyphs of&lt;br&gt;
the thing it's pointing at. The highlight was actively making the subject harder to read.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It looked like a slideshow&lt;/strong&gt; because it &lt;em&gt;was&lt;/em&gt; one — a flat rectangle pasted on a gradient,&lt;br&gt;
with a Ken Burns zoom.&lt;/p&gt;
&lt;h2&gt;
  
  
  Round two: the fixes that actually worked
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Focus became a hole, not a ring.&lt;/strong&gt; Instead of drawing &lt;em&gt;on&lt;/em&gt; the subject, dim everything&lt;br&gt;
&lt;em&gt;except&lt;/em&gt; it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.spot&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c"&gt;/* sized to the measured target rect */&lt;/span&gt;
  &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;box-shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;4000px&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.58&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;outline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.5px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;126&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;198&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;232&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;outline-offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scrim is the shadow this box casts outward, clipped by the device frame. The lit region is&lt;br&gt;
negative space, so it is &lt;em&gt;geometrically incapable&lt;/em&gt; of covering the text it points at. This is&lt;br&gt;
the Apple/Samsung how-to grammar — the frame goes quiet, the subject stays lit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Coordinates got measured, not eyeballed.&lt;/strong&gt; The screenshot script records&lt;br&gt;
&lt;code&gt;getBoundingClientRect()&lt;/code&gt; for every control the video will point at, normalised to the image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;cx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;vp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;vp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;w&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;vp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;vp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reading coordinates off a rendered PNG by eye puts the highlight &lt;em&gt;next to&lt;/em&gt; the button, and&lt;br&gt;
nothing downstream can detect that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Depth became three planes.&lt;/strong&gt; Ambient gradient → the same screenshot blown up and blurred&lt;br&gt;
behind the device (colour spill) → the crisp shot in a window with a chrome bar and a real&lt;br&gt;
shadow. Light behaving like light is what reads as modern; a drop-shadow alone reads flat at&lt;br&gt;
video scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Seams got a rulebook.&lt;/strong&gt; One direction for the whole film, every cut landing mid-motion on&lt;br&gt;
both sides with mirrored &lt;code&gt;power4&lt;/code&gt; eases, and exactly one reserved "arrival" vector spent on&lt;br&gt;
the payoff beat.&lt;/p&gt;

&lt;p&gt;Before / after, same product, same screenshots:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Before: &lt;a href="https://www.youtube.com/shorts/6oBcZolFoDQ" rel="noopener noreferrer"&gt;https://www.youtube.com/shorts/6oBcZolFoDQ&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;After: &lt;a href="https://www.youtube.com/shorts/SQRhmxUIYlc" rel="noopener noreferrer"&gt;https://www.youtube.com/shorts/SQRhmxUIYlc&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The numbers that changed how I plan
&lt;/h2&gt;

&lt;p&gt;Three measurements did more for my throughput than any visual change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Render cost is dominated by one CSS property.&lt;/strong&gt; &lt;code&gt;check&lt;/code&gt; reports&lt;br&gt;
&lt;code&gt;browserGpuMode probe → software&lt;/code&gt; — no GPU, every frame rasterised on CPU:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Composition&lt;/th&gt;
&lt;th&gt;Wall-clock&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Flat planes, no full-frame filters&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;~3–4× realtime&lt;/strong&gt; (63.9s film → 3m17s)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One blurred backdrop plane + &lt;code&gt;backdrop-filter&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;~10–23× realtime&lt;/strong&gt; (64.2s film → 21m39s)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same film, same length, ~6× apart. A four-film set cost 43 minutes of wall-clock. That's not a&lt;br&gt;
detail, that's the shape of your afternoon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Script length is arithmetic.&lt;/strong&gt; Across 33 TTS segments — 338 words, 131.5 seconds — the rate&lt;br&gt;
was &lt;strong&gt;2.57 words/sec&lt;/strong&gt;. So a 60-second film is about 144 words. I wrote my first 60s script by&lt;br&gt;
feel; it measured 92 seconds. 53% over, and a full regeneration of every segment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three lint errors fire on every first build.&lt;/strong&gt; &lt;code&gt;gsap_exit_missing_hard_kill&lt;/code&gt; (a fade ending&lt;br&gt;
on a clip boundary needs a matching &lt;code&gt;tl.set&lt;/code&gt; or a seek restores stale visibility — it fired 7×),&lt;br&gt;
&lt;code&gt;duplicate_media_discovery_risk&lt;/code&gt; (two &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; sharing a &lt;code&gt;src&lt;/code&gt;; the documented failure mode is a&lt;br&gt;
&lt;em&gt;blank render&lt;/em&gt;), and &lt;code&gt;text_occluded&lt;/code&gt; (a caption inside a clip paints under any overlay declared&lt;br&gt;
after the clips — DOM order decides).&lt;/p&gt;

&lt;p&gt;And one no linter catches: &lt;strong&gt;exit opacity has to land exactly on the cut.&lt;/strong&gt; Fading at 55% of&lt;br&gt;
the exit travel left ~0.14s of empty stage between every scene — four black frames per seam,&lt;br&gt;
passing every automated check.&lt;/p&gt;

&lt;p&gt;I wrote all of this into the skill file. Felt good. Version 1.3.0.&lt;/p&gt;
&lt;h2&gt;
  
  
  Then I ran one command I'd never run
&lt;/h2&gt;

&lt;p&gt;Later, unrelated, someone asked whether I could use stock footage or animated characters. So I&lt;br&gt;
went looking at what the tooling could actually reach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;npx hyperframes catalog &lt;span class="nt"&gt;--json&lt;/span&gt;
358 items    &lt;span class="c"&gt;# 145 blocks, 213 components&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three hundred and fifty-eight prebuilt, installable motion components. &lt;code&gt;hyperframes add &amp;lt;name&amp;gt;&lt;/code&gt;&lt;br&gt;
drops one into your project as a local sub-composition with a paste-ready snippet.&lt;/p&gt;

&lt;p&gt;Here is what was in there:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What I hand-built&lt;/th&gt;
&lt;th&gt;What already existed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A GSAP proxy tween counting a number up&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;apple-money-count&lt;/code&gt; — counts up, flashes green, &lt;strong&gt;bursts money icons with sound&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSS device chrome with three fake traffic lights&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;device-frame-stage&lt;/code&gt;, or &lt;code&gt;macos-tahoe-liquid-glass&lt;/code&gt; — a real 3D laptop with a screen slot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A blurred backdrop plane for fake depth&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;camera-dolly-zoom&lt;/code&gt; with focal length solved per frame; &lt;code&gt;rack-focus&lt;/code&gt; with real aperture bokeh&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hand-timed emphasis on headlines&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;char-slam-explode&lt;/code&gt;, &lt;code&gt;caption-particle-burst&lt;/code&gt;, &lt;code&gt;confetti&lt;/code&gt;, &lt;code&gt;vfx-shatter&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Four for four. Better versions of all of them. Sitting behind a command I never ran because I&lt;br&gt;
went straight from "read the authoring docs" to "write the composition."&lt;/p&gt;

&lt;p&gt;The honest kicker: &lt;strong&gt;the before/after above was won entirely by hand.&lt;/strong&gt; Not one frame of it&lt;br&gt;
uses the registry. I found the registry &lt;em&gt;after&lt;/em&gt; those videos shipped. The visible improvement&lt;br&gt;
came from craft; the invisible waste came from not shopping first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually changed
&lt;/h2&gt;

&lt;p&gt;The skill file gained a section that now runs before any authoring:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Shop the registry BEFORE hand-authoring motion.&lt;/strong&gt; 358 items. &lt;code&gt;catalog --json&lt;/code&gt; first — the&lt;br&gt;
bare form times out per-item. Skipping this is the single largest waste measured in this&lt;br&gt;
skill's history.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Plus a routing table for the things I'd been guessing at: humanoid presenters route to an&lt;br&gt;
avatar-video path (which needs an interactive OAuth login — a &lt;em&gt;human&lt;/em&gt; action, not something an&lt;br&gt;
agent can complete headlessly); a 3D rigged character is flatly &lt;strong&gt;not viable&lt;/strong&gt; on a 2-core,&lt;br&gt;
no-GPU box when a CSS blur alone already costs 20× realtime; stock footage is legally clean&lt;br&gt;
from Pexels but works as &lt;em&gt;texture, not actors&lt;/em&gt;; and extracted game assets are a hard no.&lt;/p&gt;

&lt;p&gt;And I cut it down while adding that. The file went 264 → 236 lines, because a comparison table&lt;br&gt;
re-arguing a decision I'd already made, and a &lt;code&gt;## Requirements&lt;/code&gt; section holding two commands,&lt;br&gt;
were both bloat.&lt;/p&gt;

&lt;h2&gt;
  
  
  The transferable bit
&lt;/h2&gt;

&lt;p&gt;The lesson isn't "read the docs." I read the docs. I read the authoring model, the data&lt;br&gt;
attributes, the determinism rules — and then implemented, from those primitives, four&lt;br&gt;
components that shipped in the box.&lt;/p&gt;

&lt;p&gt;The docs told me &lt;strong&gt;how to build&lt;/strong&gt;. Nothing prompted me to ask &lt;strong&gt;what already exists&lt;/strong&gt;. Those&lt;br&gt;
are different questions and I only asked the first one.&lt;/p&gt;

&lt;p&gt;If you're pointing an agent at an unfamiliar framework, the highest-leverage line you can put&lt;br&gt;
in its instructions isn't a code sample. It's: &lt;em&gt;inventory the catalog before you write&lt;br&gt;
anything.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>showdev</category>
      <category>automation</category>
    </item>
    <item>
      <title>Weather that cannot touch the game: building an ambient layer that stays out of the way</title>
      <dc:creator>Dhardingsea Developer</dc:creator>
      <pubDate>Mon, 10 Aug 2026 14:50:43 +0000</pubDate>
      <link>https://dev.to/dhseadev/weather-that-cannot-touch-the-game-building-an-ambient-layer-that-stays-out-of-the-way-3cn7</link>
      <guid>https://dev.to/dhseadev/weather-that-cannot-touch-the-game-building-an-ambient-layer-that-stays-out-of-the-way-3cn7</guid>
      <description>&lt;p&gt;A 1D spring water surface, wind gusts drawn as one polyline, and an aurora faked with three additive strokes — plus the one comment in the file that explains a bug I would otherwise have shipped."&lt;br&gt;
tags: javascript, canvas, gamedev, webdev&lt;/p&gt;

&lt;p&gt;I built a small Chrome arcade game called Prism Cascade. You throw light-orbs at crystal prisms, the shards fall into a pool, and every splash throws up bubbles carrying points. Thirty-second rounds. The gameplay is deliberately tiny.&lt;/p&gt;

&lt;p&gt;The part I want to write about is the part that does nothing.&lt;/p&gt;

&lt;p&gt;Underneath the game there is a layer of wind, mist, aurora, drifting motes, fireflies and fish. It is the first thing anyone comments on and it has &lt;strong&gt;zero&lt;/strong&gt; effect on play. That was the design constraint, and it turned out to be the thing that made the whole layer cheap to build and impossible to break.&lt;/p&gt;

&lt;p&gt;Here is the entire declaration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ============================== Ambient world (visual only — no gameplay physics) ==============================&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fish&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;gusts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;mist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;foamBlobs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;fireflies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;motes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;spires&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;drops&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eight arrays. No classes, no entity system, no update graph. Let me go through the three that were actually interesting to write.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The water surface is a row of springs, and it wants to explode
&lt;/h2&gt;

&lt;p&gt;The pool is not a sine wave. It is a 1D array of columns, each with a height and a velocity, coupled to its neighbours — the classic Hugo Elias water model that has been floating around demoscene articles since about 2000.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;water&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="na"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;spacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;initWater&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;water&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;W&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;water&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spacing&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nx"&gt;water&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cols&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;water&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;disturbWater&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;power&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;waterIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;water&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;power&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;water&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="nx"&gt;water&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;power&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;water&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="nx"&gt;water&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;power&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each frame, every column gets a Hooke restoring force toward zero, gets damped, and then leaks energy into its neighbours over two passes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;stepWater&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.025&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;damp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.976&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;spread&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.14&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hMax&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;34&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vMax&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;water&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;water&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;col&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="nx"&gt;damp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;vMax&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vMax&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;vMax&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;vMax&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;dt&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;hMax&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hMax&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;hMax&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;col&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;hMax&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;pass&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;pass&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;water&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;             &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;spread&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;water&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;spread&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks fine and is not fine. Run it for a while with a lot of splashes and the surface develops a shimmering sawtooth — every column high, its neighbour low, alternating all the way across the pool. It does not decay, because the neighbour-coupling term is what is &lt;em&gt;feeding&lt;/em&gt; it: at that wavelength each column is always being pushed away from both of its neighbours simultaneously.&lt;/p&gt;

&lt;p&gt;That is the Nyquist mode. It is the highest frequency the grid can represent — one full wave every two columns — and an explicit integrator on a coupled grid will happily pump energy into it forever.&lt;/p&gt;

&lt;p&gt;The fix is three lines and a comment I left in on purpose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// spatial smoothing — kills the alternating-column (Nyquist) instability mode&lt;/span&gt;
  &lt;span class="c1"&gt;// while preserving long-wavelength waves&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;water&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;smoothed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;smoothed&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.55&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 1-2-1 kernel is a low-pass filter. At the Nyquist wavelength it has almost no gain, so the sawtooth is annihilated. At the long wavelengths you actually want — the roll of a wave crossing the pool after a big splash — it barely does anything. Blending at &lt;code&gt;0.55&lt;/code&gt; rather than replacing outright keeps a little crispness.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;prev&lt;/code&gt; variable matters more than it looks. Reading &lt;code&gt;c[i-1].h&lt;/code&gt; directly would read the &lt;em&gt;already-smoothed&lt;/em&gt; value from this same pass, which turns a symmetric filter into a directional one and makes waves drift left. Caching the pre-smoothing value is the difference between a filter and a bug.&lt;/p&gt;

&lt;p&gt;Two clamps are also doing quiet work. &lt;code&gt;vMax&lt;/code&gt; stops a single enormous splash from launching one column into orbit, and &lt;code&gt;hMax&lt;/code&gt; bounds the visual amplitude so the surface can never draw outside the pool. Neither is physical. Both mean the sim cannot produce a frame that looks broken, no matter what the game throws at it.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Everything couples through exactly one function
&lt;/h2&gt;

&lt;p&gt;Fish leaping, waterfall droplets landing, crystal debris hitting the pool — all of it reaches the water through &lt;code&gt;disturbWater(x, power)&lt;/code&gt; and nothing else.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// a fish landing&lt;/span&gt;
&lt;span class="nf"&gt;disturbWater&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// a waterfall droplet, only sometimes&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;G&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;waterY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;drops&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;disturbWater&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// debris from a shattered crystal&lt;/span&gt;
&lt;span class="nf"&gt;disturbWater&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;energy&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One integer of API surface. The ambient systems do not know about each other, do not know about the game, and cannot be broken by either. When I later changed how splash energy was calculated, the water sim needed no edits — it only ever sees a number.&lt;/p&gt;

&lt;p&gt;That &lt;code&gt;Math.random() &amp;lt; 0.12&lt;/code&gt; on the droplets is worth calling out. Ninety pixel-streaks a second all ringing the surface produces mush, not rain. Letting roughly one in eight actually strike gives you distinguishable individual rings and, incidentally, cuts the work by 88%. Sparse is both cheaper and better looking, which is not a trade-off you get very often.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Wind is one polyline, and the aurora is three strokes
&lt;/h2&gt;

&lt;p&gt;I did not want a particle system for wind. Particles for wind look like snow.&lt;/p&gt;

&lt;p&gt;A gust is a single object with a lifetime, and it is drawn as one 26-segment polyline whose vertical offset is a sine wave — with the amplitude tapering along the tail so it looks like it is trailing off rather than ending:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;segs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i2&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;segs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i2&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;px&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dir&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;i2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;py&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dir&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;i2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wl&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;age&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                 &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amp&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;i2&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;segs&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;i2&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;cx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;moveTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;px&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;py&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lineTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;px&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;py&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;cx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stroke&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// little curl at the head&lt;/span&gt;
&lt;span class="nx"&gt;cx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;beginPath&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;cx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wl&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;age&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;cx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stroke&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One stroke, one arc, per gust. A new gust arrives every 4 to 10 seconds, 70% of the time from the left, and self-culls when it leaves the screen. There are usually one or two on screen and often none — the gaps are what makes it read as weather rather than as an effect that is always on.&lt;/p&gt;

&lt;p&gt;The aurora is the same trick applied to glow. No shader, no blur filter, no offscreen canvas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;cx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;globalCompositeOperation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lighter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;rib&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;rib&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;rib&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;baseY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;H&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;rib&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.07&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hueA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rib&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;280&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;160&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;pass&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;pass&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;globalAlpha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reduceFx&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mf"&gt;0.03&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;cx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lineWidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;pass&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// ... two summed sines, sampled every 36px&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three passes: progressively wider, progressively fainter, drawn additively on top of each other. That is a bloom. It costs six strokes a frame sampled every 36 pixels, and it is convincing because real glow &lt;em&gt;is&lt;/em&gt; a stack of decreasing intensities at increasing radii — you do not need to compute it, you can just draw it that way.&lt;/p&gt;

&lt;p&gt;The ribbon shape is two summed sines at different frequencies and opposite time directions. One sine reads as a mechanical wobble. Two never quite repeat.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule that made all of it safe
&lt;/h2&gt;

&lt;p&gt;Every one of these populations is bounded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mist&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* spawn */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;drops&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* spawn */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;36 motes, 8 fireflies, 5 fish, 15 spires, 1400 particles hard cap. Not one of them grows with time, level, score or how long the tab has been open. The frame cost of the ambient layer at minute one is the frame cost at hour three, which is precisely why I never had to profile it.&lt;/p&gt;

&lt;p&gt;There is one deliberate asymmetry. The rock spires on the horizon are generated from a &lt;em&gt;seeded&lt;/em&gt; RNG:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;srng&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;L&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mulberry32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0xA17A&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;while everything else uses &lt;code&gt;Math.random&lt;/code&gt;. The skyline is the same every single session; the weather in front of it never repeats. That split is the whole feel of the thing — a fixed place, with something moving through it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where reduced-motion actually landed
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;prefers-reduced-motion&lt;/code&gt; maps to a &lt;code&gt;game.reduceFx&lt;/code&gt; flag, and I will be honest about how far it goes, because "we support reduced motion" is a claim people make loosely.&lt;/p&gt;

&lt;p&gt;It gates the things that move fast or arrive suddenly: wind gusts stop spawning entirely, waterfall droplets stop spawning, fish stop leaping, and the aurora drops from 0.05 to 0.03 alpha. It does &lt;strong&gt;not&lt;/strong&gt; gate the slow continuous drift — mist, motes and fireflies keep going.&lt;/p&gt;

&lt;p&gt;That is a judgement call, not an oversight. The vestibular-trigger guidance is about large, fast, unexpected motion, and a mote drifting four pixels a second is not that. But it is a judgement call, and if someone tells me it is the wrong one, the flag is already threaded through every spawn site — it is a one-line change per system.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Prism Cascade&lt;/strong&gt; is a 30-second physics arcade for Chrome. It makes zero network requests, asks for exactly one permission (&lt;code&gt;storage&lt;/code&gt;), and the Chrome Web Store listing is still in preparation at the time of writing.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Watch it in motion: &lt;a href="https://www.youtube.com/shorts/6oBcZolFoDQ" rel="noopener noreferrer"&gt;https://www.youtube.com/shorts/6oBcZolFoDQ&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Full write-up, mechanics and privacy policy: &lt;a href="https://dhseadev.online/projects/prism-cascade/" rel="noopener noreferrer"&gt;https://dhseadev.online/projects/prism-cascade/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;More things I have built: &lt;a href="https://dhseadev.online/projects/" rel="noopener noreferrer"&gt;https://dhseadev.online/projects/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Built solo — physics, rendering, sound and the level generator are all hand-written, no engine and no third-party libraries.&lt;/p&gt;

</description>
      <category>design</category>
      <category>frontend</category>
      <category>gamedev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>HTML in, MP4 out: rendering a video with HyperFrames instead of an editor</title>
      <dc:creator>Dhardingsea Developer</dc:creator>
      <pubDate>Sun, 09 Aug 2026 14:46:52 +0000</pubDate>
      <link>https://dev.to/dhseadev/html-in-mp4-out-rendering-a-video-with-hyperframes-instead-of-an-editor-koo</link>
      <guid>https://dev.to/dhseadev/html-in-mp4-out-rendering-a-video-with-hyperframes-instead-of-an-editor-koo</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://dhseadev.online/2026/08/09/html-in-mp4-out-hyperframes/" rel="noopener noreferrer"&gt;dhseadev.online&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I did not open a video editor to make this. I described a video, and the agent stack I run every day rendered one.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/-9otYLGrCsc"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;That is a 13-second vertical short about how many items in a Dota 2 inventory are actually sellable. Every frame of it came out of an HTML file.&lt;/p&gt;

&lt;h2&gt;
  
  
  What HyperFrames is
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/heygen-com/hyperframes" rel="noopener noreferrer"&gt;HyperFrames&lt;/a&gt; is HeyGen's open-source HTML-to-MP4 renderer. Apache 2.0, Node 22+, FFmpeg on PATH. Its README is basically three sentences: write HTML, render video, built for agents.&lt;/p&gt;

&lt;p&gt;The authoring model is the whole pitch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A composition is a plain HTML file. No bundler, no JSX, no build step.&lt;/li&gt;
&lt;li&gt;Timing and track assignment ride on data attributes.&lt;/li&gt;
&lt;li&gt;Motion comes from whatever seekable animation approach you already know — GSAP, native CSS animations, Lottie, Three.js.&lt;/li&gt;
&lt;li&gt;Preview runs in a browser. The final render is headless Chrome driving FFmpeg.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing in that list requires learning a framework, which is exactly why an agent can drive it. The artifact is a file it can read and edit directly, not a component tree it has to reason about through a compiler.&lt;/p&gt;

&lt;h3&gt;
  
  
  Versus Remotion
&lt;/h3&gt;

&lt;p&gt;Remotion is the obvious comparison and it is a good tool aimed at a different room.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;HyperFrames&lt;/th&gt;
&lt;th&gt;Remotion&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Authoring&lt;/td&gt;
&lt;td&gt;HTML + CSS + seekable animation libs&lt;/td&gt;
&lt;td&gt;React components&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build step&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Bundler required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;td&gt;Source-available; paid company license past a size/revenue threshold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent fit&lt;/td&gt;
&lt;td&gt;Plain files an agent reads and edits&lt;/td&gt;
&lt;td&gt;Needs React/JSX fluency&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If your codebase is already React, stay in React. If the thing holding the pen is an agent, the no-build option wins on the only axis that counts: how many layers sit between the instruction and the pixels.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three failures, in the order they cost me
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The installer hangs.&lt;/strong&gt; The plain install command opens an interactive multi-select picker. In a normal terminal you press space a few times and move on. In a non-TTY agent shell it never returns, and there is no error to read — the process just sits there. Pass the non-interactive flags and redirect stdin from &lt;code&gt;/dev/null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Chrome is not bundled.&lt;/strong&gt; The &lt;code&gt;doctor&lt;/code&gt; command tells you this immediately, which is the entire argument for running &lt;code&gt;doctor&lt;/code&gt; first. It is the cheapest possible way to learn what is missing, and it costs one line. Then fetch the headless shell before you try to render.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The CDN fails silently, and this is the real lesson.&lt;/strong&gt; The scaffold pulls GSAP from a CDN. A render container frequently has no outbound network &lt;em&gt;from Chrome&lt;/em&gt; even when the shell does — proxied shell, unproxied browser. The script never loads, the timeline registry is never populated, and the render produces a static or blank video &lt;strong&gt;with a passing exit code&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Not a warning. Not a non-zero status. A file that exists, plays, and is wrong.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; vendor/gsap.min.js https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js
&lt;span class="c"&gt;# then reference ./vendor/gsap.min.js from the composition&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same rule for fonts and images. Local files only, never hot-linked. I had "host media locally" written down as hygiene. It is not hygiene — it is a silent-failure class, and the difference between those two words is whether you find out before or after you publish.&lt;/p&gt;

&lt;h2&gt;
  
  
  Narration decides the cuts, not the other way around
&lt;/h2&gt;

&lt;p&gt;The instinct is to author the visuals and then lay voice over them. That desynchronises by the third frame, every time, because an estimated duration and a rendered duration are different numbers and the error compounds.&lt;/p&gt;

&lt;p&gt;So the order inverts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One audio file per sentence&lt;/strong&gt;, rendered separately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure, never estimate.&lt;/strong&gt; &lt;code&gt;ffprobe -v error -show_entries format=duration -of csv=p=0 seg.wav&lt;/code&gt;. Do not trust a reported length you did not measure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concat with a fixed gap.&lt;/strong&gt; Roughly 0.28–0.32s between segments reads as natural breathing. Less sounds rushed, more sounds like a dropout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Derive cuts cumulatively:&lt;/strong&gt; &lt;code&gt;start[i] = start[i-1] + dur[i-1] + gap&lt;/code&gt;. Those offsets become each clip's timing attribute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mux after render.&lt;/strong&gt; Render silent, then combine — and verify both streams survived, because a muxed file that lost its audio track still plays perfectly and you will not notice on a machine where you already know what it is supposed to say.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two runs, measured: eight segments produced 28.90s of video against 28.904s of audio. Nine segments produced 58.26s against 58.26s. That is what deriving from measurements buys you instead of eyeballing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that is not about video
&lt;/h2&gt;

&lt;p&gt;None of the above is hard. All of it is &lt;em&gt;forgettable&lt;/em&gt;, which is worse, because a forgotten step here does not fail loudly — it ships a blank video with a green exit code.&lt;/p&gt;

&lt;p&gt;So none of it lives in my head. It lives in a skill file: a short instruction document the model loads when the task matches. Mine is deliberately thin — it installs the upstream skill pack and defers to it rather than reimplementing the framework's own workflows badly. What it holds is only the stuff that cost a failed run: the non-interactive install flags, the doctor-first order, the vendoring rule, and the audio-first timing sequence.&lt;/p&gt;

&lt;p&gt;Sitting above that is a routing layer that picks which skills load for a task and which checks can block the result before any work starts. I wrote about &lt;a href="https://dhseadev.online/2026/08/02/claude-orchestrator-layer/" rel="noopener noreferrer"&gt;how that orchestrator layer works&lt;/a&gt; separately. One of those checks is the one that matters most here: it will not let me call a video finished because a command exited zero. Both streams present, or it is not done.&lt;/p&gt;

&lt;p&gt;The pattern generalises well past video. Every one of those files exists because something failed silently once and I did not want to pay for it twice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A skill is a receipt for a mistake.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The video itself
&lt;/h2&gt;

&lt;p&gt;The numbers in the short come from &lt;a href="https://dhseadev.online/projects/dota-companion/" rel="noopener noreferrer"&gt;Dota Companion&lt;/a&gt;, a Chrome extension I build that reads public Dota 2 match data through the free &lt;a href="https://www.opendota.com/" rel="noopener noreferrer"&gt;OpenDota API&lt;/a&gt; and turns it into things worth knowing about your own play. 833 items in an inventory, 183 of them sellable. It works as a short precisely because it is one number against another number, and that is the shape that survives a vertical crop.&lt;/p&gt;




&lt;p&gt;If you try HyperFrames: run the doctor first, vendor everything, generate the audio before you write a single timing value, and verify the output file rather than the exit code. Four rules, and every one of them exists because I broke it first.&lt;/p&gt;

</description>
      <category>video</category>
      <category>ffmpeg</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I asked an agent to make a product video. It wrote HTML and rendered an MP4 - Dota Companion</title>
      <dc:creator>Dhardingsea Developer</dc:creator>
      <pubDate>Sat, 08 Aug 2026 22:24:54 +0000</pubDate>
      <link>https://dev.to/dhseadev/i-asked-an-agent-to-make-a-product-video-it-wrote-html-and-rendered-an-mp4-dota-companion-1nia</link>
      <guid>https://dev.to/dhseadev/i-asked-an-agent-to-make-a-product-video-it-wrote-html-and-rendered-an-mp4-dota-companion-1nia</guid>
      <description>&lt;p&gt;I needed two videos for a Chrome extension I built: a promo and a walkthrough. I did not want to open a video editor.&lt;/p&gt;

&lt;p&gt;So I used &lt;a href="https://github.com/heygen-com/hyperframes" rel="noopener noreferrer"&gt;HyperFrames&lt;/a&gt; — HeyGen's open-source (Apache 2.0) HTML-to-MP4 framework. Compositions are plain HTML and CSS. No JSX, no bundler, no build step. Timing lives in data attributes; motion comes from GSAP. A headless Chrome seeks the timeline frame by frame and FFmpeg encodes it.&lt;/p&gt;

&lt;p&gt;That design matters for one specific reason: &lt;strong&gt;an agent can read and edit a plain HTML file.&lt;/strong&gt; It cannot meaningfully iterate on a timeline in a GUI.&lt;/p&gt;

&lt;p&gt;Both videos below were authored as HTML, narrated with a local TTS model, and rendered locally. Here's what came out, then the three things that actually cost me time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The promo (29s)
&lt;/h2&gt;


&lt;div&gt;
    &lt;iframe src="https://www.youtube.com/embed/T7uIzXHCgSA"&gt;
    &lt;/iframe&gt;
  &lt;/div&gt;


&lt;h2&gt;
  
  
  The walkthrough (58s)
&lt;/h2&gt;


&lt;div&gt;
    &lt;iframe src="https://www.youtube.com/embed/Mh6L9-FScow"&gt;
    &lt;/iframe&gt;
  &lt;/div&gt;


&lt;p&gt;Numbers, for calibration: 1080×1080, 867 frames for the promo, 56 seconds to render. The walkthrough took just over 2 minutes. Both passed the framework's &lt;code&gt;check&lt;/code&gt; gate — 0 runtime errors, 0 layout issues, and 22/22 and 23/23 WCAG AA contrast checks respectively.&lt;/p&gt;




&lt;h2&gt;
  
  
  Failure 1: the CDN script that fails silently
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;init&lt;/code&gt; scaffold pulls GSAP from a CDN:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My render container proxied the shell but not the browser. &lt;code&gt;curl&lt;/code&gt; worked. Chrome had no network.&lt;/p&gt;

&lt;p&gt;So GSAP never loaded, &lt;code&gt;window.__timelines&lt;/code&gt; was never registered, and the render would have produced a static video — &lt;strong&gt;with a zero exit code and no error&lt;/strong&gt;. Nothing in the output says "your animation library is missing."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; vendor/gsap.min.js https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vendor everything. Fonts too. This is the failure mode I'd most expect someone else to hit, because it doesn't announce itself — you just get a video where nothing moves, and you assume you wrote the timeline wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure 2: timing the cuts before generating the audio
&lt;/h2&gt;

&lt;p&gt;My first instinct was to write the storyboard, assign each frame a duration, then generate narration to fit. That's backwards. Speech doesn't land where you guess it will, and the drift compounds — by the third frame the voice is talking over the wrong slide.&lt;/p&gt;

&lt;p&gt;Invert it. Generate audio first, one file per sentence, and read the real duration of each:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx hyperframes tts seg/s1.txt &lt;span class="nt"&gt;--voice&lt;/span&gt; am_michael &lt;span class="nt"&gt;-o&lt;/span&gt; seg/s1.wav &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;span class="c"&gt;# → {"durationSeconds": 4.48, ...}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then derive every cut point cumulatively — &lt;code&gt;start[i] = start[i-1] + dur[i-1] + gap&lt;/code&gt; — and let those offsets &lt;em&gt;become&lt;/em&gt; the &lt;code&gt;data-start&lt;/code&gt; values. A 0.3s inter-segment gap reads as natural breathing.&lt;/p&gt;

&lt;p&gt;Result: a 28.90s video against 28.904s of audio, and 58.26s against 58.26s. No drift, no nudging.&lt;/p&gt;

&lt;p&gt;Render silent, mux at the end, and verify both streams actually exist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; video.mp4 &lt;span class="nt"&gt;-i&lt;/span&gt; vo.wav &lt;span class="nt"&gt;-c&lt;/span&gt;:v copy &lt;span class="nt"&gt;-c&lt;/span&gt;:a aac &lt;span class="nt"&gt;-shortest&lt;/span&gt; out.mp4
ffprobe &lt;span class="nt"&gt;-v&lt;/span&gt; error &lt;span class="nt"&gt;-show_entries&lt;/span&gt; &lt;span class="nv"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;codec_type out.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last check matters — a mux that silently drops the audio stream still produces a file that plays.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure 3: real screenshots when the browser has no network
&lt;/h2&gt;

&lt;p&gt;The framework is strict that screenshot slots hold &lt;em&gt;real captures&lt;/em&gt;, not mockups. Correct rule. But my capture environment couldn't reach the API, so loading the extension gave me an empty UI.&lt;/p&gt;

&lt;p&gt;The wrong fix is to mock the interface. The right one is to fetch the real payload out-of-band and seed it through the app's &lt;strong&gt;own&lt;/strong&gt; cache path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;refreshOnOpen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;     &lt;span class="c1"&gt;// the app's documented cache-only path&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cache:123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// shape the app itself writes&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="nx"&gt;realData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reload&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real data, real render code, real pixels. Two conditions make this legitimate and both are required: the data is genuinely fetched, and the cache shape is one the application actually writes. Invent a shape the app never produces and you're fabricating a screenshot.&lt;/p&gt;

&lt;p&gt;One caveat I'd underline: &lt;strong&gt;inspect every offline capture.&lt;/strong&gt; Mine painted the full dashboard but left one field empty. Ship that to a store listing and it reads as a broken product.&lt;/p&gt;

&lt;h2&gt;
  
  
  An honest note on the content
&lt;/h2&gt;

&lt;p&gt;The first cut of the promo showed my real stats — including a &lt;code&gt;0&lt;/code&gt; improvement streak and a loss streak — directly under narration about tracking improvement.&lt;/p&gt;

&lt;p&gt;I swapped the shot rather than faking the numbers. Worth saying out loud, because the tempting move when your generator can produce any frame you describe is to describe a better one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Would I do it again
&lt;/h2&gt;

&lt;p&gt;For this job, yes. The whole pipeline is text: HTML, CSS, a script file, and CLI verbs. Everything is diffable, re-renderable, and re-narratable by changing a sentence and re-running. Editing a word of narration is editing a text file, not re-recording.&lt;/p&gt;

&lt;p&gt;It is not a replacement for a real editor when you want craft. It's very good at "generate this deterministically and regenerate it when the product changes" — which is most product video.&lt;/p&gt;

&lt;p&gt;The extension in the videos is &lt;a href="https://chromewebstore.google.com/detail/dota-companion/gnnamhmenhhlgjmanoofngmfeddmfbej" rel="noopener noreferrer"&gt;Dota Companion&lt;/a&gt; — free, local-first Dota 2 stats. More of my projects at &lt;a href="https://dhseadev.online/" rel="noopener noreferrer"&gt;dhseadev.online&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>video</category>
      <category>showdev</category>
    </item>
    <item>
      <title>WordPress Ate My &amp;&amp; — Making a WordPress.com Blog Multiplayer with playhtml</title>
      <dc:creator>Dhardingsea Developer</dc:creator>
      <pubDate>Sat, 08 Aug 2026 20:34:26 +0000</pubDate>
      <link>https://dev.to/dhseadev/wordpress-ate-my-making-a-wordpresscom-blog-multiplayer-with-playhtml-13p0</link>
      <guid>https://dev.to/dhseadev/wordpress-ate-my-making-a-wordpresscom-blog-multiplayer-with-playhtml-13p0</guid>
      <description>&lt;p&gt;My site, &lt;a href="https://dhseadev.online/" rel="noopener noreferrer"&gt;dhseadev.online&lt;/a&gt;, runs on WordPress.com. Managed hosting, block theme, no server code of my own. It is the least likely candidate for real-time multiplayer you can imagine.&lt;/p&gt;

&lt;p&gt;It now has a &lt;a href="https://dhseadev.online/play/" rel="noopener noreferrer"&gt;shared toy room&lt;/a&gt; where visitors drag glyph magnets around together, a &lt;a href="https://dhseadev.online/positivity-wall/" rel="noopener noreferrer"&gt;positivity wall&lt;/a&gt; where strangers leave stamps that persist for everyone, an &lt;a href="https://dhseadev.online/prism-break/" rel="noopener noreferrer"&gt;arcade page&lt;/a&gt; with a live chat, and — as of this week — a "read receipt" card at the bottom of every blog post, where you can stamp that you read it and see how many other humans did too.&lt;/p&gt;

&lt;p&gt;Zero backend. Zero plugins I wrote. One open-source library, one CDN &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;, and a short list of WordPress-specific traps that each failed &lt;em&gt;silently&lt;/em&gt; — which is the part worth writing down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The library: playhtml
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/spencerc99/playhtml" rel="noopener noreferrer"&gt;playhtml&lt;/a&gt; (by Spencer Chang) gives you collaborative HTML elements with a single data attribute. Put &lt;code&gt;can-move&lt;/code&gt; on a div and every visitor can drag it — and everyone sees everyone else's drags, live, with the position persisted. &lt;code&gt;can-spin&lt;/code&gt;, &lt;code&gt;can-grow&lt;/code&gt;, &lt;code&gt;can-duplicate&lt;/code&gt; do what they say. &lt;code&gt;can-play&lt;/code&gt; is the escape hatch: you define &lt;code&gt;defaultData&lt;/code&gt;, an event handler that calls &lt;code&gt;setData&lt;/code&gt;, and an &lt;code&gt;updateElement&lt;/code&gt; renderer, and playhtml syncs the state object across every open browser via &lt;a href="https://www.partykit.io/" rel="noopener noreferrer"&gt;PartyKit&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The mental model is: &lt;strong&gt;a tiny shared JSON document per element, keyed by page URL&lt;/strong&gt;. No auth, no database, no deploy.&lt;/p&gt;

&lt;p&gt;That per-URL keying sounds like a limitation. It turned out to be the whole design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rooms are per-URL, and that's a superpower
&lt;/h2&gt;

&lt;p&gt;playhtml scopes shared state to the page it lives on. Move an element to a different URL and it gets a fresh, empty room — the old state is orphaned, not migrated. You have to respect that when restructuring pages.&lt;/p&gt;

&lt;p&gt;But flip it around: &lt;strong&gt;one script, injected sitewide, becomes a per-page feature automatically.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My read-receipts card is a single block in the site footer template. It checks for WordPress's &lt;code&gt;single-post&lt;/code&gt; body class, and if present, injects a stamp card at the end of the article. Because rooms are per-URL, every post — past and future — gets its &lt;em&gt;own&lt;/em&gt; independent stamp count with zero per-post edits. Publishing a new post wires it up by existing.&lt;/p&gt;

&lt;p&gt;The stamp logic is ~40 lines: &lt;code&gt;defaultData: {stamps: []}&lt;/code&gt;, a click pushes &lt;code&gt;{s: seed, t: timestamp}&lt;/code&gt;, a per-browser seed in &lt;code&gt;localStorage&lt;/code&gt; deduplicates so you can only stamp once, a cap of 96 drops the oldest. I clicked the button, reloaded, and the count survived — the state had round-tripped through PartyKit and back. That reload test is the only proof that matters; everything before it is theater.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 1: WordPress silently eats &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This is the one that cost me the most and the reason this article has its title.&lt;/p&gt;

&lt;p&gt;WordPress stores raw block content, then runs it through rendering filters on output. Somewhere in that pipeline, bare &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; sequences inside a Custom HTML block can come out the other side as &lt;code&gt;&amp;amp;#038;&amp;amp;#038;&lt;/code&gt;. Your JavaScript now contains HTML entities. It fails at parse.&lt;/p&gt;

&lt;p&gt;And here's the killer: &lt;strong&gt;no console error you'd associate with your code&lt;/strong&gt;. The script just... isn't there in any working form. You wrote it, the editor shows it, the save succeeded, and the page behaves as if the feature never existed.&lt;/p&gt;

&lt;p&gt;The diagnosis that finally worked: fetch the &lt;em&gt;raw&lt;/em&gt; stored content via the REST API (&lt;code&gt;?context=edit&lt;/code&gt;), fetch the &lt;em&gt;rendered&lt;/em&gt; page, and diff the lengths. A rendered version that's longer than raw is a smell — then grep the rendered bytes for &lt;code&gt;&amp;amp;#038;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The fix is to write JavaScript with no &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// instead of: if (a &amp;amp;&amp;amp; a.b) { ... }&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// or collapse with a fallback:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// if you truly need a literal ampersand in a string:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;amp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromCharCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;38&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ridiculous? Yes. But it's a hard rule on this host, and once you know it, it's cheap to follow. My share buttons build X and Reddit intent links as &lt;em&gt;single-query-param&lt;/em&gt; URLs (everything packed into one &lt;code&gt;text=&lt;/code&gt; param) specifically so no &lt;code&gt;&amp;amp;&lt;/code&gt; ever appears in the script.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 2: &lt;code&gt;&amp;lt;/script&amp;gt;&lt;/code&gt; inside a string
&lt;/h2&gt;

&lt;p&gt;If your injected script writes a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag (say, lazy-loading playhtml itself), the closing tag inside a string literal terminates your &lt;em&gt;outer&lt;/em&gt; script at parse time. Classic, but it combines badly with trap 1's silence. Always write it split: &lt;code&gt;'&amp;lt;' + '/script&amp;gt;'&lt;/code&gt; or &lt;code&gt;&amp;lt;\/script&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 3: emoji are stripped on save
&lt;/h2&gt;

&lt;p&gt;Astral-plane emoji get stripped from Custom HTML blocks when saved over REST — as raw characters, as HTML entities, &lt;em&gt;and&lt;/em&gt; even when inserted at runtime by JavaScript that survived the save. If a label matters, use text or an inline SVG shape. I stopped fighting this one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 4: &lt;code&gt;transform&lt;/code&gt; breaks &lt;code&gt;position: fixed&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;My pages break out of the theme's 620px content column with the standard full-bleed recipe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.page-wrap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100vw&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;translateX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;-50%&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;transform&lt;/code&gt; makes the wrapper a &lt;strong&gt;containing block&lt;/strong&gt; — any &lt;code&gt;position: fixed&lt;/code&gt; descendant is now fixed relative to the page wrapper, not the viewport. A fixed overlay, toast, or ambient canvas &lt;em&gt;cannot work&lt;/em&gt; from inside page content styled this way.&lt;/p&gt;

&lt;p&gt;The escape: put viewport-fixed things in the theme's &lt;strong&gt;footer template part&lt;/strong&gt;, whose ancestor chain has no transform. That architectural accident is what pushed all my sitewide interactive code into the footer — which is also what made the per-URL room trick in the read-receipts section fall out naturally. Sometimes the constraint designs the system for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 5: duplicate SVG ids across pasted blocks
&lt;/h2&gt;

&lt;p&gt;Paste the same inline SVG (with internal &lt;code&gt;&amp;lt;linearGradient id="pg"&amp;gt;&lt;/code&gt;) into a page twelve times and you have twelve elements sharing one id. Every &lt;code&gt;url(#pg)&lt;/code&gt; reference resolves to the &lt;em&gt;first&lt;/em&gt; definition; the other eleven gradients are silently dead. My toy room had exactly this. Prefix every internal id per SVG block, and before renaming, assert that your defs and refs actually pair up — mine had 12 defs and 115 refs, so a naive sequential rename would have mis-paired them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The common thread
&lt;/h2&gt;

&lt;p&gt;Every one of these failures is &lt;strong&gt;silent&lt;/strong&gt;. No error, no warning, a successful save — and a feature that doesn't exist. The discipline that catches them isn't cleverness, it's verification: fetch the served bytes after every write and check them against what you meant to ship. Rendered-vs-raw diffs, entity scans, element counts, a reload test on anything stateful. If I hadn't made that a habit, at least three of these would have shipped as dead code and I'd have blamed the library.&lt;/p&gt;

&lt;h2&gt;
  
  
  Was it worth it?
&lt;/h2&gt;

&lt;p&gt;A blog is usually a monologue. The cheapest thing multiplayer buys you isn't a feature — it's &lt;em&gt;evidence of other people&lt;/em&gt;. Someone dragged the magnets into a shape. Someone stamped a post at 2am. The &lt;a href="https://dhseadev.online/positivity-wall/" rel="noopener noreferrer"&gt;positivity wall&lt;/a&gt; has stamps from browsers I'll never identify. For one &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag and some entity-encoding scar tissue, the site feels inhabited.&lt;/p&gt;

&lt;p&gt;If you want to poke at any of it live: the &lt;a href="https://dhseadev.online/play/" rel="noopener noreferrer"&gt;toy room&lt;/a&gt;, the &lt;a href="https://dhseadev.online/prism-break/" rel="noopener noreferrer"&gt;arcade&lt;/a&gt;, or any post on &lt;a href="https://dhseadev.online/" rel="noopener noreferrer"&gt;dhseadev.online&lt;/a&gt; — scroll to the bottom and leave a stamp. I'll see the count go up, which is the whole point.&lt;/p&gt;

</description>
      <category>html</category>
      <category>wordpress</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>Shipping an LLM chat widget with 481 bytes of CMS footprint - Wordpress</title>
      <dc:creator>Dhardingsea Developer</dc:creator>
      <pubDate>Fri, 07 Aug 2026 13:00:26 +0000</pubDate>
      <link>https://dev.to/dhseadev/shipping-an-llm-chat-widget-with-481-bytes-of-cms-footprint-wordpress-3752</link>
      <guid>https://dev.to/dhseadev/shipping-an-llm-chat-widget-with-481-bytes-of-cms-footprint-wordpress-3752</guid>
      <description>&lt;p&gt;A chat widget where the CMS holds a loader and nothing else — plus the four failure modes that cost me the most time: buffered SSE, base64 truncation that passes a syntax check, render-time escaping, and a balanced-but-wrong tag tree.&lt;/p&gt;

&lt;p&gt;Canonical_url&lt;br&gt;
&lt;a href="https://dhseadev.online/2026/08/06/ai-answers-desk-val-town-groq/" rel="noopener noreferrer"&gt;https://dhseadev.online/2026/08/06/ai-answers-desk-val-town-groq/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I maintain a small site where I write up the internal tooling I build — process-serving&lt;br&gt;
automation, Chrome extensions, desktop utilities. It had a static answers page. The&lt;br&gt;
obvious next step was letting people ask the things that page doesn't cover.&lt;/p&gt;

&lt;p&gt;Two constraints shaped the whole design:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;No server for me to maintain.&lt;/li&gt;
&lt;li&gt;The API key never reaches a browser, and never enters the CMS database.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What follows is the architecture I landed on, and — more usefully — the four things that&lt;br&gt;
actually cost me time. Three of those four are general enough to bite you on a completely&lt;br&gt;
different stack.&lt;/p&gt;
&lt;h2&gt;
  
  
  The shape
&lt;/h2&gt;

&lt;p&gt;The CMS side is &lt;strong&gt;481 bytes&lt;/strong&gt;. One block, and all it does is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DHSEA_ASK_ENDPOINT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://…&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// then inject &amp;lt;script src="…/widget.js"&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No plugin. No key in the database. No logic in the page.&lt;/p&gt;

&lt;p&gt;Everything else — the ~19KB widget, the styles, the chat logic, the proxy — lives in a&lt;br&gt;
single serverless file with three routes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Route&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /health&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Diagnostics. Written so it can never 500.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /widget.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Serves the widget itself.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST /&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Chat proxy to the model API.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The API key is an env var on the serverless platform. Because the widget is &lt;em&gt;served from&lt;/em&gt;&lt;br&gt;
the same function rather than pasted into the CMS, shipping a widget change is one&lt;br&gt;
external deploy and I never touch the site.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/health&lt;/code&gt; deserves a note. A health check that can throw is worse than no health check,&lt;br&gt;
because it fails in exactly the situation you're using it to diagnose. Mine returns&lt;br&gt;
&lt;code&gt;{ok, keyConfigured, store, storeError, fatal, day, dayReq, dayTokens, …}&lt;/code&gt; — errors&lt;br&gt;
become &lt;em&gt;fields&lt;/em&gt;, not exceptions.&lt;/p&gt;
&lt;h3&gt;
  
  
  Budget, because it's my card
&lt;/h3&gt;

&lt;p&gt;Public endpoint plus paid inference is a bad combination unless you bound it up front:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;per-IP: 4/min, 30/day&lt;/li&gt;
&lt;li&gt;global: 12/min, 1500/day&lt;/li&gt;
&lt;li&gt;hard ceiling: 300k tokens/day&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Counters live in the platform's SQLite so they survive cold starts — in-memory counters&lt;br&gt;
on a serverless host reset when the instance recycles, which means no rate limit at all&lt;br&gt;
under exactly the traffic pattern you care about.&lt;/p&gt;

&lt;p&gt;On top: an origin allowlist. My domain gets 200. A &lt;strong&gt;lookalike&lt;/strong&gt; domain gets 403. A&lt;br&gt;
request with no &lt;code&gt;Origin&lt;/code&gt; header gets 403. Test all three; the lookalike case is the one&lt;br&gt;
naive &lt;code&gt;startsWith&lt;/code&gt; checks fail.&lt;/p&gt;
&lt;h2&gt;
  
  
  Four things that cost me the most time
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Your platform may buffer SSE
&lt;/h3&gt;

&lt;p&gt;The model API streams tokens properly. My serverless host buffers, so what reached the&lt;br&gt;
browser was roughly two chunks — not a stream.&lt;/p&gt;

&lt;p&gt;Total round trip is under a second, so it reads fine. But I'd already started building a&lt;br&gt;
token-by-token typewriter UI that was never going to work on that host.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify streaming end to end on your actual deployment target&lt;/strong&gt;, not on the vendor's&lt;br&gt;
docs page. The vendor streams. That tells you nothing about what your host does with it.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. A truncated base64 payload passes a syntax check
&lt;/h3&gt;

&lt;p&gt;The widget is base64-embedded into the serverless file by a build script. If a copy step&lt;br&gt;
truncates the blob:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the file still parses — &lt;code&gt;node --check&lt;/code&gt; is &lt;strong&gt;happy&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;atob&lt;/code&gt; fails at runtime&lt;/li&gt;
&lt;li&gt;the panel renders blank&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A syntax check attests the &lt;em&gt;file&lt;/em&gt;, never the &lt;em&gt;payload&lt;/em&gt;. So: never hand-copy the payload&lt;br&gt;
(a build script does it file-to-file), and verify the deploy against the live artifact:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://…/widget.js | shasum &lt;span class="nt"&gt;-a&lt;/span&gt; 256
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…compared against the same hash computed on the local build. &lt;strong&gt;Same byte length is not&lt;br&gt;
the same bytes.&lt;/strong&gt; Compare hashes. Mine matched at 19,260 chars — that match is the&lt;br&gt;
evidence the deploy worked, not the fact that the paste didn't throw.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Verify the served bytes, not the saved content
&lt;/h3&gt;

&lt;p&gt;My CMS applies escaping at render time. So the stored content was byte-clean while the&lt;br&gt;
live page was dead and the console was empty.&lt;/p&gt;

&lt;p&gt;Reading back what you just saved proves the &lt;em&gt;write&lt;/em&gt; succeeded. It proves nothing about&lt;br&gt;
what the visitor receives. Fetch the rendered output and assert on that.&lt;/p&gt;

&lt;p&gt;This generalizes past CMSs: anywhere a transform sits between your write and the user —&lt;br&gt;
a bundler, a minifier, a template engine, a CDN — the artifact you validated is not the&lt;br&gt;
artifact they get.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. Balanced tags are not a correct tree
&lt;/h3&gt;

&lt;p&gt;This is my favorite, because every mechanical check I had passed it.&lt;/p&gt;

&lt;p&gt;A page with opening and closing &lt;code&gt;div&lt;/code&gt; counts matching &lt;strong&gt;exactly&lt;/strong&gt; — 14 and 14. And six&lt;br&gt;
sections nested inside a seventh, because one closing tag had been deferred to the end of&lt;br&gt;
the document.&lt;/p&gt;

&lt;p&gt;Balance counting cannot catch this. Absence and misplacement are different bugs, and&lt;br&gt;
counting only detects absence. Parse it instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DOMParser&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;parseFromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.qa .qa&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 6 → nested. Should be 0.&lt;/span&gt;
&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.in &amp;gt; .qa&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// should equal total .qa&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That assertion is the one that catches it, and it's the one I now run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The through-line
&lt;/h2&gt;

&lt;p&gt;Every one of these four is the same mistake wearing a different hat: &lt;strong&gt;I checked the&lt;br&gt;
thing I could reach instead of the thing the user gets.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The saved content instead of the served page. The file's syntax instead of the payload's&lt;br&gt;
integrity. The vendor's streaming behavior instead of my host's. The tag count instead&lt;br&gt;
of the tree.&lt;/p&gt;

&lt;p&gt;Each check passed. Each one attested something adjacent to what I actually needed to know.&lt;br&gt;
Whatever sits between your code and your user — a renderer, a host, an encoder, a build&lt;br&gt;
step — is the boundary your verification has to cross, or it isn't verification.&lt;/p&gt;




&lt;p&gt;The result is live at &lt;a href="https://dhseadev.online/ask/" rel="noopener noreferrer"&gt;dhseadev.online/ask&lt;/a&gt;, sitting next&lt;br&gt;
to the &lt;a href="https://dhseadev.online/answers/" rel="noopener noreferrer"&gt;static answers&lt;/a&gt; it was built to extend. If you&lt;br&gt;
break it, I'd like to hear how.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>serverless</category>
      <category>ai</category>
      <category>wordpress</category>
    </item>
    <item>
      <title>One I can't finish, one I can't measure</title>
      <dc:creator>Dhardingsea Developer</dc:creator>
      <pubDate>Thu, 06 Aug 2026 14:42:57 +0000</pubDate>
      <link>https://dev.to/dhseadev/one-i-cant-finish-one-i-cant-measure-18hk</link>
      <guid>https://dev.to/dhseadev/one-i-cant-finish-one-i-cant-measure-18hk</guid>
      <description>&lt;p&gt;I shipped two things this week that could not be less alike.&lt;/p&gt;

&lt;p&gt;One is an integration against the SaaS that most process-serving firms run their dispatch on — a system of legal record, where a wrong write becomes a wrong document attached to a real court case. The other is a 24.92 KiB Chrome extension containing a dinosaur that walks across your page and refuses to leave.&lt;/p&gt;

&lt;p&gt;They have exactly one thing in common, and it took me until I'd written both up to notice it: &lt;strong&gt;each one has a hole in it that I put there on purpose.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One I can't finish. One I can't measure.&lt;/p&gt;




&lt;h2&gt;
  
  
  The one I can't finish
&lt;/h2&gt;

&lt;p&gt;ServeManager speaks &lt;a href="https://jsonapi.org/" rel="noopener noreferrer"&gt;JSON:API&lt;/a&gt;, not flat REST. If you've never hit that shape before, the first thing it does is lie to you quietly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Returns undefined. Does not throw. Ships to production as a blank column.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;service_status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// The scalar actually lives here&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;service_status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Relationships are worse — they're pointers, not objects, and the thing they point at lives in a separate top-level &lt;code&gt;included&lt;/code&gt; array you have to resolve yourself. Getting a court name off a job is three hops:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;included&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;included&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;courtCase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;included&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;relationships&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;court_case&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;court&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;included&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;courtCase&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;relationships&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;court&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;courtName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;court&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// null-guard every hop&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every one of those failure modes is silent. Nothing throws. You get &lt;code&gt;undefined&lt;/code&gt;, it flows through your mapper, and it lands in a report as an empty cell — in a domain where an empty cell means "we have no idea whether this person was legally served."&lt;/p&gt;

&lt;p&gt;So here's the hole. There is one write I have never confirmed: attaching a file to an &lt;strong&gt;already-existing&lt;/strong&gt; job. The documented flow covers uploading during job &lt;em&gt;creation&lt;/em&gt;. The attach-afterward body schema, I have never captured from a real request.&lt;/p&gt;

&lt;p&gt;I could infer it. The create flow is right there, the naming is consistent, and I'd probably be right.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Probably right" is a fine standard for a side project and a terrible one for a system of legal record.&lt;/strong&gt; A malformed write here doesn't throw an exception — it produces a wrong document on someone's real case, and nobody finds out until the day it matters. So the feature is gated. It does not run against production. It sits there, unfinished, until I spend the hour with a network tab open and capture what the request actually looks like.&lt;/p&gt;

&lt;p&gt;The full writeup, with the status codes that look like your bug and aren't (406, 409, and a 403 that's a permissions level rather than a bad payload), is here: &lt;strong&gt;&lt;a href="https://dhseadev.online/2026/08/06/servemanager-api-document-not-record/" rel="noopener noreferrer"&gt;The ServeManager API is a document, not a record&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The one I can't measure
&lt;/h2&gt;

&lt;p&gt;Same week. Different universe.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dhseadev.online/projects/annoying-dino/" rel="noopener noreferrer"&gt;Annoying Dino&lt;/a&gt; is a Manifest V3 extension that puts a tiny T-Rex in your browser. You can tap him, feed him, or ignore him. His mood shifts based on which you pick. There's an egg-and-hatch collection running alongside. That's the product. It is the least ambitious thing I have ever shipped.&lt;/p&gt;

&lt;p&gt;It is also, by a factor of nearly five, &lt;strong&gt;the most-installed thing in my portfolio.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I didn't want to write that sentence without checking it, so I counted every listing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Extension&lt;/th&gt;
&lt;th&gt;Installs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Annoying Dino&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;19&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Arcanum Idle&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dino Dots&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Planet Express Lounge&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cobalt Star / Kawaii Time Garden / Spellsurge / Hot Dog Rush&lt;/td&gt;
&lt;td&gt;2 each&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Devotion&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Four listings wouldn't surface a count at all, so the honest version is "leads everything I could measure." Nineteen users is not a business or a validation — it's nineteen browsers. But it beats the idle game with nine automated test suites, and that gap is the interesting part.&lt;/p&gt;

&lt;p&gt;Here's the hole: &lt;strong&gt;I have no idea whether any of those nineteen people have opened it since.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There is no analytics in any of my extensions. No event pings, no install beacon, no heartbeat. I check that the way I check a bug — grep the whole codebase for network APIs before the privacy claims get written. Which means I cannot tell you a retention curve, a DAU number, or whether the feeding mechanic is the part people actually like. I can't even tell you if it's broken on someone's machine right now.&lt;/p&gt;

&lt;p&gt;Anyone with an analytics tag would have known all of that on day two.&lt;/p&gt;

&lt;p&gt;And I still won't add one — because of what this specific extension is. It runs on the pages you visit. Any usage telemetry worth having from it would be, functionally, a log of when you were browsing and what you were looking at when a cartoon reptile walked past. There is no version of "just a little event tracking" there that isn't a surveillance surface wearing a joke costume.&lt;/p&gt;

&lt;p&gt;Full numbers and reasoning: &lt;strong&gt;&lt;a href="https://dhseadev.online/2026/08/06/annoying-dino-most-installed/" rel="noopener noreferrer"&gt;The most-installed thing I have built is a dinosaur that bothers you&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The thing they actually share
&lt;/h2&gt;

&lt;p&gt;I thought these were opposites. A legal-record API and a joke dinosaur; maximum consequence and zero consequence.&lt;/p&gt;

&lt;p&gt;They're the same decision made twice.&lt;/p&gt;

&lt;p&gt;In both cases there was a shortcut available that would have made the product &lt;em&gt;better on paper&lt;/em&gt;. Infer the field shape and ship the attach feature. Add three lines of telemetry and finally know if anyone plays the thing. Both shortcuts are small, normal, and what most people would do without a second thought.&lt;/p&gt;

&lt;p&gt;And in both cases the cost of taking the shortcut lands on &lt;strong&gt;someone who is not me&lt;/strong&gt;. A guessed field shape puts a wrong document on a stranger's court case. A telemetry ping puts a stranger's browsing pattern on a server. The benefit is all mine — a shipped feature, a retention chart — and the risk is all theirs.&lt;/p&gt;

&lt;p&gt;So the rule I've ended up with is not "be careful." It's narrower than that:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When the shortcut's upside accrues to you and its downside accrues to your user, the shortcut isn't a tradeoff. It's a transfer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The follow-on is that you have to be willing to hold the gap. Ship the integration with the feature switched off and say so. Ship the extension and admit publicly you can't measure it. Both look like weakness in a portfolio. Both are the actual engineering.&lt;/p&gt;

&lt;p&gt;The unglamorous coda: neither hole is permanent. The attach schema needs one hour with DevTools open. The dino telemetry is never coming — but the install count is a real signal, and it already told me something I didn't want to hear about the gap between what I find interesting to build and what a stranger will actually click.&lt;/p&gt;




&lt;p&gt;If you want the longer versions, I keep the case studies at &lt;strong&gt;&lt;a href="https://dhseadev.online/writing/" rel="noopener noreferrer"&gt;dhseadev.online/writing&lt;/a&gt;&lt;/strong&gt; and the running devlog at &lt;strong&gt;&lt;a href="https://dhseadev.online/updates/" rel="noopener noreferrer"&gt;dhseadev.online/updates&lt;/a&gt;&lt;/strong&gt;. Everything I ship is local-first, no accounts, no telemetry — which, as established, is occasionally very inconvenient for me.&lt;/p&gt;

&lt;p&gt;What's the shortcut you refuse to take, and what does it cost you?&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Every key failed in exactly the same way</title>
      <dc:creator>Dhardingsea Developer</dc:creator>
      <pubDate>Wed, 05 Aug 2026 01:16:35 +0000</pubDate>
      <link>https://dev.to/dhseadev/every-key-failed-in-exactly-the-same-way-21mi</link>
      <guid>https://dev.to/dhseadev/every-key-failed-in-exactly-the-same-way-21mi</guid>
      <description>&lt;h2&gt;
  
  
  Cloudflare error 1010 rejects Python's default User-Agent before the API's auth layer ever sees the request. How to tell it from a real 401, and the four headers that fix it."
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://dhseadev.online/2026/08/04/every-key-failed-the-same-way/" rel="noopener noreferrer"&gt;dhseadev.online&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I lost an hour to a credential that was never wrong.&lt;/p&gt;

&lt;p&gt;The task was small: a read-only &lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt; server over a vendor REST API, so an assistant could read job records without me pasting spreadsheet exports into a chat window. The vendor authenticates with the customer's own API key over HTTP Basic. Two lines of &lt;code&gt;urllib&lt;/code&gt;. Done before lunch.&lt;/p&gt;

&lt;p&gt;Every request came back the same way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;403&lt;/span&gt; &lt;span class="ne"&gt;Forbidden&lt;/span&gt;

error code: 1010
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Cloudflare error 1010 is not an authentication failure.&lt;/strong&gt; It is a browser-signature ban issued at Cloudflare's edge — &lt;a href="https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/" rel="noopener noreferrer"&gt;"the owner of this website has banned your access based on your browser's signature"&lt;/a&gt; — and Python's default &lt;code&gt;User-Agent&lt;/code&gt; is enough to trigger it.&lt;/p&gt;

&lt;p&gt;The request never reaches the API's own auth layer. That is why every credential you try fails identically. The fix is four request headers, not a new key.&lt;/p&gt;

&lt;p&gt;If that solves your afternoon, you can stop reading. The rest is the part I found more useful: how you can tell, from the failures alone, that you are not talking to the thing you think you are talking to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a correct key still returns 403
&lt;/h2&gt;

&lt;p&gt;I did what everyone does. Key as the username with an empty password. Key as a Bearer token. Key in an &lt;code&gt;X-API-Key&lt;/code&gt; header. Re-checked the base64 padding. Regenerated the key. Tried again.&lt;/p&gt;

&lt;p&gt;Six variants. Every one returned 403 and &lt;code&gt;error code: 1010&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is the shape of a permissions problem. It is where you start drafting an email to support asking which scope your key is missing, and where the hour goes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tell is that the failures were identical
&lt;/h2&gt;

&lt;p&gt;An authentication layer discriminates. That is the entire job description.&lt;/p&gt;

&lt;p&gt;A malformed header should not fail the same way as a well-formed header carrying a revoked key, which should not fail the same way as a valid key hitting an endpoint it cannot see. Those are three different conditions and a competent auth layer says three different things about them.&lt;/p&gt;

&lt;p&gt;When six materially different requests produce byte-identical responses, nothing is reading those bytes. The answer is coming from something standing in front of the thing you are trying to talk to.&lt;/p&gt;

&lt;p&gt;That generalises well past Cloudflare, and it is the part worth keeping: &lt;strong&gt;identical failure output across varied input means the input is not being examined.&lt;/strong&gt; Vary something that should matter. If the error does not move, you are debugging the wrong layer.&lt;/p&gt;

&lt;p&gt;In this case the thing in front was Cloudflare's browser-integrity check. Python's standard library announces itself as &lt;code&gt;User-Agent: Python-urllib/3.x&lt;/code&gt;, and the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/User-Agent" rel="noopener noreferrer"&gt;User-Agent header&lt;/a&gt; is the cheapest fingerprint an edge can filter on. That string alone got every request I sent discarded before the vendor's servers ever saw it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Four request headers. No proxy, no scraping framework, no third-party HTTP client.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;HEADERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Mozilla/5.0 (Windows NT 10.0; Win64; x64) &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AppleWebKit/537.36 (KHTML, like Gecko) &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Chrome/126.0.0.0 Safari/537.36&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Accept&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json, text/plain, */*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Accept-Language&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en-US,en;q=0.9&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Accept-Encoding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gzip, deflate, br&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I proved it side by side with a key I made up on purpose:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Headers&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Python default&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;403&lt;/code&gt; · &lt;code&gt;error code: 1010&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser set above&lt;/td&gt;
&lt;td&gt;&lt;code&gt;401 HTTP Basic: Access denied.&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A 401 was the win condition. A correct rejection of a deliberately fake credential meant the request had finally reached the application.&lt;/p&gt;

&lt;p&gt;The honest caveat: this defeats User-Agent fingerprinting and nothing else. If the edge escalates to TLS or JA3 fingerprinting, &lt;code&gt;urllib&lt;/code&gt; stops working again and the answer becomes a browser-impersonating client. I have not tested that, because it has not happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trick I took away: 401 is evidence you can collect for free
&lt;/h2&gt;

&lt;p&gt;The useful consequence showed up later. I had written tools against seven endpoint paths. Five of them I had never successfully called — I had them from documentation, which is a claim, not a measurement.&lt;/p&gt;

&lt;p&gt;So I called all seven with the correct headers and &lt;strong&gt;no credential at all&lt;/strong&gt;: account, jobs, companies, court cases, courts, employees, invoices.&lt;/p&gt;

&lt;p&gt;Every one returned 401. Not one returned 404.&lt;/p&gt;

&lt;p&gt;Those two codes answer different questions. &lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-15.5.5" rel="noopener noreferrer"&gt;RFC 9110 §15.5.5&lt;/a&gt; defines 404 as the origin server finding no current representation for the target resource — the route does not exist. &lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html#section-15.5.2" rel="noopener noreferrer"&gt;§15.5.2&lt;/a&gt; defines 401 as a request lacking valid authentication credentials — the route exists and is refusing you.&lt;/p&gt;

&lt;p&gt;That distinction confirmed all seven paths, using zero credentials and touching zero customer data. It is the cheapest verification step I know of, and I had never deliberately reached for it before. You can run it against an API before you have a key, before you have written a client, before you have permission to do anything at all.&lt;/p&gt;

&lt;p&gt;It does not confirm everything. Proving a door exists is not the same as knowing what is behind it — the field mappings behind five of those tools are still inferred rather than captured, and they are labelled that way in the README until someone runs them against a live key.&lt;/p&gt;

&lt;h2&gt;
  
  
  One unrelated landmine, since it will cost somebody an afternoon
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;mcp&lt;/code&gt; package on PyPI is at 2.0.0, and &lt;code&gt;FastMCP&lt;/code&gt; is gone from it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# stale — every tutorial I could find still says this
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mcp.server.fastmcp&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastMCP&lt;/span&gt;

&lt;span class="c1"&gt;# current
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mcp.server.mcpserver&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MCPServer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;.tool()&lt;/code&gt; decorator and &lt;code&gt;.run(transport="stdio")&lt;/code&gt; are unchanged, so the migration is one line. Checked against the SDK rather than against the tutorials, on 4 August 2026 — if you are reading this much later, check it again.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually took from it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Identical failures are a signal, not noise.&lt;/strong&gt; If varying the input does not vary the output, the input is not being read.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A written environment fact is still a hypothesis.&lt;/strong&gt; My own handoff notes said neither sandbox could reach that host at all. That was wrong. The network route was always fine and the original blocker had only ever been a missing key — I nearly designed around a constraint that did not exist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ask the question that costs nothing first.&lt;/strong&gt; The unauthenticated probe took ninety seconds and settled something I had planned to settle with a credential I did not yet have.&lt;/p&gt;




&lt;p&gt;The server shipped read-only: ten tools, 28 unit tests, no mutating operations. Anything that writes back into a system of record stays behind a confirmation gate, which is its own problem and its own post.&lt;/p&gt;

&lt;p&gt;I write these up as I go at &lt;strong&gt;&lt;a href="https://dhseadev.online/" rel="noopener noreferrer"&gt;dhseadev.online&lt;/a&gt;&lt;/strong&gt;. If this was your kind of thing, the nearest neighbours are &lt;a href="https://dhseadev.online/2026/08/02/claude-orchestrator-layer/" rel="noopener noreferrer"&gt;the orchestrator layer that routes this work&lt;/a&gt;, &lt;a href="https://dhseadev.online/projects/serveboard/" rel="noopener noreferrer"&gt;ServeBoard&lt;/a&gt; — the same data seen from the other end, where &lt;a href="https://dhseadev.online/2026/07/25/what-counts-as-one-job/" rel="noopener noreferrer"&gt;deciding what counts as one job&lt;/a&gt; turned out to be the hard part — and &lt;a href="https://dhseadev.online/engineering-profile/" rel="noopener noreferrer"&gt;a longer piece on how I work&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>api</category>
      <category>debugging</category>
    </item>
    <item>
      <title>I built a local-first Dota 2 companion as a Chrome extension — and learned what "minimum permissions" actually means</title>
      <dc:creator>Dhardingsea Developer</dc:creator>
      <pubDate>Tue, 04 Aug 2026 10:45:03 +0000</pubDate>
      <link>https://dev.to/dhseadev/i-built-a-local-first-dota-2-companion-as-a-chrome-extension-and-learned-what-minimum-4f7e</link>
      <guid>https://dev.to/dhseadev/i-built-a-local-first-dota-2-companion-as-a-chrome-extension-and-learned-what-minimum-4f7e</guid>
      <description>&lt;p&gt;Building a no-account, no-API-key Dota 2 stats extension, and three Chrome/Steam lessons that cost me a Web Store rejection.&lt;br&gt;
&lt;a href="https://dhseadev.online/projects/dota-companion/" rel="noopener noreferrer"&gt;https://dhseadev.online/projects/dota-companion/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most Dota 2 stat sites answer "how did that game go?" I wanted one that answers a harder question: &lt;strong&gt;am I actually getting better?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://chromewebstore.google.com/detail/dota-companion/gnnamhmenhhlgjmanoofngmfeddmfbej" rel="noopener noreferrer"&gt;Dota Companion&lt;/a&gt; — a Chrome extension that lives in your toolbar. No account, no API key, no signup. Paste a Steam profile URL and it works.&lt;/p&gt;

&lt;p&gt;The build taught me three things worth passing on, one of which got the extension flagged by the Chrome Web Store.&lt;/p&gt;
&lt;h2&gt;
  
  
  The feature I actually cared about: improvement streak
&lt;/h2&gt;

&lt;p&gt;Win streaks measure luck as much as skill. In a five-player team game, you can play well and lose.&lt;/p&gt;

&lt;p&gt;So the headline metric isn't wins — it's an &lt;strong&gt;improvement streak&lt;/strong&gt;: consecutive games where your GPM beat &lt;em&gt;your own&lt;/em&gt; 20-game baseline. It's a metric you control, and it moves when you improve rather than when your team cooperates.&lt;/p&gt;

&lt;p&gt;Same idea drives goals. Pick GPM, KDA, last hits, or win rate; the baseline snapshots your current 20-game average when you set the goal. You compete with past-you.&lt;/p&gt;

&lt;p&gt;The rest is the between-games layer: today's W/L, a GPM trend sparkline, recent matches with hero, K/D/A, duration and how long ago, most-played heroes, and private notes and tags on players you run into — stored on your device, never uploaded.&lt;/p&gt;
&lt;h2&gt;
  
  
  Lesson 1: &lt;code&gt;chrome.tabs.create()&lt;/code&gt; does not need the &lt;code&gt;tabs&lt;/code&gt; permission
&lt;/h2&gt;

&lt;p&gt;This one cost me a rejection email.&lt;/p&gt;

&lt;p&gt;I had one tabs call — opening a bundled inventory page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tabs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;inventory.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So &lt;code&gt;"tabs"&lt;/code&gt; went in the manifest. Reasonable, right?&lt;/p&gt;

&lt;p&gt;Wrong. Google flagged it under their minimum-permissions policy:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The following permission(s) need not be requested for the methods/properties implemented by the item: &lt;code&gt;tabs&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;chrome.tabs.create()&lt;/code&gt; has never required the &lt;code&gt;tabs&lt;/code&gt; permission.&lt;/strong&gt; No tab-creation API does. That permission gates exactly one thing: reading &lt;em&gt;sensitive tab metadata&lt;/em&gt; — &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;pendingUrl&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;favIconUrl&lt;/code&gt; — off &lt;code&gt;Tab&lt;/code&gt; objects.&lt;/p&gt;

&lt;p&gt;I passed a URL in and ignored the returned object. I was declaring a capability I never used.&lt;/p&gt;

&lt;p&gt;I verified the fix rather than trusting the docs — loaded the extension with &lt;code&gt;tabs&lt;/code&gt; removed and called the API in a real browser:&lt;/p&gt;

&lt;p&gt;runtime granted permissions: {"permissions":["storage"],"origins":["&lt;a href="https://api.opendota.com/*%22%5D" rel="noopener noreferrer"&gt;https://api.opendota.com/*"]&lt;/a&gt;}&lt;br&gt;
chrome.tabs.create -&amp;gt; {"ok":true,"tabId":210614909}&lt;br&gt;
pages before/after: 2 / 3&lt;br&gt;
errors: NONE&lt;/p&gt;

&lt;p&gt;Tab opened. Zero errors. The permission was pure liability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; grep your actual call sites before declaring anything. "This API sounds tab-related, so I need &lt;code&gt;tabs&lt;/code&gt;" is exactly the reasoning the policy exists to catch. Ask instead: &lt;em&gt;do I read the returned object's sensitive fields?&lt;/em&gt; If no, you don't need it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Lesson 2: rate limits you share with the user are a design constraint, not an error case
&lt;/h2&gt;

&lt;p&gt;The extension can estimate your Dota 2 inventory value from Steam Community Market prices. Steam rate-limits those endpoints hard — and critically, &lt;strong&gt;the limit applies to the whole IP, not to your extension.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Blow through it and you don't just break your feature. You break the user's normal Steam browsing. That reframes the problem: you're not optimizing your own throughput, you're spending a budget that isn't yours.&lt;/p&gt;

&lt;p&gt;So the design is deliberately timid:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nothing fetches automatically.&lt;/strong&gt; Every request traces back to a button the user pressed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A visible request budget&lt;/strong&gt;, capped well short of the real limit, shown in the UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hard backoff.&lt;/strong&gt; If Steam pushes back, it stops for hours rather than retrying.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Steam login cookies&lt;/strong&gt; are sent. Public data only.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Slower than technically possible, on purpose. A tool that degrades the rest of your browsing isn't a good tool.&lt;/p&gt;
&lt;h2&gt;
  
  
  Lesson 3: optional permissions are worth the extra state
&lt;/h2&gt;

&lt;p&gt;The inventory feature needs &lt;code&gt;steamcommunity.com&lt;/code&gt; access. Most users never open it.&lt;/p&gt;

&lt;p&gt;Requesting that host at install time means every user sees a scary permission prompt for a feature they may never touch. So it's an &lt;strong&gt;optional host permission&lt;/strong&gt;, requested at the moment of use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;origins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ORIGIN&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// gate the UI&lt;/span&gt;
&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;origins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ORIGIN&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// only on click&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cost: a real permission-state machine — gated UI, a grant screen, a declined path. Benefit: the install prompt asks for nothing surprising, and the request arrives with obvious context.&lt;/p&gt;

&lt;p&gt;Worth it. Install-time friction is where extensions die.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing logic that lives in a popup
&lt;/h2&gt;

&lt;p&gt;Extension code resists testing — everything wants &lt;code&gt;chrome.*&lt;/code&gt; or the DOM. The fix was a hard boundary: all pure logic (streaks, baselines, formatting, aggregation) sits in modules that touch &lt;strong&gt;nothing&lt;/strong&gt; — no &lt;code&gt;chrome.*&lt;/code&gt;, no DOM, no &lt;code&gt;fetch&lt;/code&gt;, not even the clock. Current time is passed in as an argument.&lt;/p&gt;

&lt;p&gt;Those modules dual-export: a global for the extension, &lt;code&gt;module.exports&lt;/code&gt; for Node. The whole stat layer runs headless with zero browser mocks.&lt;/p&gt;

&lt;p&gt;That caught a genuinely nasty one. OpenDota returns &lt;code&gt;start_time&lt;/code&gt; in &lt;strong&gt;epoch seconds&lt;/strong&gt;. &lt;code&gt;new Date(start_time)&lt;/code&gt; expects &lt;strong&gt;milliseconds&lt;/strong&gt; — so every match silently renders as 1970. There's now a regression test asserting exactly that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chrome Web Store:&lt;/strong&gt; &lt;a href="https://chromewebstore.google.com/detail/dota-companion/gnnamhmenhhlgjmanoofngmfeddmfbej" rel="noopener noreferrer"&gt;Dota Companion&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project page:&lt;/strong&gt; &lt;a href="https://dhseadev.online/projects/dota-companion/" rel="noopener noreferrer"&gt;dhseadev.online/projects/dota-companion&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Data comes from the &lt;a href="https://www.opendota.com/" rel="noopener noreferrer"&gt;OpenDota API&lt;/a&gt; — you'll need "Expose Public Match Data" enabled in Dota 2 settings for matches to show up. English and Simplified Chinese.&lt;/p&gt;

&lt;p&gt;Not affiliated with Valve.&lt;/p&gt;

</description>
      <category>chrome</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>Twelve gradients, one id - DHSeaDev</title>
      <dc:creator>Dhardingsea Developer</dc:creator>
      <pubDate>Mon, 03 Aug 2026 22:51:32 +0000</pubDate>
      <link>https://dev.to/dhseadev/twelve-gradients-one-id-dhseadev-3j5p</link>
      <guid>https://dev.to/dhseadev/twelve-gradients-one-id-dhseadev-3j5p</guid>
      <description>&lt;p&gt;A markup defect with no visual symptom, the audit that missed it five times, and the guard that stopped the fix from being worse than the bug.&lt;/p&gt;

&lt;p&gt;The page in question: &lt;a href="https://dhseadev.online/play/" rel="noopener noreferrer"&gt;dhseadev.online/play&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;MARKUP · A DEFECT WITH NO SYMPTOM&lt;/strong&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Twelve gradients, one id
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Eleven of them were dead. The page looked exactly right, and that was the problem.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;A site scan flagged nothing. My own audit reported zero defects across forty-six pages.&lt;br&gt;
The page rendered precisely as designed. &lt;em&gt;Eleven of its twelve gradients had never been&lt;br&gt;
used by anything.&lt;/em&gt;&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;THE DEFECT&lt;/strong&gt; ————————————&lt;/p&gt;
&lt;h2&gt;
  
  
  What twelve copies of one emblem actually produces
&lt;/h2&gt;

&lt;p&gt;I had an SVG emblem I liked, so I pasted it into a page twelve times. Each copy brought&lt;br&gt;
its own gradient definition along with it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 40 40"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;defs&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;linearGradient&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"pg"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;stop&lt;/span&gt; &lt;span class="na"&gt;stop-color=&lt;/span&gt;&lt;span class="s"&gt;"#C4B5FD"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;stop&lt;/span&gt; &lt;span class="na"&gt;stop-color=&lt;/span&gt;&lt;span class="s"&gt;"#93C5FD"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;stop&lt;/span&gt; &lt;span class="na"&gt;stop-color=&lt;/span&gt;&lt;span class="s"&gt;"#F0ABFC"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/linearGradient&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/defs&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"url(#pg)"&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Twelve &lt;code&gt;&amp;lt;svg&amp;gt;&lt;/code&gt; blocks. Twelve elements carrying &lt;code&gt;id="pg"&lt;/code&gt;. Duplicate ids are invalid&lt;br&gt;
HTML, and &lt;code&gt;url(#pg)&lt;/code&gt; resolves to the first match in the document — so every emblem on&lt;br&gt;
the page was painted by gradient number one, and the remaining eleven were parsed, held&lt;br&gt;
in memory, and referenced by nothing.&lt;/p&gt;

&lt;p&gt;The reason it stayed invisible is almost insulting: all twelve definitions held&lt;br&gt;
identical stops. There was no visual symptom because there was nothing to see. A bug&lt;br&gt;
like this waits patiently for the day you change one gradient and the change lands on&lt;br&gt;
the wrong element, or on none of them.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;THE AUDIT&lt;/strong&gt; ————————————&lt;/p&gt;
&lt;h2&gt;
  
  
  Why five passes of my own tooling missed it
&lt;/h2&gt;

&lt;p&gt;My checks looked for missing &lt;code&gt;alt&lt;/code&gt; attributes, broken links, absent structured data.&lt;br&gt;
"Are any ids duplicated" was not among them, because it had never yet cost me anything.&lt;/p&gt;

&lt;p&gt;It costs four lines to add.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[id]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;THE FIX&lt;/strong&gt; ————————————&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap sitting inside the repair
&lt;/h2&gt;

&lt;p&gt;The obvious remedy is a unique id per gradient. My first attempt renamed sequentially&lt;br&gt;
across the whole document, and I put a count assertion in front of it out of habit&lt;br&gt;
rather than suspicion.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/id="pg"/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`id="pg&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/url&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="sr"&gt;#pg&lt;/span&gt;&lt;span class="se"&gt;\)&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`url(#pg&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The assertion failed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;defs = 12 · refs = 115&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Each emblem defined one gradient and referenced it roughly ten times. A sequential&lt;br&gt;
rename would have paired the second definition with the second reference — which&lt;br&gt;
belonged to the first emblem. It would have converted an invisible defect into a&lt;br&gt;
visibly broken page, and it would have done so while reporting success.&lt;/p&gt;

&lt;p&gt;The correct repair scopes the rename to each &lt;code&gt;&amp;lt;svg&amp;gt;&lt;/code&gt; block, so a definition can only&lt;br&gt;
ever be paired with references from its own subtree.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;lt;svg&lt;/span&gt;&lt;span class="se"&gt;[\s\S]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;&amp;lt;&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;svg&amp;gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/id="pg"/g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`pg&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;block&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id="pg"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`id="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;url(#pg)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`url(#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Twelve blocks renamed. Zero leftover definitions, zero leftover references, twelve&lt;br&gt;
distinct ids, and no duplicate id anywhere on the page.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;WHAT IT LEAVES BEHIND&lt;/strong&gt; ————————————&lt;/p&gt;

&lt;p&gt;Inline SVG pasted more than once duplicates every internal id it carries — not only&lt;br&gt;
gradients, but filters, masks, clipPaths, patterns, and every &lt;code&gt;aria-labelledby&lt;/code&gt; target.&lt;br&gt;
A duplicate-id check belongs in the audit precisely because this class of bug produces&lt;br&gt;
nothing to look at.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;And a bulk find-and-replace that assumes definitions and references pair up should be&lt;br&gt;
made to prove it before it writes a single byte. Mine was guarded by habit rather than&lt;br&gt;
foresight, and habit was the only reason a twelve-against-one-hundred-and-fifteen&lt;br&gt;
mismatch never shipped.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Found while auditing dhseadev.online, August 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>svg</category>
      <category>html</category>
      <category>css</category>
    </item>
    <item>
      <title>I built a shared wall that can't be spammed, by removing the text box</title>
      <dc:creator>Dhardingsea Developer</dc:creator>
      <pubDate>Mon, 03 Aug 2026 19:38:06 +0000</pubDate>
      <link>https://dev.to/dhseadev/i-built-a-shared-wall-that-cant-be-spammed-by-removing-the-text-box-2dc6</link>
      <guid>https://dev.to/dhseadev/i-built-a-shared-wall-that-cant-be-spammed-by-removing-the-text-box-2dc6</guid>
      <description>&lt;p&gt;Why the Positivity Wall has no text field, no accounts and no likes — and what subtraction has to do with building alongside AI.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://dhseadev.online/2026/08/01/joy-is-what-is-left/" rel="noopener noreferrer"&gt;https://dhseadev.online/2026/08/01/joy-is-what-is-left/&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;I spent a day making a website easier for search engines to find, and finished it by&lt;br&gt;
building a page that has nothing to do with search engines. Nine panels. You click one,&lt;br&gt;
you leave a stamp, and the stamp is still yours the next time you come back.&lt;/p&gt;

&lt;p&gt;The interesting part isn't the feature. It's the three things I took out.&lt;/p&gt;
&lt;h2&gt;
  
  
  The first version was worse
&lt;/h2&gt;

&lt;p&gt;The original idea was a 3×3 grid of embedded videos from people who make cheerful&lt;br&gt;
things — reposted on my site. I talked myself out of it in about ten minutes, for a&lt;br&gt;
reason that turned out to matter: &lt;strong&gt;embedding someone else's video sends my visitor to&lt;br&gt;
their platform.&lt;/strong&gt; It borrows nothing and gives away the only thing I have.&lt;/p&gt;

&lt;p&gt;So the content had to come from the visitor. And the moment content comes from&lt;br&gt;
visitors, you're running a moderation problem whether you meant to or not.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why there's no text box
&lt;/h2&gt;

&lt;p&gt;The obvious design is a text field. &lt;em&gt;Leave a nice message.&lt;/em&gt; I didn't build that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three characters is enough for a slur.&lt;/strong&gt; Any free-text field on a public page is a&lt;br&gt;
commitment to moderate it forever, and I'm one person who won't always be paying&lt;br&gt;
attention. A field I can't supervise is a field I shouldn't ship.&lt;/p&gt;

&lt;p&gt;What I built instead is a stamp generated from a random seed in your browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// seed -&amp;gt; shape, glyph, hue. no assets, no storage, no input.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;rnd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;9301&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;49297&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;233280&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;233280&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;stamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;hue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;360&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;sides&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;// triangle .. heptagon&lt;/span&gt;
    &lt;span class="na"&gt;rot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;360&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;glyph&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can't type anything. &lt;strong&gt;You can't be obscene with a hexagon.&lt;/strong&gt; And the stamp is more&lt;br&gt;
personal than initials would have been, because it's yours and you didn't choose it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Identity without accounts
&lt;/h2&gt;

&lt;p&gt;No sign-up. No name. The page doesn't know who you are and has no way to find out.&lt;/p&gt;

&lt;p&gt;What it has is one integer in &lt;code&gt;localStorage&lt;/code&gt;. That's enough to say &lt;em&gt;this mark is yours&lt;/em&gt;&lt;br&gt;
and &lt;em&gt;welcome back&lt;/em&gt;, and nothing else. Clear your browser and you become a new person,&lt;br&gt;
which feels correct.&lt;/p&gt;

&lt;p&gt;Shared state runs on &lt;a href="https://playhtml.fun" rel="noopener noreferrer"&gt;playhtml&lt;/a&gt;, which is CRDT-backed — two people&lt;br&gt;
stamping at the same moment both land, with no server I run and no database I have to&lt;br&gt;
secure. &lt;strong&gt;The less I store, the less there is to leak.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What this has to do with AI
&lt;/h2&gt;

&lt;p&gt;I build alongside AI constantly. What I notice is that the tools are very good at&lt;br&gt;
producing &lt;em&gt;more&lt;/em&gt; — more pages, more features, more words — and the discipline I keep&lt;br&gt;
having to apply is subtraction.&lt;/p&gt;

&lt;p&gt;The wall could have had comments, likes, leaderboards, streaks, a login. Every one was&lt;br&gt;
available and cheap. Every one would have made it worse: more surface to moderate, more&lt;br&gt;
data to hold, more reasons to visit out of obligation instead of pleasure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Joy in software isn't a feature you add. It's what's left when you stop adding things&lt;br&gt;
that create obligation.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Credit
&lt;/h2&gt;

&lt;p&gt;The premise isn't mine. It came from &lt;a href="https://amandagore.com/the-joy-project-amanda-gore/" rel="noopener noreferrer"&gt;Amanda Gore and The Joy&lt;br&gt;
Project&lt;/a&gt;, whose work is about&lt;br&gt;
helping people reconnect with themselves and each other. Her version happens in rooms&lt;br&gt;
full of people. Mine is nine rectangles and some SVG — but the mechanism is the same&lt;br&gt;
one she's pointing at: a person feels better when they're briefly, specifically seen.&lt;/p&gt;

&lt;p&gt;You can &lt;a href="https://dhseadev.online/positivity-wall/" rel="noopener noreferrer"&gt;leave a stamp here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ux</category>
      <category>showdev</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
