<?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%2F0fcad7e4-838d-44f1-9327-88e55c681dac.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>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>
