<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>ErrorZap</title><link>https://errorzap.com/</link><description>Recent content on ErrorZap</description><image><title>ErrorZap</title><url>https://errorzap.com/og.png</url><link>https://errorzap.com/og.png</link></image><generator>Hugo</generator><language>en-US</language><copyright>ErrorZap</copyright><lastBuildDate>Wed, 29 Jul 2026 00:00:00 -0600</lastBuildDate><atom:link href="https://errorzap.com/index.xml" rel="self" type="application/rss+xml"/><item><title>About ErrorZap</title><link>https://errorzap.com/about/</link><pubDate>Wed, 29 Jul 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/about/</guid><description>Why ErrorZap exists and what belongs here.</description><content:encoded><![CDATA[<p>ErrorZap is where I write down the technical stories that are too useful,
strange, expensive, or funny to leave trapped in a terminal scrollback.</p>
<p>I work in managed IT, infrastructure, security, automation, backups, networks,
and the untidy space where a clean diagram meets a real building full of old
wiring. The posts here are field notes, not recycled product copy. Details are
generalized when they could identify a client or expose a system, but the
failure modes and lessons stay real.</p>
<p>You will also find the <a href="/categories/90s-kid/">90s Kid collection</a>, because the
people who survived IRQ conflicts, dial-up noise, rewinding VHS tapes, and
printers that sounded like farm machinery deserve their own archive.</p>
<h2 id="what-you-can-expect">What you can expect</h2>
<ul>
<li>The problem, including what made it misleading.</li>
<li>The evidence that separated the real cause from the tempting guess.</li>
<li>The fix, the boundary, or the lesson worth keeping.</li>
<li>Enough technical detail to be useful without publishing somebody&rsquo;s private
infrastructure.</li>
<li>A willingness to say when the machine was right and I was the problem.</li>
</ul>
<p>ErrorZap uses privacy-first traffic and performance measurement through
Cloudflare. It does not use an advertising network or build visitor profiles.
See the <a href="/privacy/">privacy note</a> for the short version.</p>
<p>For managed IT help, <a href="/hire/">hire me</a>.</p>
]]></content:encoded></item><item><title>Privacy</title><link>https://errorzap.com/privacy/</link><pubDate>Wed, 29 Jul 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/privacy/</guid><description>The short, human explanation of what ErrorZap measures.</description><content:encoded><![CDATA[<p>ErrorZap measures aggregate traffic and page performance using Cloudflare Web
Analytics. Cloudflare describes this product as privacy-first and says its Web
Analytics service does not collect or use visitors&rsquo; personal data.</p>
<p>There is no advertising network, cross-site visitor profile, newsletter
tracking pixel, or comment-account database on ErrorZap.</p>
<p>The operational logs needed to securely deliver the site may still include
normal request information for a limited period. If the analytics or hosting
stack changes, this page will change with it.</p>
<p>Last updated: July 29, 2026.</p>
]]></content:encoded></item><item><title>Start Here</title><link>https://errorzap.com/start/</link><pubDate>Wed, 29 Jul 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/start/</guid><description>The fastest way into ErrorZap: real failures, useful recoveries, and 90s technology.</description><content:encoded><![CDATA[<p>ErrorZap is a field notebook for the incidents worth remembering: the failure
that looked impossible, the fix that finally made sense, the warning that
should save somebody else an afternoon, and the old technology that somehow
trained us for all of it.</p>
<h2 id="start-with-the-expensive-lessons">Start with the expensive lessons</h2>
<ul>
<li><a href="/posts/how-my-own-backup-deleted-my-files/">How My Own Backup Deleted My Files</a></li>
<li><a href="/posts/the-firewall-api-call-that-wipes-your-router/">The Firewall API Call That Wipes Your Router</a></li>
<li><a href="/posts/the-settings-api-that-corrupted-every-setting/">The Settings API That Corrupted Every Setting</a></li>
<li><a href="/posts/i-found-my-own-server-on-shodan/">I Found My Own Server on Shodan</a></li>
</ul>
<h2 id="then-try-the-weird-ones">Then try the weird ones</h2>
<ul>
<li><a href="/posts/seven-network-gotchas-that-look-like-ghosts/">Seven Network Gotchas That Look Like Ghosts</a></li>
<li><a href="/posts/the-smart-blind-that-took-down-the-network/">The Smart Blind That Took Down the Network</a></li>
<li><a href="/posts/the-pwa-that-froze-when-offline/">The PWA That Froze When Offline</a></li>
<li><a href="/posts/when-postgres-wont-start/">When Postgres Won&rsquo;t Start</a></li>
</ul>
<h2 id="or-go-back-to-the-90s">Or go back to the 90s</h2>
<p>The <a href="/categories/90s-kid/">90s Kid collection</a> is about floppy disks, pagers,
dial-up, VCR clocks, flying toasters, and learning technical patience before
anybody called it troubleshooting.</p>
<h2 id="find-something-specific">Find something specific</h2>
<p><a href="/search/">Search every post</a>, browse the <a href="/archives/">complete archive</a>, or
subscribe using the <a href="/index.xml">RSS feed</a>.</p>
]]></content:encoded></item><item><title>Hire Me</title><link>https://errorzap.com/hire/</link><pubDate>Tue, 07 Jul 2026 09:00:00 -0600</pubDate><guid>https://errorzap.com/hire/</guid><description>Remote managed IT, monitoring, and ransomware-proof backups for small businesses. The incidents on this blog are from my real client work.</description><content:encoded><![CDATA[<p>I&rsquo;m Justin. I&rsquo;ve spent 25+ years fixing infrastructure, based in Pueblo, Colorado. The incidents on this blog aren&rsquo;t hypotheticals — they&rsquo;re from my real client work.</p>
<h2 id="what-i-do-remotely">What I do remotely</h2>
<ul>
<li>Managed IT and monitoring</li>
<li>Patch management</li>
<li>Ransomware-proof backups (append-only, immutable storage)</li>
<li>Camera and alarm system health monitoring</li>
<li>Microsoft 365 / Google Workspace management and security audits</li>
<li>VoIP phone systems</li>
<li>Help desk</li>
</ul>
<h2 id="pricing">Pricing</h2>
<p>No games, no &ldquo;call for a quote&rdquo; runaround:</p>
<table>
	<thead>
			<tr>
					<th>Service</th>
					<th>Price</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Workstation monitoring &amp; management</td>
					<td>$35/device/mo</td>
			</tr>
			<tr>
					<td>Server monitoring &amp; management</td>
					<td>$200/mo</td>
			</tr>
			<tr>
					<td>Remote security exposure audit (flat)</td>
					<td>$495</td>
			</tr>
			<tr>
					<td>Hourly work</td>
					<td>$150/hr</td>
			</tr>
	</tbody>
</table>
<h2 id="contact">Contact</h2>
<ul>
<li>Phone: 719-530-4040</li>
<li>Email: <a href="mailto:service@kccsonline.com">service@kccsonline.com</a></li>
<li>Form: <a href="https://callthenerd.com">callthenerd.com</a></li>
</ul>
<p>See more of what I run at <a href="https://modernsystemspro.com">modernsystemspro.com</a> and <a href="https://pueblosecure.com">pueblosecure.com</a>.</p>
]]></content:encoded></item><item><title>The Phantom Bad Blocks: How a 15-Year-Old Linux Bug Took Down 75 Security Cameras</title><link>https://errorzap.com/posts/phantom-bad-blocks/</link><pubDate>Thu, 11 Jun 2026 01:00:00 -0600</pubDate><guid>https://errorzap.com/posts/phantom-bad-blocks/</guid><description>A field report on diagnosing and repairing a RAID failure that wasn&amp;rsquo;t a hardware failure at all — and why &amp;lsquo;replace the disk&amp;rsquo; would have made it far worse.</description><content:encoded><![CDATA[<p>A client&rsquo;s surveillance system had stopped recording. Seventy-five cameras, a 44-terabyte recorder, and a banner on the dashboard reading <strong>&ldquo;Recovering storage.&rdquo;</strong> Internally the system had flipped itself into a read-only &ldquo;limited mode&rdquo; and quietly stopped saving video. By the time we dug in, it had been down for <strong>two and a half days.</strong></p>
<p>The obvious diagnosis — the one most techs would reach for, and the one the symptoms practically beg you to believe — is <em>a disk is dying, replace it.</em></p>
<p>That diagnosis would have made everything dramatically worse. Here&rsquo;s why, and what was actually going on.</p>
<figure style="text-align:center;margin:36px 0">
<svg viewBox="0 0 560 300" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="A cartoon ghost labelled BAD BLOCK floating over three hard drives, one of them spooked" style="max-width:480px;width:100%">
  <!-- drives -->
  <g font-family="ui-monospace,monospace" font-size="11">
    <g>
      <rect x="64"  y="236" width="104" height="46" rx="7" fill="#313244" stroke="#45475a" stroke-width="1.5"/>
      <circle cx="92" cy="259" r="11" fill="#1e1e2e" stroke="#585b70"/><circle cx="92" cy="259" r="3" fill="#6c7086"/>
      <text x="120" y="263" fill="#a6e3a1">OK</text>
    </g>
    <g>
      <rect x="228" y="236" width="104" height="46" rx="7" fill="#3a2230" stroke="#f38ba8" stroke-width="1.8"/>
      <circle cx="256" cy="259" r="11" fill="#2a1820" stroke="#f38ba8"/><circle cx="256" cy="259" r="3" fill="#f38ba8"/>
      <text x="284" y="263" fill="#f38ba8">?!</text>
    </g>
    <g>
      <rect x="392" y="236" width="104" height="46" rx="7" fill="#313244" stroke="#45475a" stroke-width="1.5"/>
      <circle cx="420" cy="259" r="11" fill="#1e1e2e" stroke="#585b70"/><circle cx="420" cy="259" r="3" fill="#6c7086"/>
      <text x="448" y="263" fill="#a6e3a1">OK</text>
    </g>
  </g>
  <!-- ghost -->
  <g>
    <path d="M220 156 L220 84 A60 56 0 0 1 340 84 L340 156 l-20 -16 l-20 16 l-20 -16 l-20 16 l-20 -16 l-20 16 Z" fill="#cba6f7" opacity="0.95"/>
    <ellipse cx="262" cy="104" rx="8" ry="11" fill="#11111b"/>
    <ellipse cx="298" cy="104" rx="8" ry="11" fill="#11111b"/>
    <ellipse cx="280" cy="126" rx="6" ry="8" fill="#11111b"/>
    <!-- little arm + tag -->
    <path d="M340 120 q26 4 40 18" stroke="#cba6f7" stroke-width="7" fill="none" stroke-linecap="round" opacity="0.95"/>
    <g transform="rotate(-8 410 150)">
      <rect x="372" y="132" width="86" height="30" rx="5" fill="#f38ba8"/>
      <text x="415" y="151" font-family="ui-monospace,monospace" font-size="11" font-weight="700" fill="#11111b" text-anchor="middle">BAD BLOCK</text>
    </g>
  </g>
</svg>
<figcaption style="color:#6c7086;font-size:14px;margin-top:8px">A phantom that was never there — scribbled onto disks that were perfectly fine.</figcaption>
</figure>
<h2 id="the-symptom-vs-the-disease">The symptom vs. the disease</h2>
<p>The logs were screaming the same error, over and over, hundreds of times:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>EXT4-fs warning (device md3): I/O error writing to
</span></span><span style="display:flex;"><span>  inode 339687425, block 357888066
</span></span><span style="display:flex;"><span>Buffer I/O error on device md3, logical block 357888066
</span></span></code></pre></div><p>Same block. Every single time. The filesystem wanted to write to one specific spot on the array and the write kept failing.</p>
<p>When a write to a RAID array fails on the same block forever, instinct says <em>bad sector, failing drive.</em> So the first thing we did was pull the health stats (SMART) off all seven drives.</p>
<p>Every counter that would indicate a failing disk — reallocated sectors, pending sectors, offline-uncorrectable sectors, cable/CRC errors — was <strong>zero. On all seven drives.</strong> The disks were pristine.</p>
<figure style="margin:30px 0">
<div style="background:#11111b;border:1px solid #313244;border-radius:12px;padding:16px 18px;font-family:ui-monospace,Menlo,Consolas,monospace;font-size:13.5px;line-height:1.8;overflow-x:auto">
<div style="color:#6c7086;margin-bottom:10px"># smartctl -A /dev/sd[a-g] &nbsp;·&nbsp; the five minutes that cracked the case</div>
<div style="color:#cdd6f4">Reallocated_Sector_Ct &nbsp;....&nbsp; <span style="color:#a6e3a1">0</span></div>
<div style="color:#cdd6f4">Current_Pending_Sector &nbsp;...&nbsp; <span style="color:#a6e3a1">0</span></div>
<div style="color:#cdd6f4">Offline_Uncorrectable &nbsp;....&nbsp; <span style="color:#a6e3a1">0</span></div>
<div style="color:#cdd6f4">UDMA_CRC_Error_Count &nbsp;.....&nbsp; <span style="color:#a6e3a1">0</span></div>
<div style="color:#fab387;margin-top:10px">×7 drives — all zero. Not one of them is dying.</div>
</div>
</figure>
<p>Healthy disks. A write that won&rsquo;t complete. Same block forever. That contradiction is the whole story.</p>
<h2 id="whats-actually-a-bad-block-list">What&rsquo;s actually a &ldquo;bad block list&rdquo;?</h2>
<p>Linux software RAID (the <code>md</code> subsystem, the thing <code>mdadm</code> drives) has a feature called the <strong>Bad Block List</strong>, or BBL. The idea sounds reasonable: if the array ever has trouble with a specific spot on a member disk, instead of kicking the whole disk out, it just writes that spot down in a little list — <em>&ldquo;don&rsquo;t use this block&rdquo;</em> — and routes around it.</p>
<p>Helpful in theory. In practice, it has been <strong>one of the most controversial features in the Linux RAID stack for about fifteen years.</strong> Kernel developers have openly argued for removing it. The reason is exactly what we walked into:</p>
<ol>
<li>Something causes a <strong>transient</strong> I/O hiccup on a member — a momentary timeout, a controller blip, a drive that stalls for a fraction of a second under heavy write load. Nothing actually wrong with the media.</li>
<li>The BBL feature misreads that hiccup as a bad block and <strong>writes it down permanently.</strong></li>
<li>On RAID5, where every stripe is spread across multiple disks, the bad-block entry gets <strong>propagated onto other members.</strong> Over time, the same logical region ends up flagged on several disks.</li>
</ol>
<p>We confirmed this precisely. Four of the seven disks carried <strong>byte-for-byte identical</strong> bad-block ranges — and that is the tell:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>       RAID5  ·  seven disks  ·  44 TB usable
</span></span><span style="display:flex;"><span>  ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┐
</span></span><span style="display:flex;"><span>  │ sda │ sdb │ sdc │ sdd │ sde │ sdf │ sdg │
</span></span><span style="display:flex;"><span>  ├─────┼─────┼─────┼─────┼─────┼─────┼─────┤
</span></span><span style="display:flex;"><span>  │  ·  │ ▓▓▓ │ ▓▓▓ │ ▓▓▓ │  ·  │  ·  │ ▓▓▓ │
</span></span><span style="display:flex;"><span>  │clean│ BBL │ BBL │ BBL │clean│clean│ BBL │
</span></span><span style="display:flex;"><span>  └─────┴─────┴─────┴─────┴─────┴─────┴─────┘
</span></span><span style="display:flex;"><span>           ╰──── identical phantom ranges ────╯
</span></span><span style="display:flex;"><span>       real bad sectors do NOT line up to the exact
</span></span><span style="display:flex;"><span>       same address across four separate drives.
</span></span></code></pre></div><h2 id="why-this-stops-recording-entirely">Why this stops recording entirely</h2>
<p>This is the part that turns a phantom bookkeeping error into a system outage.</p>
<p>To write one stripe of video, RAID5 has to write to <strong>several disks at once</strong> plus update parity. If even one of those disks says <em>&ldquo;that block is on my no-go list,&rdquo;</em> the <strong>entire stripe write fails</strong> and returns an I/O error:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>  ONE stripe write ──► every data member must accept it
</span></span><span style="display:flex;"><span>   ┌────┐┌────┐┌────┐┌────┐┌────┐┌────┐
</span></span><span style="display:flex;"><span>   │ D0 ││ D1 ││ D2 ││ D3 ││ D4 ││ P  │
</span></span><span style="display:flex;"><span>   └────┘└─🚫─┘└────┘└─🚫─┘└────┘└────┘
</span></span><span style="display:flex;"><span>            │         │
</span></span><span style="display:flex;"><span>            ╰── &#34;on my no-go list&#34; ──╯
</span></span><span style="display:flex;"><span>                      │
</span></span><span style="display:flex;"><span>                      ▼
</span></span><span style="display:flex;"><span>        ✗  THE WHOLE STRIPE FAILS  →  EIO
</span></span><span style="display:flex;"><span>        →  filesystem protectively goes read-only
</span></span><span style="display:flex;"><span>        →  recorder drops to &#34;limited mode&#34;
</span></span><span style="display:flex;"><span>        →  75 cameras stop saving video
</span></span></code></pre></div><p>The filesystem above sees the write fail, decides its metadata is now untrustworthy, and protectively remounts read-only. The camera software sees a read-only volume and drops into limited mode. Recording stops. All of it.</p>
<p>So: zero failing hardware, and a total recording outage, caused entirely by a list of imaginary bad spots the software refused to write over.</p>
<h2 id="the-trap">The trap</h2>
<p>Now the dangerous part. The &ldquo;replace the disk&rdquo; reflex.</p>
<p>If you pull one of those drives and let the array rebuild onto a replacement, <strong>mdadm re-creates a bad-block list on the new member and re-propagates the same poison.</strong> You&rsquo;d do four sequential multi-hour degraded rebuilds, each one re-importing the exact problem you were trying to remove, while running the array in its most fragile state. Replacing disks here doesn&rsquo;t just fail to fix it — it actively risks the array.</p>
<figure style="text-align:center;margin:34px 0">
<svg viewBox="0 0 560 180" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="A brand new disk goes into the array, then a bad-block ghost reappears on it" style="max-width:520px;width:100%">
  <g font-family="ui-monospace,monospace" font-size="12">
    <!-- new disk -->
    <rect x="44" y="80" width="140" height="58" rx="9" fill="#1e2a1e" stroke="#a6e3a1" stroke-width="1.8"/>
    <circle cx="78" cy="109" r="13" fill="#11150f" stroke="#a6e3a1"/><circle cx="78" cy="109" r="3.5" fill="#a6e3a1"/>
    <text x="104" y="106" fill="#a6e3a1">NEW</text>
    <text x="104" y="122" fill="#a6e3a1">disk ✓</text>
    <text x="44" y="68" fill="#6c7086" font-size="11">swap it in…</text>
    <!-- arrow -->
    <line x1="200" y1="109" x2="300" y2="109" stroke="#6c7086" stroke-width="2.5"/>
    <path d="M300 109 l-12 -6 l0 12 Z" fill="#6c7086"/>
    <text x="214" y="100" fill="#6c7086" font-size="11">rebuild</text>
    <!-- disk with ghost -->
    <rect x="318" y="80" width="140" height="58" rx="9" fill="#313244" stroke="#45475a" stroke-width="1.8"/>
    <circle cx="352" cy="109" r="13" fill="#1e1e2e" stroke="#585b70"/><circle cx="352" cy="109" r="3.5" fill="#6c7086"/>
    <!-- ghost popping up -->
    <path d="M386 78 L386 46 A19 18 0 0 1 424 46 L424 78 l-6.3 -6 l-6.3 6 l-6.3 -6 l-6.3 6 l-6.3 -6 Z" fill="#cba6f7"/>
    <ellipse cx="399" cy="56" rx="3.4" ry="4.6" fill="#11111b"/>
    <ellipse cx="412" cy="56" rx="3.4" ry="4.6" fill="#11111b"/>
    <!-- BBL tag -->
    <rect x="430" y="58" width="44" height="22" rx="4" fill="#f38ba8"/>
    <text x="452" y="73" fill="#11111b" font-size="11" font-weight="700" text-anchor="middle">BBL</text>
    <text x="318" y="160" fill="#6c7086" font-size="11">…and the list rebuilds itself.</text>
  </g>
</svg>
<figcaption style="color:#6c7086;font-size:14px;margin-top:8px">Replace a disk and the array writes the bad-block list right back onto the new one. The ghost just photocopies itself.</figcaption>
</figure>
<p>The correct fix is the opposite of intuition: <strong>don&rsquo;t touch the hardware. Erase the lists.</strong></p>
<h2 id="the-repair">The repair</h2>
<p><code>mdadm</code> has an escape hatch for exactly this — a flag that clears (and disables) the bad-block feature across every member as the array is assembled:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>mdadm --assemble --update<span style="color:#ff79c6">=</span>force-no-bbl /dev/md3 /dev/sd<span style="color:#ff79c6">[</span>a-g<span style="color:#ff79c6">]</span><span style="color:#bd93f9">5</span>
</span></span></code></pre></div><p>The careful sequence around that one command:</p>
<ol>
<li><strong>Back up everything first.</strong> Recovery codes, configuration, and a full pre-incident database snapshot, all pulled off the box. Nothing to lose before touching the array.</li>
<li><strong>Quiet the system.</strong> Stop every service touching the volume so nothing is mid-write. (This box had <em>seventeen</em> interlocking services and a storage daemon that re-assembles the array on its own.)</li>
<li><strong>Unmount and stop the array.</strong></li>
<li><strong>Clear the bad-block lists</strong> with the command above. Afterward, every member reported the magic words:</li>
</ol>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>  sda5  →  No bad-blocks list configured
</span></span><span style="display:flex;"><span>  sdb5  →  No bad-blocks list configured
</span></span><span style="display:flex;"><span>  sdc5  →  No bad-blocks list configured
</span></span><span style="display:flex;"><span>  sdd5  →  No bad-blocks list configured
</span></span><span style="display:flex;"><span>  sde5  →  No bad-blocks list configured
</span></span><span style="display:flex;"><span>  sdf5  →  No bad-blocks list configured
</span></span><span style="display:flex;"><span>  sdg5  →  No bad-blocks list configured
</span></span><span style="display:flex;"><span>                                 ☁  poof. gone.
</span></span></code></pre></div><ol start="5">
<li><strong>Check the filesystem.</strong> A read-only <code>e2fsck -fn</code> preview came back <strong>clean</strong> — only two trivial, optional defragmentation suggestions. Zero corruption, zero data to remove. That confirmed the &ldquo;filesystem damage&rdquo; was never real damage; it was just the blocked writes.</li>
<li><strong>Reboot and verify.</strong></li>
</ol>
<p>The result: array healthy, <strong>zero</strong> I/O errors, bad-block lists empty, and within seconds of the services coming back, the full camera fleet was writing video again. Total surgery time: about an hour. Zero permanent data loss.</p>
<h2 id="the-gotcha-worth-its-own-paragraph">The gotcha worth its own paragraph</h2>
<p>These appliances don&rsquo;t assemble their array from a config file the way a generic Linux server does. A storage daemon brings the array up at boot. That means the daemon will happily <strong>re-assemble and re-mount the array out from under you</strong> in the middle of a repair if you don&rsquo;t stop it — and it has to be running again before reboot or the box comes up with no storage at all. Finding and accounting for every one of those interlocking services (seventeen, including media-server processes that respawn instantly when killed) was most of the actual work. The headline command takes one second; safely getting the system into a state where that command is safe to run takes the hour.</p>
<h2 id="the-kicker">The kicker</h2>
<p>Once a system surprises you, the right question is: <em>where else do we have this and not know it yet?</em></p>
<p>So we audited every storage system under management for the same fingerprint. Most weren&rsquo;t even capable of this failure (different storage technology). One was clean. And one <strong>other</strong> recorder was sitting in the <strong>earliest stage of the exact same bug</strong> — two phantom bad-block ranges just beginning to form, still recording fine, no errors yet. Caught months before it would have become an outage. We added automated monitoring across the fleet that checks these lists every 30 minutes, so the next occurrence is a 30-minute alert instead of a two-day blackout.</p>
<h2 id="takeaways">Takeaways</h2>
<ul>
<li><strong>Symptoms lie. Health data doesn&rsquo;t.</strong> The single most useful five minutes of this entire incident was reading SMART and seeing all zeros. That one fact ruled out the obvious-but-wrong diagnosis and pointed at the real one.</li>
<li><strong>The bad-block list is a footgun.</strong> If you run Linux software RAID, know this feature exists, know it can manufacture failures out of transient hiccups, and know <code>--update=force-no-bbl</code> is how you clear it. Consider monitoring for non-empty lists proactively.</li>
<li><strong>The intuitive fix can be the destructive one.</strong> &ldquo;Replace the disk&rdquo; would have deepened the hole. Sometimes the move is to change nothing physical and correct the software&rsquo;s bad bookkeeping.</li>
<li><strong>Always back up before array surgery,</strong> even when you&rsquo;re confident. <em>Especially</em> when you&rsquo;re confident.</li>
<li><strong>Then go look at everything else.</strong> The bug you just fixed is rarely the only instance of it you own.</li>
</ul>
<p>The hardware was never broken. The drives were never dying. A fifteen-year-old software feature, trying to be helpful, wrote down a problem that didn&rsquo;t exist and then refused to work around its own mistake. Once you can see that, the fix is almost anticlimactic. Seeing it is the job.</p>
<div style="text-align:center;margin:40px 0 0">
<svg viewBox="0 0 130 120" width="96" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="A faint ghost fading away" style="opacity:0.45">
  <path d="M30 110 L30 52 A35 33 0 0 1 100 52 L100 110 l-11.6 -10 l-11.6 10 l-11.6 -10 l-11.6 10 l-11.6 -10 Z" fill="#cba6f7"/>
  <ellipse cx="53" cy="64" rx="5" ry="7" fill="#11111b"/>
  <ellipse cx="77" cy="64" rx="5" ry="7" fill="#11111b"/>
  <ellipse cx="65" cy="84" rx="4" ry="5.5" fill="#11111b"/>
</svg>
<div style="color:#6c7086;font-size:13px;font-family:ui-monospace,monospace;margin-top:4px">~ no bad-blocks list configured ~</div>
</div>
]]></content:encoded></item><item><title>The Alert That Went to Nobody</title><link>https://errorzap.com/posts/the-alert-that-went-to-nobody/</link><pubDate>Tue, 09 Jun 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/the-alert-that-went-to-nobody/</guid><description>A critical event fired a perfectly good alert into a receiver wired to absolutely no one — and everybody slept fine because of it.</description><content:encoded><![CDATA[<figure style="text-align:center;margin:0 0 30px"><img src="hero.png" alt="The Alert That Went to Nobody" style="max-width:520px;width:100%;border-radius:14px"/></figure>
<p>A customer called on a Tuesday. A critical service had been down for hours. Their first question was the one that ruins your week:</p>
<p>&ldquo;Why didn&rsquo;t we get an alert?&rdquo;</p>
<p>I had built the alerting. I knew the rules existed. I&rsquo;d watched them fire in testing months ago. So my honest, confident, completely wrong answer was: &ldquo;You should have.&rdquo;</p>
<p>That&rsquo;s the trap. &ldquo;Should have&rdquo; is what you say right before you learn something humbling about your own stack.</p>
<h2 id="the-investigation">The investigation</h2>
<p>First instinct: the rule didn&rsquo;t fire. Easy theory, easy to check. So I pulled the alert history.</p>
<p>The rule fired. On time. With the right severity, the right labels, the right everything. It did its job flawlessly and then handed the alert off to&hellip; something.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>┌──────────┐    ┌────────────┐    ┌──────────┐    ┌────────┐
</span></span><span style="display:flex;"><span>│  RULE    │──► │  RECEIVER  │──► │ CHANNEL  │──► │ HUMAN  │
</span></span><span style="display:flex;"><span>│ ✓ fired  │    │ ✓ matched  │    │  ✗ none  │    │  ✗ —   │
</span></span><span style="display:flex;"><span>└──────────┘    └────────────┘    └──────────┘    └────────┘
</span></span><span style="display:flex;"><span>                                   ▲
</span></span><span style="display:flex;"><span>                                   │
</span></span><span style="display:flex;"><span>                          the chain dies right here
</span></span></code></pre></div><p>The rule routed to a receiver. The receiver had a name, a config block, a place in the tree. It looked alive. And its list of channels — the email addresses, the webhook URLs, the push targets, whatever was supposed to carry the message to a person — was empty.</p>
<p>Not broken. Not misconfigured. <strong>Empty.</strong> The alert was delivered, perfectly, to nobody.</p>
<figure style="text-align:center;margin:34px 0">
<svg viewBox="0 0 560 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="An alert envelope flying into an empty mailbox">
  <rect x="0" y="0" width="560" height="220" rx="10" fill="#11111b"/>
  <text x="280" y="30" text-anchor="middle" fill="#cdd6f4" font-family="monospace" font-size="15">rule → receiver → channel → ?</text>
  <!-- envelope -->
  <g>
    <rect x="60" y="92" width="96" height="64" rx="6" fill="#1e1e2e" stroke="#cba6f7" stroke-width="2"/>
    <path d="M60 98 L108 130 L156 98" fill="none" stroke="#cba6f7" stroke-width="2"/>
    <text x="108" y="180" text-anchor="middle" fill="#cba6f7" font-family="monospace" font-size="12">ALERT</text>
  </g>
  <!-- flight path -->
  <path d="M170 124 Q300 70 400 124" fill="none" stroke="#fab387" stroke-width="2" stroke-dasharray="6 6"/>
  <polygon points="400,124 388,116 390,132" fill="#fab387"/>
  <!-- empty mailbox -->
  <g>
    <rect x="408" y="96" width="96" height="60" rx="8" fill="#1e1e2e" stroke="#45475a" stroke-width="2"/>
    <rect x="408" y="96" width="96" height="20" rx="8" fill="#313244"/>
    <text x="456" y="138" text-anchor="middle" fill="#6c7086" font-family="monospace" font-size="13">[ empty ]</text>
    <line x1="456" y1="156" x2="456" y2="184" stroke="#45475a" stroke-width="3"/>
  </g>
<p><text x="456" y="206" text-anchor="middle" fill="#f38ba8" font-family="monospace" font-size="12">no recipients ✗</text>
</svg></p>
<figcaption style="color:#6c7086;font-size:14px;margin-top:8px">The mail always went out. There was just no mailbox at the end of the route.</figcaption>
</figure>
<h2 id="the-aha">The &ldquo;aha&rdquo;</h2>
<p>Here&rsquo;s the part that stung. Everyone — me included — had validated the wrong layer.</p>
<p>We tested that rules existed. We tested that rules fired. We never tested that a fired alert reached a phone, an inbox, a pager, a chat channel — a <em>person</em>. The chain has four links; we&rsquo;d verified two and assumed the other two by vibes.</p>
<p>An alert is not a notification. A rule firing is not someone finding out. The gap between those two facts is exactly where this outage lived — silent, for months — manufacturing a warm, false confidence that the system &ldquo;had alerting.&rdquo;</p>
<p>It did. The alerting just talked to a wall.</p>
<h2 id="the-fix">The fix</h2>
<p>Audit every receiver. Every single one needs at least one channel, and that channel has to actually reach a human or a device. No empties, no dead webhooks, no stale email lists.</p>
<p>Then — and this is the part people skip — add a heartbeat. A &ldquo;dead-man&rdquo; alert that is <em>supposed</em> to fire on a schedule. If it stops arriving, your delivery path is broken and you find out on purpose instead of during a customer call.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># 1. Find every receiver with an empty / missing channel list.</span>
</span></span><span style="display:flex;"><span>amtool config routes show
</span></span><span style="display:flex;"><span>amtool config show | yq <span style="color:#f1fa8c">&#39;.receivers[] | select(
</span></span></span><span style="display:flex;"><span><span style="color:#f1fa8c">  ([.email_configs, .webhook_configs, .pushover_configs,
</span></span></span><span style="display:flex;"><span><span style="color:#f1fa8c">    .slack_configs, .opsgenie_configs] | flatten | length) == 0
</span></span></span><span style="display:flex;"><span><span style="color:#f1fa8c">) | .name&#39;</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># 2. End-to-end test: fire a synthetic alert and confirm it lands on a device.</span>
</span></span><span style="display:flex;"><span>amtool alert add deadmans_switch <span style="color:#f1fa8c">\
</span></span></span><span style="display:flex;"><span>  <span style="color:#8be9fd;font-style:italic">severity</span><span style="color:#ff79c6">=</span>info <span style="color:#8be9fd;font-style:italic">service</span><span style="color:#ff79c6">=</span>alerting <span style="color:#f1fa8c">\
</span></span></span><span style="display:flex;"><span>  --annotation <span style="color:#8be9fd;font-style:italic">summary</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;heartbeat — if you can read this, delivery works&#34;</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#6272a4"># 3. The dead-man heartbeat: a rule that is ALWAYS firing on purpose.</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">groups</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#ff79c6">name</span>: heartbeat
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">rules</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ff79c6">alert</span>: DeadMansSwitch
</span></span><span style="display:flex;"><span>        <span style="color:#ff79c6">expr</span>: vector(1)
</span></span><span style="display:flex;"><span>        <span style="color:#ff79c6">labels</span>: { <span style="color:#ff79c6">severity</span>: heartbeat }
</span></span><span style="display:flex;"><span>        <span style="color:#ff79c6">annotations</span>:
</span></span><span style="display:flex;"><span>          <span style="color:#ff79c6">summary</span>: <span style="color:#f1fa8c">&#34;Alerting pipeline is alive. Silence = it isn&#39;t.&#34;</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>DELIVERY CHAIN ── post-fix
</span></span><span style="display:flex;"><span>  RULE ─► RECEIVER ─► CHANNEL ─► DEVICE
</span></span><span style="display:flex;"><span>   ✓        ✓          ✓ (≥1)     ✓ buzzed
</span></span><span style="display:flex;"><span>        + DeadMansSwitch heartbeat every 60s ►► if it goes quiet, you know
</span></span></code></pre></div><p>Flip the logic. A normal alert proves a problem exists. A heartbeat proves the <em>path itself</em> still works. You want both, because the path is the thing that fails silently.</p>
<h2 id="why-it-happened">Why it happened</h2>
<p>Someone — possibly past me — created the receiver as a placeholder, fully intending to fill in the channels later. Later never came. The config was syntactically valid, so nothing complained. There&rsquo;s no error on an empty channel list; it&rsquo;s a legal, boring state.</p>
<p>And because the rules were visibly there, every review after that pattern-matched &ldquo;alerting: configured ✓&rdquo; and moved on. Nobody walked the whole chain to a buzzing phone. The placeholder calcified into production.</p>
<h2 id="takeaways">Takeaways</h2>
<ul>
<li><strong>An alert with no destination is worse than no alert.</strong> No alerting at least keeps you honest. A dead receiver hands you false confidence and bills you for it during an outage.</li>
<li><strong>Validate the entire chain: rule → receiver → channel → device.</strong> Verifying any single link tells you almost nothing about the link after it.</li>
<li><strong>Test delivery, not configuration.</strong> &ldquo;The rule exists&rdquo; and &ldquo;a human got paged&rdquo; are different claims. Only the second one matters at 2 a.m.</li>
<li><strong>Add a dead-man heartbeat.</strong> A signal that <em>should</em> arrive on schedule turns silent failure into a loud, on-purpose one.</li>
<li><strong>Audit receivers for empties on a cadence.</strong> Placeholders, decommissioned inboxes, and rotted webhooks accumulate. Grep them out before they grep you.</li>
</ul>
]]></content:encoded></item><item><title>The TLS Cert Only an API Could Renew</title><link>https://errorzap.com/posts/the-cert-only-an-api-could-renew/</link><pubDate>Tue, 09 Jun 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/the-cert-only-an-api-could-renew/</guid><description>A FreePBX user panel served an expired TLS cert, every reload swore it was fixed, and the only thing that actually moved the needle was calling the cert module&amp;rsquo;s own regeneration API.</description><content:encoded><![CDATA[<figure style="text-align:center;margin:0 0 30px"><img src="hero.png" alt="The TLS Cert Only an API Could Renew" style="max-width:520px;width:100%;border-radius:14px"/></figure>
<p>A client&rsquo;s user control panel popped a browser security wall. Expired certificate. Classic.</p>
<p>The panel runs on a FreePBX appliance, on a high port, separate from the main admin UI. Users hit it for voicemail and call history. Now they hit a full-page TLS warning instead.</p>
<p>No big deal, I figured. Renew the cert, reload, done before the coffee gets cold.</p>
<p>The coffee got cold.</p>
<h2 id="the-investigation">The investigation</h2>
<p>First I confirmed it wasn&rsquo;t the browser lying to me:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>openssl s_client -connect 192.0.2.10:8003 &lt;/dev/null 2&gt;/dev/null <span style="color:#f1fa8c">\
</span></span></span><span style="display:flex;"><span>  | openssl x509 -noout -dates -subject
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># notBefore=Apr  1 00:00:00 2024 GMT</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># notAfter=Apr  1 00:00:00 2025 GMT</span>
</span></span></code></pre></div><p>Dead for over a year. The panel had been quietly serving a corpse the whole time, and nobody noticed until a browser update got strict about it.</p>
<p>So I did the obvious things. The UI has a big friendly <strong>Apply Config</strong> button. Clicked it. Green checkmark. &ldquo;Reload completed successfully.&rdquo;</p>
<p>Re-ran my <code>openssl</code> check. Same expired dates.</p>
<p>Fine. Bigger hammer. From the CLI:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>fwconsole reload
</span></span><span style="display:flex;"><span>fwconsole restart
</span></span><span style="display:flex;"><span>fwconsole ma updateall
</span></span></code></pre></div><p>Three clean exits. Three &ldquo;success&rdquo; lines. Three pieces of nothing — the panel kept serving the exact same expired cert, byte for byte.</p>
<p>That&rsquo;s the moment the job stops being annoying and gets interesting. When every &ldquo;fix&rdquo; reports success and the resource never changes, you&rsquo;re not fixing the resource. You&rsquo;re poking something that doesn&rsquo;t own it.</p>
<h2 id="the-aha">The &ldquo;aha&rdquo;</h2>
<p>I drew the actual ownership chain — the thing nobody draws until they&rsquo;re an hour in:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>┌──────────────────────────────────────────────────────┐
</span></span><span style="display:flex;"><span>│  reload / apply-config / updateall                    │
</span></span><span style="display:flex;"><span>│      │                                                │
</span></span><span style="display:flex;"><span>│      ▼  &#34;rebuild my running config from what exists&#34;  │
</span></span><span style="display:flex;"><span>│  ┌───────────────────┐                                │
</span></span><span style="display:flex;"><span>│  │  User Panel :8003 │  ── binds ──►  cert file ✗     │
</span></span><span style="display:flex;"><span>│  └───────────────────┘               (expired, stale) │
</span></span><span style="display:flex;"><span>│                                            ▲           │
</span></span><span style="display:flex;"><span>│                                            │ OWNS      │
</span></span><span style="display:flex;"><span>│                                  ┌───────────────────┐ │
</span></span><span style="display:flex;"><span>│                                  │ Certificate Mgr   │ │
</span></span><span style="display:flex;"><span>│                                  │  (lifecycle here) │ │
</span></span><span style="display:flex;"><span>│                                  └───────────────────┘ │
</span></span><span style="display:flex;"><span>└──────────────────────────────────────────────────────┘
</span></span></code></pre></div><p>The reload path rebuilds config from whatever certs already exist on disk. It never <em>regenerates</em> one. The thing that actually mints and renews the cert is the <strong>Certificate Manager</strong> module. Reload was faithfully re-binding the panel to a file that was still expired — and honestly reporting success, because re-binding <em>did</em> succeed. The cert was just garbage.</p>
<p>Nobody was renewing it because nobody told the component that owns renewals to do its job.</p>
<figure style="text-align:center;margin:34px 0">
<svg viewBox="0 0 560 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Reload re-binds a stale cert; only the Certificate Manager API reissues it">
  <defs>
    <marker id="arrow" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto">
      <path d="M0,0 L7,3 L0,6 Z" fill="#cba6f7"/>
    </marker>
  </defs>
  <rect x="0" y="0" width="560" height="220" rx="10" fill="#11111b"/>
  <rect x="30" y="40" width="150" height="60" rx="8" fill="#1e1e2e" stroke="#45475a"/>
  <text x="105" y="66" fill="#cdd6f4" font-family="monospace" font-size="13" text-anchor="middle">reload /</text>
  <text x="105" y="84" fill="#cdd6f4" font-family="monospace" font-size="13" text-anchor="middle">apply config</text>
  <rect x="30" y="130" width="150" height="60" rx="8" fill="#1e1e2e" stroke="#f38ba8"/>
  <text x="105" y="156" fill="#f38ba8" font-family="monospace" font-size="13" text-anchor="middle">panel :8003</text>
  <text x="105" y="174" fill="#f38ba8" font-family="monospace" font-size="12" text-anchor="middle">cert expired</text>
  <rect x="360" y="130" width="170" height="60" rx="8" fill="#1e1e2e" stroke="#a6e3a1"/>
  <text x="445" y="156" fill="#a6e3a1" font-family="monospace" font-size="13" text-anchor="middle">Certificate Mgr</text>
  <text x="445" y="174" fill="#94e2d5" font-family="monospace" font-size="12" text-anchor="middle">owns lifecycle</text>
  <line x1="105" y1="100" x2="105" y2="130" stroke="#6c7086" stroke-width="2"/>
  <text x="120" y="120" fill="#6c7086" font-family="monospace" font-size="11" text-anchor="start">re-binds stale file</text>
  <path d="M360 160 L200 160" stroke="#cba6f7" stroke-width="2" fill="none" marker-end="url(#arrow)"/>
  <text x="280" y="150" fill="#cba6f7" font-family="monospace" font-size="11" text-anchor="middle">regenerate</text>
</svg>
<figcaption style="color:#6c7086;font-size:14px;margin-top:8px">Reload re-binds the panel to a stale file. Only the Certificate Manager can reissue it.</figcaption>
</figure>
<h2 id="the-fix">The fix</h2>
<p>Stop driving the reload path. Drive the API of the thing that owns the cert.</p>
<p>The Certificate Manager exposes a regeneration call. Invoke it directly, let it reissue, then point the panel at the fresh cert and reload <em>once</em> to bind:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># Ask the module that OWNS the cert to reissue it</span>
</span></span><span style="display:flex;"><span>fwconsole certificates --regen-self-signed
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># (or, when an ACME/managed cert is in play)</span>
</span></span><span style="display:flex;"><span>fwconsole certificates --update
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># Now bind the panel to the freshly minted cert, then reload to apply</span>
</span></span><span style="display:flex;"><span>fwconsole certificates --default<span style="color:#ff79c6">=</span>&lt;new_cert_id&gt;
</span></span><span style="display:flex;"><span>fwconsole reload
</span></span></code></pre></div><p>Then verify against the live socket — not the UI&rsquo;s word, the actual TLS handshake:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>openssl s_client -connect 192.0.2.10:8003 &lt;/dev/null 2&gt;/dev/null <span style="color:#f1fa8c">\
</span></span></span><span style="display:flex;"><span>  | openssl x509 -noout -dates
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># notBefore=Jun  9 00:00:00 2026 GMT</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># notAfter=Jun  9 00:00:00 2028 GMT   ✓</span>
</span></span></code></pre></div><p>Fresh dates. Panel loaded clean. Coffee remade.</p>
<h2 id="why-it-happened">Why it happened</h2>
<p>&ldquo;Reload&rdquo; and &ldquo;apply config&rdquo; are <em>rebuild</em> operations. They take the current declared state and re-render it. If the declared state references a cert file, they&rsquo;ll happily wire it up — expired or not. Validity isn&rsquo;t their job.</p>
<p>Renewal lives in exactly one place: the module that issues certs. Every other button just consumes the output. So every reload was a no-op on the resource, dressed up as a success because the binding step really did succeed.</p>
<p>The trap is that &ldquo;success&rdquo; was true at the wrong altitude. The command did what it was scoped to do. It just wasn&rsquo;t scoped to the thing I needed.</p>
<h2 id="takeaways">Takeaways</h2>
<ul>
<li><strong>When every fix reports success but the resource never changes, you&rsquo;re poking the wrong subsystem.</strong> Find the component that actually owns the resource&rsquo;s lifecycle and drive <em>its</em> API.</li>
<li><strong>&ldquo;reload&rdquo; / &ldquo;apply config&rdquo; usually means re-render, not regenerate.</strong> They consume artifacts; they rarely create them.</li>
<li><strong>Trust the wire, not the UI.</strong> <code>openssl s_client ... | openssl x509 -noout -dates</code> against the live port is ground truth. A green checkmark is a claim.</li>
<li><strong>Map ownership before you start hammering buttons.</strong> One quick &ldquo;who actually mints this?&rdquo; diagram beats an hour of confident no-ops.</li>
<li><strong>Expired certs hide for months.</strong> Monitor <code>notAfter</code> on every TLS port — including the weird high-port sub-services nobody remembers exist.</li>
</ul>
]]></content:encoded></item><item><title>When Postgres Won't Start</title><link>https://errorzap.com/posts/when-postgres-wont-start/</link><pubDate>Mon, 08 Jun 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/when-postgres-wont-start/</guid><description>A database stuck forever on &amp;lsquo;starting up&amp;rsquo; wasn&amp;rsquo;t broken — it was clawing its way through WAL replay on a dying disk.</description><content:encoded><![CDATA[<figure style="text-align:center;margin:0 0 30px"><img src="hero.png" alt="When Postgres Won't Start" style="max-width:520px;width:100%;border-radius:14px"/></figure>
<p>The pager went off at the worst possible hour, which is the only hour pagers know.</p>
<p>Every app talking to the database was throwing the same line:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>FATAL:  the database system is starting up
</span></span></code></pre></div><p>Not &ldquo;down.&rdquo; Not &ldquo;connection refused.&rdquo; <em>Starting up.</em> As if it were thirty seconds from being fine. It said that thirty seconds ago too. And thirty seconds before that.</p>
<h2 id="the-investigation">The investigation</h2>
<p>First instinct: restart it harder. Bad instinct. I sat on my hands.</p>
<p>The host had taken an ugly storage hiccup earlier — the array blipped, the kernel got cranky, and Postgres went down without a clean shutdown. So I went to the only source of truth that matters here: the server log.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo tail -f /var/lib/pgsql/data/log/postgresql-*.log
</span></span></code></pre></div><p>And there it was, crawling:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>LOG:  database system was not properly shut down; automatic recovery in progress
</span></span><span style="display:flex;"><span>LOG:  redo starts at 3F/A2000028
</span></span><span style="display:flex;"><span>LOG:  redo in progress, elapsed time: 412.06 s, current LSN: 3F/A2090110
</span></span></code></pre></div><p>Recovery <em>was</em> happening. It was just happening at the speed of continental drift. In seven minutes of replay it had moved the LSN by a rounding error.</p>
<p>That&rsquo;s not a Postgres bug. That&rsquo;s a disk gasping for air.</p>
<h2 id="the-aha">The &ldquo;aha&rdquo;</h2>
<p>Here&rsquo;s the thing every junior forgets at 3 a.m.: after an unclean stop, Postgres doesn&rsquo;t just &ldquo;boot.&rdquo; It replays its <strong>write-ahead log</strong> to drag the data files back to a consistent state. Until that replay finishes, the server refuses connections — and the polite way it refuses is <code>the database system is starting up</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>        unclean shutdown
</span></span><span style="display:flex;"><span>                │
</span></span><span style="display:flex;"><span>                ▼
</span></span><span style="display:flex;"><span>   ┌────────────────────────┐      reads WAL
</span></span><span style="display:flex;"><span>   │  PostgreSQL  (startup)  │ ───────────────┐
</span></span><span style="display:flex;"><span>   └────────────────────────┘                 │
</span></span><span style="display:flex;"><span>                │                              ▼
</span></span><span style="display:flex;"><span>                │ replays records      ┌───────────────┐
</span></span><span style="display:flex;"><span>                │ onto data files      │   WAL (pg_wal) │
</span></span><span style="display:flex;"><span>                ▼                       └───────┬───────┘
</span></span><span style="display:flex;"><span>   ┌────────────────────────┐                  │
</span></span><span style="display:flex;"><span>   │   data files on DISK    │ ◄────────────────┘
</span></span><span style="display:flex;"><span>   └───────────┬────────────┘
</span></span><span style="display:flex;"><span>               │  if DISK is slow/failing,
</span></span><span style="display:flex;"><span>               ▼  replay crawls → &#34;starting up&#34; forever
</span></span><span style="display:flex;"><span>        clients see: FATAL: starting up
</span></span></code></pre></div><p>WAL replay is only as fast as the disk it&rsquo;s writing to. The storage hiccup hadn&rsquo;t <em>killed</em> the disk — it had wounded it. Every read was retrying, every write was waiting. Recovery was real, honest, and effectively never going to end on that hardware.</p>
<p>The cardinal sin would&rsquo;ve been to <code>kill -9</code> the startup process to &ldquo;speed things up.&rdquo; Kill recovery mid-replay and you risk a data directory that&rsquo;s half-applied and fully useless.</p>
<figure style="text-align:center;margin:34px 0">
<svg viewBox="0 0 560 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="WAL replay throttled by a failing disk">
  <rect x="0" y="0" width="560" height="220" rx="10" fill="#11111b"/>
  <rect x="36" y="42" width="150" height="64" rx="8" fill="#1e1e2e" stroke="#45475a"/>
  <text x="111" y="70" fill="#cdd6f4" font-family="monospace" font-size="13" text-anchor="middle">Postgres</text>
  <text x="111" y="90" fill="#fab387" font-family="monospace" font-size="12" text-anchor="middle">(startup)</text>
  <rect x="212" y="42" width="130" height="64" rx="8" fill="#1e1e2e" stroke="#45475a"/>
  <text x="277" y="70" fill="#cdd6f4" font-family="monospace" font-size="13" text-anchor="middle">WAL</text>
  <text x="277" y="90" fill="#6c7086" font-family="monospace" font-size="11" text-anchor="middle">redo log</text>
  <rect x="368" y="42" width="150" height="64" rx="8" fill="#1e1e2e" stroke="#f38ba8"/>
  <text x="443" y="70" fill="#f38ba8" font-family="monospace" font-size="13" text-anchor="middle">DISK</text>
  <text x="443" y="90" fill="#f38ba8" font-family="monospace" font-size="11" text-anchor="middle">slow / failing</text>
  <line x1="186" y1="74" x2="208" y2="74" stroke="#89b4fa" stroke-width="2"/>
  <polygon points="208,74 200,70 200,78" fill="#89b4fa"/>
  <line x1="342" y1="74" x2="364" y2="74" stroke="#f38ba8" stroke-width="2" stroke-dasharray="5 4"/>
  <polygon points="364,74 356,70 356,78" fill="#f38ba8"/>
  <text x="280" y="150" fill="#a6e3a1" font-family="monospace" font-size="13" text-anchor="middle">replay is only as fast as the disk underneath it</text>
  <text x="280" y="178" fill="#6c7086" font-family="monospace" font-size="12" text-anchor="middle">wounded disk  ⇒  recovery crawls  ⇒  "starting up" forever</text>
</svg>
<figcaption style="color:#6c7086;font-size:14px;margin-top:8px">Recovery wasn't stuck — it was throttled by the storage it was trying to heal.</figcaption>
</figure>
<h2 id="the-fix">The fix</h2>
<p>Rule one of database recovery: <strong>a database cannot recover on a broken disk.</strong> Fix the floor before you ask anyone to dance on it.</p>
<p>So I stopped Postgres cleanly, took the storage out of the equation, and verified the hardware first.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># 1. Stop the instance cleanly — let it park, don&#39;t kill -9</span>
</span></span><span style="display:flex;"><span>sudo systemctl stop postgresql
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 2. Check the storage BEFORE touching the database</span>
</span></span><span style="display:flex;"><span>sudo smartctl -a /dev/sda | grep -iE <span style="color:#f1fa8c">&#39;reallocated|pending|health&#39;</span>
</span></span><span style="display:flex;"><span>dmesg --ctime | grep -iE <span style="color:#f1fa8c">&#39;i/o error|ata|reset&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 3. Move the data dir onto healthy storage (rsync, preserve everything)</span>
</span></span><span style="display:flex;"><span>sudo rsync -aHAX --info<span style="color:#ff79c6">=</span>progress2 /var/lib/pgsql/data/ /srv/pgdata-healthy/
</span></span></code></pre></div><p>With the data directory living on a disk that wasn&rsquo;t actively dying, I pointed Postgres at it and let recovery run — <em>and left it alone.</em></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo -u postgres /usr/bin/postgres -D /srv/pgdata-healthy &amp;
</span></span><span style="display:flex;"><span>sudo tail -f /srv/pgdata-healthy/log/postgresql-*.log
</span></span></code></pre></div><p>This time the LSN sprinted instead of crawled:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>LOG:  redo in progress, elapsed time: 9.21 s, current LSN: 3F/F1A40020
</span></span><span style="display:flex;"><span>LOG:  redo done at 3F/F1A40020
</span></span><span style="display:flex;"><span>LOG:  database system is ready to accept connections
</span></span></code></pre></div><p>Twelve seconds of replay on good hardware versus an eternity on bad. Same WAL. Same database. Different floor.</p>
<p>And because I never fully trust a data directory that&rsquo;s been through a disk event, the first thing I did once it was up was take a logical backup — the kind that doesn&rsquo;t care about block-level corruption:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>pg_dump -Fc -d appdb -f /srv/backups/appdb_<span style="color:#ff79c6">$(</span>date +%F<span style="color:#ff79c6">)</span>.dump
</span></span></code></pre></div><h2 id="why-it-happened">Why it happened</h2>
<p>The unclean shutdown was never the real problem. Postgres handled that <em>exactly</em> as designed — replay the WAL, return to consistency, open the doors.</p>
<p>The problem was that recovery was asked to run on storage that couldn&rsquo;t keep up. WAL replay is I/O, and I/O on a wounded disk is mostly waiting. &ldquo;Starting up forever&rdquo; is what a healthy recovery process looks like when it&rsquo;s standing on broken ground.</p>
<p>Fix the ground. The database knows what to do.</p>
<h2 id="takeaways">Takeaways</h2>
<ul>
<li><strong>&ldquo;Starting up&rdquo; forever almost always means WAL replay</strong> — Postgres is recovering from an unclean stop, not hanging for no reason.</li>
<li><strong>Watch the server log, not the client errors.</strong> <code>redo in progress</code> with a moving LSN means it&rsquo;s working; a stalled LSN means your storage is the suspect.</li>
<li><strong>Fix the storage first.</strong> A database physically cannot finish recovery on a slow or failing disk — verify SMART/<code>dmesg</code>, move to healthy media, <em>then</em> let it replay.</li>
<li><strong>Never <code>kill -9</code> recovery to &ldquo;speed it up.&rdquo;</strong> A half-applied data directory can be worse than no database at all.</li>
<li><strong>Keep logical dumps (<code>pg_dump</code>).</strong> A physical data directory can&rsquo;t always be salvaged after a disk event — a logical backup is the restore path that survives bad blocks.</li>
</ul>
]]></content:encoded></item><item><title>The Sensor That Cried Wolf</title><link>https://errorzap.com/posts/the-sensor-that-cried-wolf/</link><pubDate>Sun, 07 Jun 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/the-sensor-that-cried-wolf/</guid><description>A loud-sound detector paged us all night for slamming doors — because it measured volume, not meaning.</description><content:encoded><![CDATA[<figure style="text-align:center;margin:0 0 30px"><img src="hero.png" alt="The Sensor That Cried Wolf" style="max-width:520px;width:100%;border-radius:14px"/></figure>
<p>The page came in at 2:14 a.m. Then 2:16. Then 2:19.</p>
<p>By the time I&rsquo;d wiped the sleep out of my eyes there were forty-one alerts in the channel, all from the same audio detector at a customer site. &ldquo;LOUD EVENT DETECTED.&rdquo; Forty-one of them. My first thought was that someone was kicking the building in.</p>
<p>Nobody was kicking the building. Someone closed a heavy door. Then, hours later, a kitchen dropped a sheet pan on a tile floor. Each one lit up the detector like a fireworks finale.</p>
<p>This is the story of a sensor that screamed at everything and meant nothing.</p>
<h2 id="the-investigation">The investigation</h2>
<p>The setup was simple on paper. A mic feeds audio into a small model that&rsquo;s supposed to flag impact-type events — glass breaks, hard slams, the kind of thing you actually want a human to look at. It fires a webhook, and the webhook pages whoever&rsquo;s on call.</p>
<p>In practice it fired on <em>anything</em> loud.</p>
<p>I pulled the trigger log and lined it up against what people swore actually happened that night.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>┌──────────────────────────────────────────────────────────┐
</span></span><span style="display:flex;"><span>│  TRIGGER LOG  vs  GROUND TRUTH                            │
</span></span><span style="display:flex;"><span>├──────────────┬───────────────┬───────────────┬───────────┤
</span></span><span style="display:flex;"><span>│  time        │ detector says │ peak level    │ reality   │
</span></span><span style="display:flex;"><span>├──────────────┼───────────────┼───────────────┼───────────┤
</span></span><span style="display:flex;"><span>│  02:14:07    │ ► IMPACT      │  ▓▓▓▓▓▓▓ 0.91  │ door slam ✗│
</span></span><span style="display:flex;"><span>│  02:16:55    │ ► IMPACT      │  ▓▓▓▓▓▓  0.88  │ door slam ✗│
</span></span><span style="display:flex;"><span>│  02:19:31    │ ► IMPACT      │  ▓▓▓▓▓▓▓ 0.93  │ dropped pan✗│
</span></span><span style="display:flex;"><span>│  03:48:12    │ ► IMPACT      │  ▓▓▓▓▓   0.81  │ HVAC bang ✗│
</span></span><span style="display:flex;"><span>│  (silent)    │   ——          │  ▓▓      0.34  │ real break✗│
</span></span><span style="display:flex;"><span>└──────────────┴───────────────┴───────────────┴───────────┘
</span></span><span style="display:flex;"><span>        every loud thing fired ── the one real event didn&#39;t
</span></span></code></pre></div><p>That last row is the one that made my stomach drop. There <em>had</em> been a genuine event earlier in the week — a real break — and it was quieter than a slammed door. The detector had ignored it.</p>
<h2 id="the-aha">The &ldquo;aha&rdquo;</h2>
<p>So I read the actual trigger code. And there it was, in one branch:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#ff79c6">if</span> peak_amplitude <span style="color:#ff79c6">&gt;</span> THRESHOLD:
</span></span><span style="display:flex;"><span>    fire_alert(<span style="color:#f1fa8c">&#34;IMPACT&#34;</span>)
</span></span></code></pre></div><p>That&rsquo;s it. That&rsquo;s the whole brain.</p>
<p>The model attached to this thing was a perfectly good audio classifier. It returned a predicted label and a confidence score for every clip — <code>door</code>, <code>speech</code>, <code>glass_break</code>, <code>hvac</code>, the works. And the trigger logic threw all of it away and looked at one number: <strong>was it loud?</strong></p>
<p>Loudness is not meaning. A slammed door is loud and boring. A real break can be quiet and important. We&rsquo;d built a smoke detector that goes off when you turn the lights on.</p>
<figure style="text-align:center;margin:34px 0">
<svg viewBox="0 0 560 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Amplitude-only versus label-plus-confidence detection">
<rect x="0" y="0" width="560" height="220" rx="10" fill="#1e1e2e"/>
<text x="280" y="30" text-anchor="middle" fill="#cdd6f4" font-family="monospace" font-size="15">loudness ≠ meaning</text>
<rect x="28" y="52" width="230" height="138" rx="8" fill="#11111b" stroke="#45475a"/>
<text x="143" y="76" text-anchor="middle" fill="#f38ba8" font-family="monospace" font-size="13">AMPLITUDE ONLY</text>
<rect x="58" y="96" width="22" height="64" fill="#f38ba8"/>
<rect x="92" y="120" width="22" height="40" fill="#45475a"/>
<rect x="126" y="104" width="22" height="56" fill="#f38ba8"/>
<rect x="160" y="132" width="22" height="28" fill="#45475a"/>
<rect x="194" y="100" width="22" height="60" fill="#f38ba8"/>
<text x="143" y="178" text-anchor="middle" fill="#6c7086" font-family="monospace" font-size="11">fires on everything ✗</text>
<rect x="302" y="52" width="230" height="138" rx="8" fill="#11111b" stroke="#45475a"/>
<text x="417" y="76" text-anchor="middle" fill="#a6e3a1" font-family="monospace" font-size="13">LABEL + CONFIDENCE</text>
<rect x="332" y="96" width="22" height="64" fill="#313244"/>
<rect x="366" y="120" width="22" height="40" fill="#313244"/>
<rect x="400" y="104" width="22" height="56" fill="#a6e3a1"/>
<rect x="434" y="132" width="22" height="28" fill="#313244"/>
<rect x="468" y="100" width="22" height="60" fill="#313244"/>
<text x="405" y="92" fill="#a6e3a1" font-family="monospace" font-size="14">✓</text>
<text x="417" y="178" text-anchor="middle" fill="#6c7086" font-family="monospace" font-size="11">fires on the right one ✓</text>
</svg>
<figcaption style="color:#6c7086;font-size:14px;margin-top:8px">Same five sounds. Volume flags all of them; label + confidence flags the one that matters.</figcaption>
</figure>
<h2 id="the-fix">The fix</h2>
<p>The model already knew what it was hearing. We just had to <em>listen to the label</em> instead of the volume — then gate on confidence and stop it from machine-gunning the same event.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>ALERT_LABELS <span style="color:#ff79c6">=</span> {<span style="color:#f1fa8c">&#34;glass_break&#34;</span>, <span style="color:#f1fa8c">&#34;impact&#34;</span>, <span style="color:#f1fa8c">&#34;alarm&#34;</span>}
</span></span><span style="display:flex;"><span>MIN_CONFIDENCE <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">0.82</span>
</span></span><span style="display:flex;"><span>DEBOUNCE_SECONDS <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">30</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>_last_fire <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">0.0</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">def</span> <span style="color:#50fa7b">handle</span>(clip):
</span></span><span style="display:flex;"><span>    label, confidence <span style="color:#ff79c6">=</span> classifier<span style="color:#ff79c6">.</span>predict(clip)   <span style="color:#6272a4"># what was it, how sure</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#6272a4"># 1) classify by LABEL, not loudness</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">if</span> label <span style="color:#ff79c6">not</span> <span style="color:#ff79c6">in</span> ALERT_LABELS:
</span></span><span style="display:flex;"><span>        <span style="color:#ff79c6">return</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#6272a4"># 2) confidence gate</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">if</span> confidence <span style="color:#ff79c6">&lt;</span> MIN_CONFIDENCE:
</span></span><span style="display:flex;"><span>        <span style="color:#ff79c6">return</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#6272a4"># 3) debounce repeats within a window</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">global</span> _last_fire
</span></span><span style="display:flex;"><span>    now <span style="color:#ff79c6">=</span> time<span style="color:#ff79c6">.</span>monotonic()
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">if</span> now <span style="color:#ff79c6">-</span> _last_fire <span style="color:#ff79c6">&lt;</span> DEBOUNCE_SECONDS:
</span></span><span style="display:flex;"><span>        <span style="color:#ff79c6">return</span>
</span></span><span style="display:flex;"><span>    _last_fire <span style="color:#ff79c6">=</span> now
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    fire_alert(label, confidence)
</span></span></code></pre></div><p>Then — and this is the part people skip — I refused to trust it until I&rsquo;d measured it. We had that hand-labeled log from the noisy night, so I scored the new logic against ground truth before letting it page a single human again.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># replay the labeled clips through the new gate, compare to truth</span>
</span></span><span style="display:flex;"><span>python eval_detector.py <span style="color:#f1fa8c">\
</span></span></span><span style="display:flex;"><span>    --clips ./ground_truth/clips/ <span style="color:#f1fa8c">\
</span></span></span><span style="display:flex;"><span>    --labels ./ground_truth/truth.csv <span style="color:#f1fa8c">\
</span></span></span><span style="display:flex;"><span>    --report precision_recall
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># precision  0.94   (almost no false alarms)</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># recall     0.88   (catches the real ones, including the quiet break)</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># false-positives/night:  41  -&gt;  1</span>
</span></span></code></pre></div><p>Forty-one false alarms a night down to one. The quiet break? Caught.</p>
<h2 id="why-it-happened">Why it happened</h2>
<p>The amplitude check was the <em>first</em> thing someone wrote — a five-minute proof of concept to confirm the mic and the webhook worked end to end. It worked. It shipped. The classifier got bolted on later for &ldquo;labeling,&rdquo; but nobody ever rewired the trigger to actually use it.</p>
<p>So the smart part rode along as a passenger while the dumbest possible heuristic drove. Classic. The proof of concept becomes production because it never visibly breaks — until 2 a.m. on a night with a heavy door.</p>
<h2 id="takeaways">Takeaways</h2>
<ul>
<li><strong>Loudness is not meaning.</strong> Amplitude tells you <em>something happened</em>, not <em>what</em>. If your model predicts a label, trigger on the label.</li>
<li><strong>Gate on confidence.</strong> A bare classification with no threshold is just a louder guess. Make the model commit before it pages a human.</li>
<li><strong>Debounce.</strong> One physical event becomes many samples. Collapse repeats inside a window or you&rsquo;ll get a 41-message wall.</li>
<li><strong>Validate against ground truth before you trust it.</strong> Keep a hand-labeled event log and measure precision/recall. &ldquo;Seems better&rdquo; is not a number.</li>
<li><strong>Audit the trigger path, not just the model.</strong> A great classifier is worthless if a five-minute <code>if loud:</code> is still the thing pulling the trigger.</li>
</ul>
]]></content:encoded></item><item><title>Why Your Fitness Tracker's Correlations Are Lying to You</title><link>https://errorzap.com/posts/why-your-fitness-trackers-correlations-are-lying/</link><pubDate>Sat, 06 Jun 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/why-your-fitness-trackers-correlations-are-lying/</guid><description>I built a dashboard that confidently told me walking ruins my sleep — then I learned what a confounder is.</description><content:encoded><![CDATA[<figure style="text-align:center;margin:0 0 30px"><img src="hero.png" alt="Why Your Fitness Tracker's Correlations Are Lying to You" style="max-width:520px;width:100%;border-radius:14px"/></figure>
<p>It was a Friday night and my brand-new health dashboard had an opinion.</p>
<p>In a tidy little card, glowing red, it announced: <strong>&ldquo;More steps strongly correlates with WORSE sleep (r = -0.61).&rdquo;</strong></p>
<p>I stared at it. So the cure for insomnia is&hellip; sitting still? Cool. Great. I&rsquo;d just spent a weekend wiring up Postgres, a correlation engine, and a slick dark UI to discover that exercise is bad for you.</p>
<p>The number was real. The correlation was real. The conclusion was complete garbage. And it took me an embarrassingly long evening to figure out why.</p>
<h2 id="the-investigation">The investigation</h2>
<p>The setup was simple. Four metrics per day — steps, sleep hours, resting heart rate, weight — pulled from the tracker&rsquo;s export into a table. The engine did the obvious thing: loop over every pair of columns, run a Pearson correlation, sort by absolute value, surface the &ldquo;strongest findings.&rdquo;</p>
<p>First I assumed a bug. Sign flipped somewhere, units crossed, a join gone sideways. I pulled the raw rows.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> <span style="color:#ff79c6">day</span>, steps, sleep_hours, resting_hr
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">FROM</span> health_daily
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">ORDER</span> <span style="color:#ff79c6">BY</span> <span style="color:#ff79c6">day</span> <span style="color:#ff79c6">DESC</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">LIMIT</span> <span style="color:#bd93f9">10</span>;
</span></span></code></pre></div><p>The data was fine. High-step days really did have less sleep. The math wasn&rsquo;t lying. The math was just answering a dumber question than I thought I&rsquo;d asked.</p>
<p>Then I noticed the pattern in the rows. The big-step / bad-sleep days clustered. They were the days I remembered — the chaotic ones. Travel days. Deadline days. The days I was on my feet <em>because</em> everything was on fire.</p>
<h2 id="the-aha">The &ldquo;aha&rdquo;</h2>
<p>There was a third variable sitting in the middle of the whole thing, and I&rsquo;d never measured it: <strong>a stressful day.</strong></p>
<p>A busy, stressful day makes me walk more (running around, pacing, errands) AND sleep worse (wired, late, anxious). Steps and sleep don&rsquo;t touch each other. They&rsquo;re both just <em>symptoms</em> of the same hidden cause.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>                  ┌──────────────────────┐
</span></span><span style="display:flex;"><span>                  │   STRESSFUL DAY      │   &lt;- the thing I never logged
</span></span><span style="display:flex;"><span>                  │   (the confounder)   │
</span></span><span style="display:flex;"><span>                  └─────────┬────────────┘
</span></span><span style="display:flex;"><span>                            │
</span></span><span style="display:flex;"><span>                ┌───────────┴───────────┐
</span></span><span style="display:flex;"><span>                ▼                       ▼
</span></span><span style="display:flex;"><span>         ┌────────────┐         ┌──────────────┐
</span></span><span style="display:flex;"><span>         │  + STEPS   │  ?????  │  - SLEEP     │
</span></span><span style="display:flex;"><span>         └────────────┘ &lt;-----&gt; └──────────────┘
</span></span><span style="display:flex;"><span>            no actual arrow between these two
</span></span></code></pre></div><p>The engine drew the dotted line at the bottom and called it a discovery. It had no idea the box at the top even existed.</p>
<p>And it got worse. With only a few weeks of data, every correlation was riding on a tiny sample — a couple of weird days could swing <code>r</code> wildly. On top of that I was testing <em>every pair</em>: 4 metrics is 6 comparisons, and the more pairs you test, the more likely pure noise hands you a juicy-looking number. Test enough things and &ldquo;significant&rdquo; findings appear for free.</p>
<p>Confounders, tiny samples, and many comparisons. Three different ways to manufacture confident nonsense, all firing at once.</p>
<figure style="text-align:center;margin:34px 0">
<svg viewBox="0 0 560 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="A confounder driving two correlated symptoms">
<rect x="0" y="0" width="560" height="220" fill="#11111b"/>
<rect x="205" y="20" width="150" height="48" rx="8" fill="#1e1e2e" stroke="#cba6f7" stroke-width="2"/>
<text x="280" y="42" fill="#cba6f7" font-family="monospace" font-size="14" text-anchor="middle">STRESSFUL DAY</text>
<text x="280" y="60" fill="#6c7086" font-family="monospace" font-size="11" text-anchor="middle">(hidden cause)</text>
<line x1="240" y1="68" x2="120" y2="135" stroke="#fab387" stroke-width="2"/>
<line x1="320" y1="68" x2="440" y2="135" stroke="#fab387" stroke-width="2"/>
<rect x="45" y="138" width="150" height="48" rx="8" fill="#1e1e2e" stroke="#a6e3a1" stroke-width="2"/>
<text x="120" y="167" fill="#a6e3a1" font-family="monospace" font-size="14" text-anchor="middle">+ STEPS</text>
<rect x="365" y="138" width="150" height="48" rx="8" fill="#1e1e2e" stroke="#89b4fa" stroke-width="2"/>
<text x="440" y="167" fill="#89b4fa" font-family="monospace" font-size="14" text-anchor="middle">- SLEEP</text>
<line x1="195" y1="162" x2="365" y2="162" stroke="#f38ba8" stroke-width="2" stroke-dasharray="6 6"/>
<text x="280" y="153" fill="#f38ba8" font-family="monospace" font-size="12" text-anchor="middle">spurious r</text>
<text x="280" y="205" fill="#6c7086" font-family="monospace" font-size="11" text-anchor="middle">solid = real cause & dashed = the lie the engine reported</text>
</svg>
<figcaption style="color:#6c7086;font-size:14px;margin-top:8px">The engine saw the red dotted line. It never saw the mauve box.</figcaption>
</figure>
<h2 id="the-fix">The fix</h2>
<p>I stopped trusting the engine and started guarding it. Four changes.</p>
<p><strong>1. Require a real sample size.</strong> No pair gets reported under a floor.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>MIN_N <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">21</span>  <span style="color:#6272a4"># three weeks minimum before we say a word</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">if</span> pair<span style="color:#ff79c6">.</span>n <span style="color:#ff79c6">&lt;</span> MIN_N:
</span></span><span style="display:flex;"><span>    pair<span style="color:#ff79c6">.</span>verdict <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#34;insufficient_data&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">continue</span>
</span></span></code></pre></div><p><strong>2. Correct for multiple comparisons.</strong> If you test 6 pairs, a raw p &lt; 0.05 means almost nothing. Hold them all to a stricter bar.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#ff79c6">from</span> statsmodels.stats.multitest <span style="color:#ff79c6">import</span> multipletests
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>pvals <span style="color:#ff79c6">=</span> [p<span style="color:#ff79c6">.</span>pvalue <span style="color:#ff79c6">for</span> p <span style="color:#ff79c6">in</span> pairs]
</span></span><span style="display:flex;"><span>reject, p_adj, _, _ <span style="color:#ff79c6">=</span> multipletests(pvals, alpha<span style="color:#ff79c6">=</span><span style="color:#bd93f9">0.05</span>, method<span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;holm&#34;</span>)
</span></span></code></pre></div><p><strong>3. Control for the obvious third variable.</strong> Partial correlation: hold the suspected confounder fixed and see if anything survives.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#ff79c6">import</span> pingouin <span style="color:#ff79c6">as</span> pg
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># does steps-vs-sleep survive once we account for a stress proxy?</span>
</span></span><span style="display:flex;"><span>result <span style="color:#ff79c6">=</span> pg<span style="color:#ff79c6">.</span>partial_corr(
</span></span><span style="display:flex;"><span>    data<span style="color:#ff79c6">=</span>df, x<span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;steps&#34;</span>, y<span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;sleep_hours&#34;</span>, covar<span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;stress_proxy&#34;</span>
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># the -0.61 collapsed toward zero once stress was held constant</span>
</span></span></code></pre></div><p><strong>4. Relabel the output.</strong> The card no longer says &ldquo;correlates with.&rdquo; It says <strong>&ldquo;possible link — investigate.&rdquo;</strong> Every finding ships as a hypothesis, never a verdict.</p>
<p>The steps/sleep &ldquo;finding&rdquo; failed three of the four gates and got demoted to a quiet maybe. Exactly where it belonged.</p>
<h2 id="why-it-happened">Why it happened</h2>
<p>Because a correlation engine is a confidence machine with no judgment. It will faithfully compute a number for any two columns you hand it and present that number with the same authority whether it&rsquo;s bedrock truth or coincidence between two symptoms of a Tuesday from hell.</p>
<p>The math was never wrong. My question was. &ldquo;Are these two columns correlated?&rdquo; is trivial. &ldquo;Does one <em>affect</em> the other?&rdquo; is a completely different question the engine was never equipped to answer — and I let the pretty red card pretend it had.</p>
<h2 id="takeaways">Takeaways</h2>
<ul>
<li><strong>Correlation engines on personal data manufacture convincing nonsense.</strong> A real <code>r</code> with a fake meaning looks identical to a true insight on a dashboard.</li>
<li><strong>Hunt the confounder first.</strong> If two things correlate, ask what unmeasured third thing could be driving both before you believe either causes the other.</li>
<li><strong>Tiny samples lie loudly.</strong> A handful of weird days can swing a correlation hard. Set a minimum-N floor and enforce it in code.</li>
<li><strong>Testing many pairs guarantees false hits.</strong> Correct for multiple comparisons (Holm, Bonferroni, FDR) or you&rsquo;ll &ldquo;discover&rdquo; links that are pure noise.</li>
<li><strong>Ship correlations as hypotheses, never conclusions.</strong> Label the output &ldquo;investigate,&rdquo; not &ldquo;proven.&rdquo; Your dashboard&rsquo;s job is to point, not to swear.</li>
</ul>
]]></content:encoded></item><item><title>The App That Wouldn't Open After an Update</title><link>https://errorzap.com/posts/the-app-that-wouldnt-open-after-an-update/</link><pubDate>Fri, 05 Jun 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/the-app-that-wouldnt-open-after-an-update/</guid><description>A desktop app auto-updated, swallowed its own window, and hung forever — and the fix had nothing to do with the install.</description><content:encoded><![CDATA[<figure style="text-align:center;margin:0 0 30px"><img src="hero.png" alt="The App That Wouldn't Open After an Update" style="max-width:520px;width:100%;border-radius:14px"/></figure>
<p>A user pings me: &ldquo;The app won&rsquo;t open anymore.&rdquo;</p>
<p>Classic. I ask the usual: did anything change? &ldquo;It updated this morning.&rdquo;</p>
<p>Of course it did.</p>
<p>I remote in and double-click the icon. The cursor does the little spinny thing. The process shows up in Task Manager — CPU ticking, memory allocated, the whole act. But no window. Ever. It just sits there, alive and useless, like a server that boots to a blinking cursor and stops caring.</p>
<p>Kill it. Relaunch. Same thing. Process up, window absent. Reboot the machine. Same thing. Reinstall the app clean. <strong>Same. Thing.</strong></p>
<p>That&rsquo;s the part that bugged me. A fresh install hanging the exact same way means the problem isn&rsquo;t the binary. The binary is fine. Something <em>outside</em> the binary is poisoning it.</p>
<h2 id="the-investigation">The investigation</h2>
<p>When a process starts but never paints a window, it&rsquo;s usually stuck waiting on something — a lock, a handle, a chunk of state it expects to load. So I stopped staring at the app and started watching what it <em>touched</em>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>  Launch ───► Process starts ───► loads cached runtime bundles
</span></span><span style="display:flex;"><span>                                         │
</span></span><span style="display:flex;"><span>                                   ┌─────┴──────┐
</span></span><span style="display:flex;"><span>                                   ▼            ▼
</span></span><span style="display:flex;"><span>                            new v2 binary   OLD v1 cache  ✗
</span></span><span style="display:flex;"><span>                                   │            │
</span></span><span style="display:flex;"><span>                                   └─────┬──────┘
</span></span><span style="display:flex;"><span>                                         ▼
</span></span><span style="display:flex;"><span>                              version mismatch ► hang
</span></span><span style="display:flex;"><span>                                         ▼
</span></span><span style="display:flex;"><span>                                   ✗ no window
</span></span></code></pre></div><p>There it was. The updater swapped the binary to v2 but left a directory of cached runtime bundles from v1 sitting right where the new build looks first. New code, stale cache, two versions trying to share one brain. The app loads the old bundles, chokes on the mismatch, and waits forever for a handshake that&rsquo;s never coming.</p>
<p>Reinstalling didn&rsquo;t fix it because reinstalling doesn&rsquo;t <em>clear that cache</em>. The installer drops a fresh binary and walks away. The landmine stays armed.</p>
<figure style="text-align:center;margin:34px 0">
<svg viewBox="0 0 560 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="New binary loading stale cache and hanging">
<rect x="0" y="0" width="560" height="220" rx="10" fill="#11111b"/>
<rect x="40" y="70" width="150" height="80" rx="10" fill="#1e1e2e" stroke="#a6e3a1" stroke-width="2"/>
<text x="115" y="105" fill="#a6e3a1" font-family="monospace" font-size="15" text-anchor="middle">v2 binary</text>
<text x="115" y="128" fill="#6c7086" font-family="monospace" font-size="12" text-anchor="middle">fresh install ✓</text>
<rect x="370" y="70" width="150" height="80" rx="10" fill="#1e1e2e" stroke="#f38ba8" stroke-width="2"/>
<text x="445" y="105" fill="#f38ba8" font-family="monospace" font-size="15" text-anchor="middle">v1 cache</text>
<text x="445" y="128" fill="#6c7086" font-family="monospace" font-size="12" text-anchor="middle">stale bundles ✗</text>
<line x1="190" y1="110" x2="365" y2="110" stroke="#fab387" stroke-width="2" stroke-dasharray="6 5"/>
<polygon points="365,110 353,104 353,116" fill="#fab387"/>
<text x="278" y="98" fill="#fab387" font-family="monospace" font-size="13" text-anchor="middle">loads first</text>
<text x="278" y="185" fill="#cba6f7" font-family="monospace" font-size="14" text-anchor="middle">mismatch ‣ window never paints</text>
</svg>
<figcaption style="color:#6c7086;font-size:14px;margin-top:8px">The new binary is innocent — it's the leftover cache that hangs it.</figcaption>
</figure>
<h2 id="the-aha">The aha</h2>
<p>Here&rsquo;s the tell that saved me: I deleted that cache directory and the app <em>limped</em> to life. Window appeared. It ran. Notably, clearing the cache needed <strong>no admin rights</strong> — it lives in the user profile, not Program Files. So this isn&rsquo;t a &ldquo;corrupt install, nuke from orbit&rdquo; problem. It&rsquo;s a &ldquo;stale state&rdquo; problem.</p>
<p>But &ldquo;limped&rdquo; is the operative word. Deleting the cache once gets you a window, but the app&rsquo;s background helper service was still holding the old runtime state in memory. It&rsquo;d rebuild the cache half-correctly and get flaky again. The durable fix was to make the helper rebuild from scratch.</p>
<h2 id="the-fix">The fix</h2>
<p>Order matters. Kill everything first, <em>then</em> clear the cache, <em>then</em> restart the helper so it regenerates clean state, <em>then</em> launch.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-powershell" data-lang="powershell"><span style="display:flex;"><span><span style="color:#6272a4"># 1. Kill every process the app owns — no orphans holding the cache open</span>
</span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">Get-Process</span> -Name <span style="color:#f1fa8c">&#34;TheApp*&#34;</span> -ErrorAction SilentlyContinue | <span style="color:#8be9fd;font-style:italic">Stop-Process</span> -Force
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 2. Delete the stale runtime/bundle cache (user profile — no admin needed)</span>
</span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">Remove-Item</span> -Recurse -Force <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$env:LOCALAPPDATA</span><span style="color:#f1fa8c">\TheApp\Cache\bundles&#34;</span> -ErrorAction SilentlyContinue
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 3. Restart the background helper service so it rebuilds state fresh</span>
</span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">Restart-Service</span> -Name <span style="color:#f1fa8c">&#34;TheAppHelper&#34;</span> -Force
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 4. Confirm exactly one helper is up before relaunching</span>
</span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">Get-Service</span> -Name <span style="color:#f1fa8c">&#34;TheAppHelper&#34;</span>
</span></span></code></pre></div><p>Then launch the app normally. Window paints, app stays up across reboots. Done.</p>
<p>If you skip step 1 and a stray process still has a handle on the cache dir, the delete silently no-ops and you&rsquo;re right back where you started — wondering why a &ldquo;fix&rdquo; did nothing. Clear the orphans first, every time.</p>
<h2 id="why-it-happened">Why it happened</h2>
<p>Auto-updaters are optimists. They assume the new binary and the old cached state are compatible, so they swap the executable and leave the rest in place to &ldquo;save time.&rdquo; Most of the time that&rsquo;s fine. When the runtime bundle format changes between versions, it absolutely is not — and the app has no graceful path for &ldquo;my cache is from the future&rsquo;s past,&rdquo; so it just hangs.</p>
<p>Reinstalling feels like the big hammer, but it only touches the binary. The poison was always in the user-profile cache the installer never looks at.</p>
<h2 id="takeaways">Takeaways</h2>
<ul>
<li><strong>A hang right after an auto-update is stale state, not a corrupt install.</strong> Clear the cache and restart the helper <em>before</em> you reinstall.</li>
<li><strong>A reinstall that changes nothing is a clue, not a dead end</strong> — it means the problem lives outside the install footprint.</li>
<li><strong>Cache/runtime dirs usually sit in the user profile</strong> — clearing them needs no admin, so try the cheap fix first.</li>
<li><strong>Kill orphan processes before deleting their files.</strong> A held handle turns your fix into a no-op and wastes an hour.</li>
<li><strong>Restart the background service, don&rsquo;t just delete and pray.</strong> Deleting the cache gets a window; restarting the helper makes it <em>stay</em>.</li>
</ul>
]]></content:encoded></item><item><title>Teaching a Computer to Mute the Sounds I Hate</title><link>https://errorzap.com/posts/teaching-a-computer-to-mute-the-sounds-i-hate/</link><pubDate>Thu, 04 Jun 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/teaching-a-computer-to-mute-the-sounds-i-hate/</guid><description>I tried to delete a sound from my life with a volume threshold. The sound was never loud — it was a fingerprint, and I had to teach a machine to read it.</description><content:encoded><![CDATA[<p>There is a specific sound. A mouth click. A wet little lip-smack between words. A sharp inhale before a sentence.</p>
<p>You probably don&rsquo;t hear them. I hear nothing else.</p>
<p>Misophonia means certain sounds don&rsquo;t annoy me — they hijack me. So I did what any sysadmin does with a problem that won&rsquo;t leave: I decided to make it someone else&rsquo;s job. Specifically, a script&rsquo;s. The pitch was simple. Detect the trigger sounds in any audio or video, scrub them out before playback, hand me back a clean track.</p>
<p>The pitch was simple. The first version was a disaster.</p>
<h2 id="the-naive-approach">The naive approach</h2>
<p>My first instinct was the dumbest possible one, which is usually where I start.</p>
<p>Triggers feel violent, so I assumed they were loud. Threshold the loudness, mute anything that spikes, done by lunch.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#6272a4"># v1: the volume-gate theory of everything</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">import</span> numpy <span style="color:#ff79c6">as</span> np
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">def</span> <span style="color:#50fa7b">scrub_naive</span>(samples, sr, thresh_db<span style="color:#ff79c6">=-</span><span style="color:#bd93f9">18.0</span>):
</span></span><span style="display:flex;"><span>    frame <span style="color:#ff79c6">=</span> <span style="color:#8be9fd;font-style:italic">int</span>(<span style="color:#bd93f9">0.02</span> <span style="color:#ff79c6">*</span> sr)               <span style="color:#6272a4"># 20ms frames</span>
</span></span><span style="display:flex;"><span>    out <span style="color:#ff79c6">=</span> samples<span style="color:#ff79c6">.</span>copy()
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">for</span> i <span style="color:#ff79c6">in</span> <span style="color:#8be9fd;font-style:italic">range</span>(<span style="color:#bd93f9">0</span>, <span style="color:#8be9fd;font-style:italic">len</span>(samples) <span style="color:#ff79c6">-</span> frame, frame):
</span></span><span style="display:flex;"><span>        chunk <span style="color:#ff79c6">=</span> samples[i:i<span style="color:#ff79c6">+</span>frame]
</span></span><span style="display:flex;"><span>        rms <span style="color:#ff79c6">=</span> np<span style="color:#ff79c6">.</span>sqrt(np<span style="color:#ff79c6">.</span>mean(chunk<span style="color:#ff79c6">**</span><span style="color:#bd93f9">2</span>)) <span style="color:#ff79c6">+</span> <span style="color:#bd93f9">1e-9</span>
</span></span><span style="display:flex;"><span>        db <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">20</span> <span style="color:#ff79c6">*</span> np<span style="color:#ff79c6">.</span>log10(rms)
</span></span><span style="display:flex;"><span>        <span style="color:#ff79c6">if</span> db <span style="color:#ff79c6">&gt;</span> thresh_db:               <span style="color:#6272a4"># &#34;too loud&#34; -&gt; kill it</span>
</span></span><span style="display:flex;"><span>            out[i:i<span style="color:#ff79c6">+</span>frame] <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">0.0</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">return</span> out
</span></span></code></pre></div><p>I ran it on a podcast. It muted the consonants. It muted laughter. It muted entire emphasized words. And the lip-smacks? Sailed right through, untouched, smug.</p>
<p>Because the triggers were never loud. A lip-smack sits <em>below</em> normal speech in raw energy. I&rsquo;d built a machine that deleted the wrong things at the wrong volume for the wrong reason.</p>
<h2 id="the-aha">The aha</h2>
<p>I pulled the waveforms apart and actually looked at them. That&rsquo;s when it clicked.</p>
<p>A vowel is periodic and narrowband — energy stacked in tidy harmonic bands, sustained over time. A mouth click is the opposite animal: short, broadband, transient. A flat smear of energy across the whole spectrum, gone in 30 milliseconds.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>   AMPLITUDE-OVER-TIME  (loudness lies)        SPECTRUM  (signature tells the truth)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>   speech  ┌─────────────┐   click ┌─┐         speech ▁▃█▇▅▂   (harmonic, banded)
</span></span><span style="display:flex;"><span>           │ /\  /\  /\   │        │█│         click  ▅▅▆▅▆▅   (flat, broadband)
</span></span><span style="display:flex;"><span>   ────────┘/  \/  \/  \  └──┐ ────┘ └──        breath ▂▂▃▂▂▃   (broadband, low, hissy)
</span></span><span style="display:flex;"><span>            loud + sustained    quiet + 30ms
</span></span><span style="display:flex;"><span>                  ^ v1 chased THIS, the wrong axis
</span></span></code></pre></div><p>The trigger wasn&rsquo;t a <em>level</em>. It was a <em>shape</em>. A fingerprint smeared across frequency and time. And &ldquo;find this fingerprint in a stream&rdquo; is not a thresholding problem.</p>
<p>It&rsquo;s a classification problem. I&rsquo;d been trying to solve a recognition task with a ruler.</p>
<figure style="text-align:center;margin:34px 0">
<svg viewBox="0 0 560 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Pipeline from audio to features to classifier to clean output">
  <rect x="0" y="0" width="560" height="220" fill="#11111b"/>
  <rect x="24" y="84" width="96" height="52" rx="8" fill="#1e1e2e" stroke="#45475a"/>
  <text x="72" y="108" fill="#cdd6f4" font-family="monospace" font-size="13" text-anchor="middle">audio</text>
  <text x="72" y="124" fill="#6c7086" font-family="monospace" font-size="11" text-anchor="middle">frames</text>
  <rect x="156" y="60" width="120" height="100" rx="8" fill="#1e1e2e" stroke="#45475a"/>
  <text x="216" y="86" fill="#fab387" font-family="monospace" font-size="12" text-anchor="middle">features</text>
  <text x="216" y="106" fill="#cdd6f4" font-family="monospace" font-size="10" text-anchor="middle">spectral flat.</text>
  <text x="216" y="120" fill="#cdd6f4" font-family="monospace" font-size="10" text-anchor="middle">ZCR / onset</text>
  <text x="216" y="134" fill="#cdd6f4" font-family="monospace" font-size="10" text-anchor="middle">MFCC</text>
  <rect x="312" y="60" width="120" height="100" rx="8" fill="#1e1e2e" stroke="#cba6f7"/>
  <text x="372" y="92" fill="#cba6f7" font-family="monospace" font-size="12" text-anchor="middle">classifier</text>
  <text x="372" y="114" fill="#cdd6f4" font-family="monospace" font-size="10" text-anchor="middle">p(trigger)</text>
  <text x="372" y="132" fill="#6c7086" font-family="monospace" font-size="10" text-anchor="middle">+ threshold</text>
  <rect x="468" y="84" width="80" height="52" rx="8" fill="#1e1e2e" stroke="#a6e3a1"/>
  <text x="508" y="108" fill="#a6e3a1" font-family="monospace" font-size="12" text-anchor="middle">clean</text>
  <text x="508" y="124" fill="#6c7086" font-family="monospace" font-size="10" text-anchor="middle">duck & fade</text>
  <line x1="120" y1="110" x2="156" y2="110" stroke="#89b4fa" stroke-width="2"/>
  <line x1="276" y1="110" x2="312" y2="110" stroke="#89b4fa" stroke-width="2"/>
  <line x1="432" y1="110" x2="468" y2="110" stroke="#89b4fa" stroke-width="2"/>
  <polygon points="156,110 148,106 148,114" fill="#89b4fa"/>
  <polygon points="312,110 304,106 304,114" fill="#89b4fa"/>
  <polygon points="468,110 460,106 460,114" fill="#89b4fa"/>
</svg>
<figcaption style="color:#6c7086;font-size:14px;margin-top:8px">Stop measuring how loud it is. Describe what it looks like, then let a classifier decide.</figcaption>
</figure>
<h2 id="the-fix">The fix</h2>
<p>So I threw out the ruler and described the signature instead. Per frame, extract the features that actually separate a click from a vowel — spectral flatness (how broadband it is), zero-crossing rate, onset sharpness, MFCCs. Then a small classifier outputs a probability, and only frames over a confidence threshold get touched.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#ff79c6">import</span> librosa<span style="color:#ff79c6">,</span> numpy <span style="color:#ff79c6">as</span> np
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">def</span> <span style="color:#50fa7b">features</span>(chunk, sr):
</span></span><span style="display:flex;"><span>    S <span style="color:#ff79c6">=</span> np<span style="color:#ff79c6">.</span>abs(librosa<span style="color:#ff79c6">.</span>stft(chunk, n_fft<span style="color:#ff79c6">=</span><span style="color:#bd93f9">512</span>, hop_length<span style="color:#ff79c6">=</span><span style="color:#bd93f9">128</span>))
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">return</span> np<span style="color:#ff79c6">.</span>concatenate([
</span></span><span style="display:flex;"><span>        [librosa<span style="color:#ff79c6">.</span>feature<span style="color:#ff79c6">.</span>spectral_flatness(S<span style="color:#ff79c6">=</span>S)<span style="color:#ff79c6">.</span>mean()],   <span style="color:#6272a4"># broadband?</span>
</span></span><span style="display:flex;"><span>        [librosa<span style="color:#ff79c6">.</span>feature<span style="color:#ff79c6">.</span>zero_crossing_rate(chunk)<span style="color:#ff79c6">.</span>mean()],<span style="color:#6272a4"># transient hiss?</span>
</span></span><span style="display:flex;"><span>        librosa<span style="color:#ff79c6">.</span>feature<span style="color:#ff79c6">.</span>mfcc(y<span style="color:#ff79c6">=</span>chunk, sr<span style="color:#ff79c6">=</span>sr, n_mfcc<span style="color:#ff79c6">=</span><span style="color:#bd93f9">13</span>)<span style="color:#ff79c6">.</span>mean(axis<span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>),
</span></span><span style="display:flex;"><span>    ])
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">def</span> <span style="color:#50fa7b">scrub</span>(samples, sr, clf, p<span style="color:#ff79c6">=</span><span style="color:#bd93f9">0.85</span>, debounce_ms<span style="color:#ff79c6">=</span><span style="color:#bd93f9">40</span>):
</span></span><span style="display:flex;"><span>    frame, out <span style="color:#ff79c6">=</span> <span style="color:#8be9fd;font-style:italic">int</span>(<span style="color:#bd93f9">0.02</span><span style="color:#ff79c6">*</span>sr), samples<span style="color:#ff79c6">.</span>copy()
</span></span><span style="display:flex;"><span>    cooldown, hold <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">0</span>, <span style="color:#8be9fd;font-style:italic">int</span>(debounce_ms<span style="color:#ff79c6">/</span><span style="color:#bd93f9">20</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">for</span> i <span style="color:#ff79c6">in</span> <span style="color:#8be9fd;font-style:italic">range</span>(<span style="color:#bd93f9">0</span>, <span style="color:#8be9fd;font-style:italic">len</span>(samples)<span style="color:#ff79c6">-</span>frame, frame):
</span></span><span style="display:flex;"><span>        prob <span style="color:#ff79c6">=</span> clf<span style="color:#ff79c6">.</span>predict_proba(features(samples[i:i<span style="color:#ff79c6">+</span>frame], sr)[<span style="color:#ff79c6">None</span>])[<span style="color:#bd93f9">0</span>,<span style="color:#bd93f9">1</span>]
</span></span><span style="display:flex;"><span>        <span style="color:#ff79c6">if</span> prob <span style="color:#ff79c6">&gt;=</span> p: cooldown <span style="color:#ff79c6">=</span> hold          <span style="color:#6272a4"># fire + arm debounce</span>
</span></span><span style="display:flex;"><span>        <span style="color:#ff79c6">if</span> cooldown <span style="color:#ff79c6">&gt;</span> <span style="color:#bd93f9">0</span>:                        <span style="color:#6272a4"># short cross-fade, not a hard cut</span>
</span></span><span style="display:flex;"><span>            out[i:i<span style="color:#ff79c6">+</span>frame] <span style="color:#ff79c6">*=</span> np<span style="color:#ff79c6">.</span>linspace(<span style="color:#bd93f9">1</span>, <span style="color:#bd93f9">0</span>, frame) <span style="color:#ff79c6">if</span> cooldown<span style="color:#ff79c6">==</span>hold <span style="color:#ff79c6">else</span> <span style="color:#bd93f9">0.0</span>
</span></span><span style="display:flex;"><span>            cooldown <span style="color:#ff79c6">-=</span> <span style="color:#bd93f9">1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">return</span> out
</span></span></code></pre></div><p>Two details earned their keep. The <strong>confidence threshold</strong> stops it firing on every sibilant <code>s</code>. The <strong>debounce</strong> keeps one click from flickering the gate on and off mid-word. And I duck with a short cross-fade instead of a hard zero, so the cut doesn&rsquo;t <em>itself</em> become a click.</p>
<p>On isolated triggers — a click in a gap, a breath between sentences — it works. Validated against a hand-labeled set, and it catches the things that used to make me leave the room.</p>
<h2 id="why-it-happened">Why it happened</h2>
<p>I anthropomorphized the problem. The sounds <em>feel</em> loud and aggressive to me, so I encoded my emotional read of them as a signal property. The DSP didn&rsquo;t care about my feelings. Loudness and identity are different axes, and I&rsquo;d built my whole v1 on the wrong one.</p>
<p>And I&rsquo;ll be honest about the part that isn&rsquo;t solved: triggers that land <em>on top of</em> speech. When a lip-smack overlaps a spoken word, ducking the frame mangles the word too. Source separation in real time is a genuinely harder problem, and pretending I&rsquo;d nailed it would be the same mistake in a nicer suit. That&rsquo;s phase two. For now the tool is honest about what it can and can&rsquo;t pull apart.</p>
<h2 id="takeaways">Takeaways</h2>
<ul>
<li><strong>&ldquo;Detect this sound&rdquo; is classification, not thresholding.</strong> If you reach for a volume gate, ask whether the thing you want is actually defined by loudness — it usually isn&rsquo;t.</li>
<li><strong>Define the signature in feature space.</strong> Spectral flatness, ZCR, onset, MFCCs separate a broadband transient from a harmonic vowel far better than amplitude ever will.</li>
<li><strong>Validate against labeled examples.</strong> &ldquo;It feels right&rdquo; is not a metric. A confidence threshold is only meaningful when you&rsquo;ve measured it against ground truth.</li>
<li><strong>Debounce and cross-fade your edits.</strong> A hard cut to fix a click can introduce a new click. Smooth the seams.</li>
<li><strong>Be loud about the cases you can&rsquo;t solve.</strong> Isolated triggers: handled. Triggers overlapping speech: not yet, and saying so is the whole point.</li>
</ul>
]]></content:encoded></item><item><title>I Found My Own Server on Shodan</title><link>https://errorzap.com/posts/i-found-my-own-server-on-shodan/</link><pubDate>Wed, 03 Jun 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/i-found-my-own-server-on-shodan/</guid><description>I scanned my own infrastructure like an attacker would — and found an admin panel wide open that my firewall swore was closed.</description><content:encoded><![CDATA[<figure style="text-align:center;margin:0 0 30px"><img src="hero.png" alt="I Found My Own Server on Shodan" style="max-width:520px;width:100%;border-radius:14px"/></figure>
<p>It started as a slow-afternoon habit: scan yourself the way an attacker would. Pull up Shodan, type in your own public IP, and brace.</p>
<p>I expected a clean sheet. UFW was configured. <code>ufw deny</code> on everything that wasn&rsquo;t 80, 443, and the VPN. I&rsquo;d checked it a dozen times. Green across the board.</p>
<p>Shodan disagreed.</p>
<p>There it was, indexed and timestamped: an admin/management panel answering on a port that was supposed to be firewalled into oblivion. Banner, title, the works. Anyone with a browser could&rsquo;ve found it. Some of them probably had.</p>
<h2 id="the-investigation">The investigation</h2>
<p>First reaction: Shodan&rsquo;s cache is stale. It happens. So I scanned from the outside, from a box that had no business reaching anything internal.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>nmap -Pn -p <span style="color:#bd93f9">9000</span> 192.0.2.10 --open   <span style="color:#6272a4"># from a cloud box, not my network</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># (really: nmap -Pn -p 9000 &lt;my-public-ip&gt;)</span>
</span></span><span style="display:flex;"><span>nmap -Pn -p <span style="color:#bd93f9">9000</span> 192.0.2.10
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>PORT     STATE  SERVICE
</span></span><span style="display:flex;"><span>9000/tcp open   http
</span></span></code></pre></div><p>Open. From the public internet. Not stale.</p>
<p>So I SSH&rsquo;d in and asked UFW what it thought it was doing.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo ufw status verbose | grep <span style="color:#bd93f9">9000</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># (nothing — port 9000 is not allowed)</span>
</span></span><span style="display:flex;"><span>sudo ufw status | head
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># Status: active</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 22/tcp   ALLOW   Anywhere</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 443/tcp  ALLOW   Anywhere</span>
</span></span></code></pre></div><p>UFW was adamant: 9000 is closed. The internet was equally adamant: 9000 is open. One of them was lying, and firewalls don&rsquo;t lie. They just get bypassed.</p>
<h2 id="the-aha">The &ldquo;aha&rdquo;</h2>
<p>Then I remembered what was actually listening on 9000.</p>
<p>A Docker container.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker ps --format <span style="color:#f1fa8c">&#39;{{.Names}}\t{{.Ports}}&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># admin-panel   0.0.0.0:9000-&gt;9000/tcp</span>
</span></span></code></pre></div><p><code>0.0.0.0:9000-&gt;9000/tcp</code>. There it is. That <code>-p 9000:9000</code> in the compose file. Docker doesn&rsquo;t ask UFW for permission — it writes its <strong>own</strong> iptables rules, straight into the <code>DOCKER</code> chain, and they get evaluated before your tidy UFW <code>INPUT</code> rules ever run.</p>
<p>Your <code>ufw deny</code> is a polite note on the front door. Docker built a second door around back and propped it open.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>  Inbound packet → :9000
</span></span><span style="display:flex;"><span>        │
</span></span><span style="display:flex;"><span>        ▼
</span></span><span style="display:flex;"><span>  ┌───────────────────────────┐
</span></span><span style="display:flex;"><span>  │ iptables nat/PREROUTING   │
</span></span><span style="display:flex;"><span>  │  DOCKER chain (DNAT)  ◄────┼── Docker put this here.
</span></span><span style="display:flex;"><span>  │  → 172.17.0.2:9000        │     UFW never gets a vote.
</span></span><span style="display:flex;"><span>  └───────────────────────────┘
</span></span><span style="display:flex;"><span>        │            ╲
</span></span><span style="display:flex;"><span>        ▼             ╲ (never reached)
</span></span><span style="display:flex;"><span>   container :9000     ┌────────────────────┐
</span></span><span style="display:flex;"><span>                       │ ufw-user chain     │
</span></span><span style="display:flex;"><span>                       │  DENY 9000  (moot) │
</span></span><span style="display:flex;"><span>                       └────────────────────┘
</span></span></code></pre></div><figure style="text-align:center;margin:34px 0">
<svg viewBox="0 0 560 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Docker port publishing > host firewall (DNAT wins & UFW is skipped)">
  <rect x="0" y="0" width="560" height="220" rx="10" fill="#11111b"/>
  <text x="280" y="30" text-anchor="middle" fill="#cdd6f4" font-family="monospace" font-size="15">Why the firewall didn't matter</text>
<p><text x="40" y="120" text-anchor="middle" fill="#f38ba8" font-family="monospace" font-size="24">☠</text>
<text x="40" y="145" text-anchor="middle" fill="#6c7086" font-family="monospace" font-size="11">internet</text></p>
  <line x1="62" y1="110" x2="150" y2="110" stroke="#f38ba8" stroke-width="2" marker-end="url(#a)"/>
  <rect x="150" y="70" width="150" height="80" rx="8" fill="#1e1e2e" stroke="#45475a"/>
  <text x="225" y="100" text-anchor="middle" fill="#fab387" font-family="monospace" font-size="13">DOCKER chain</text>
  <text x="225" y="122" text-anchor="middle" fill="#a6e3a1" font-family="monospace" font-size="12">DNAT :9000 & DNAT wins</text>
  <text x="225" y="140" text-anchor="middle" fill="#6c7086" font-family="monospace" font-size="10">first < ufw, host rule > skipped</text>
  <rect x="150" y="165" width="150" height="40" rx="8" fill="#1e1e2e" stroke="#313244"/>
  <text x="225" y="190" text-anchor="middle" fill="#6c7086" font-family="monospace" font-size="12">ufw DENY 9000 (moot)</text>
  <line x1="300" y1="110" x2="400" y2="110" stroke="#89b4fa" stroke-width="2" marker-end="url(#b)"/>
  <rect x="400" y="80" width="130" height="60" rx="8" fill="#313244" stroke="#cba6f7"/>
  <text x="465" y="108" text-anchor="middle" fill="#cba6f7" font-family="monospace" font-size="13">admin-panel</text>
  <text x="465" y="127" text-anchor="middle" fill="#cdd6f4" font-family="monospace" font-size="11">172.17.0.2:9000</text>
  <defs>
    <marker id="a" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto"><path d="M0,0 L6,3 L0,6 Z" fill="#f38ba8"/></marker>
    <marker id="b" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto"><path d="M0,0 L6,3 L0,6 Z" fill="#89b4fa"/></marker>
  </defs>
</svg>
<figcaption style="color:#6c7086;font-size:14px;margin-top:8px">Docker's DNAT rule wins the race. UFW's deny never gets to vote.</figcaption>
</figure>
<h2 id="the-fix">The fix</h2>
<p>The publish target was the bug. <code>0.0.0.0</code> means &ldquo;the whole world.&rdquo; It almost never should.</p>
<p>I bound the panel to loopback only and put it behind the reverse proxy with auth, reachable just through the VPN.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#6272a4"># docker-compose.yml — before</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">ports</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f1fa8c">&#34;9000:9000&#34;</span>        <span style="color:#6272a4"># = 0.0.0.0:9000, published to the internet</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># after</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">ports</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f1fa8c">&#34;127.0.0.1:9000:9000&#34;</span>   <span style="color:#6272a4"># localhost only; proxy/VPN reaches it</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker compose up -d
</span></span><span style="display:flex;"><span>docker ps --format <span style="color:#f1fa8c">&#39;{{.Names}}\t{{.Ports}}&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># admin-panel   127.0.0.1:9000-&gt;9000/tcp</span>
</span></span></code></pre></div><p>For anything that genuinely must survive the firewall, lock it at the <code>DOCKER-USER</code> chain — the one place Docker promises not to clobber:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># allow only the tailnet, drop everyone else for published container ports</span>
</span></span><span style="display:flex;"><span>sudo iptables -I DOCKER-USER -i eth0 -s 100.64.0.0/10 -j RETURN
</span></span><span style="display:flex;"><span>sudo iptables -I DOCKER-USER -i eth0 -p tcp --dport <span style="color:#bd93f9">9000</span> -j DROP
</span></span></code></pre></div><p>Re-scan from outside:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>nmap -Pn -p <span style="color:#bd93f9">9000</span> 192.0.2.10
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># PORT     STATE    SERVICE</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 9000/tcp filtered http</span>
</span></span></code></pre></div><p><code>filtered</code>. Dark. The panel only answers behind the tunnel now.</p>
<h2 id="why-it-happened">Why it happened</h2>
<p>Nobody screwed up the firewall. The firewall did exactly what it was told. The trap is the mental model: people assume UFW sits at the front gate and inspects everything inbound. Docker doesn&rsquo;t go through that gate — it cuts its own keyhole in <code>iptables</code> and your host rules never see the packet.</p>
<p><code>-p 9000:9000</code> reads like &ldquo;expose this locally.&rdquo; It actually means &ldquo;publish this to <code>0.0.0.0</code>.&rdquo; One missing <code>127.0.0.1:</code> prefix and an internal admin tool was on the public internet for who-knows-how-long, indexed by Shodan, waiting.</p>
<h2 id="takeaways">Takeaways</h2>
<ul>
<li><strong>Scan yourself the way an attacker would.</strong> Shodan plus an external <code>nmap</code> from a box outside your network is the only ground truth. Your config file is a hypothesis, not a result.</li>
<li><strong>Docker port publishing ignores your host firewall.</strong> <code>-p PORT:PORT</code> writes its own iptables rules and bypasses UFW entirely. <code>ufw deny</code> does nothing for published container ports.</li>
<li><strong>Bind admin services to <code>127.0.0.1</code> or a VPN, never <code>0.0.0.0</code>.</strong> Always prefix the publish: <code>127.0.0.1:9000:9000</code>. Default-public is a footgun.</li>
<li><strong>Use Docker-aware firewalling.</strong> If a container port truly needs outside reach, gate it in the <code>DOCKER-USER</code> chain — the one chain Docker won&rsquo;t overwrite.</li>
<li><strong>Admin panels belong on localhost or a tunnel, behind a reverse proxy and auth.</strong> Convenient-but-public is just public.</li>
</ul>
]]></content:encoded></item><item><title>Seven Network Gotchas That Look Like Ghosts</title><link>https://errorzap.com/posts/seven-network-gotchas-that-look-like-ghosts/</link><pubDate>Tue, 02 Jun 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/seven-network-gotchas-that-look-like-ghosts/</guid><description>The network isn&amp;rsquo;t haunted — it&amp;rsquo;s orphaned ports, stale DNS, and a firewall eating your return path. Run the checklist before you light the candles.</description><content:encoded><![CDATA[<figure style="text-align:center;margin:0 0 30px"><img src="hero.png" alt="Seven Network Gotchas That Look Like Ghosts" style="max-width:520px;width:100%;border-radius:14px"/></figure>
<p>The ticket said: <em>&ldquo;The server is possessed.&rdquo;</em></p>
<p>That&rsquo;s a direct quote. An app had been rock-solid for two years, then one Tuesday it &ldquo;just stopped&rdquo; — no config change, no deploy, no nothing. The on-call engineer had already rebooted twice, blamed cosmic rays, and was halfway to blaming the building&rsquo;s wiring.</p>
<p>It wasn&rsquo;t possessed. It never is.</p>
<p>After enough years you learn that 95% of &ldquo;haunted network&rdquo; tickets are the same seven mundane bastards wearing bedsheets. None of them are mysterious. All of them are one check away from being solved. The trick is running the check <em>before</em> you start theorizing about ghosts.</p>
<p>Here&rsquo;s the scene I keep coming back to.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>  THE GHOST                        THE CHECK              VERDICT
</span></span><span style="display:flex;"><span>  ─────────────────────────        ──────────────────     ──────
</span></span><span style="display:flex;"><span>  &#34;service won&#39;t start&#34;     ──►    who owns the port?     ✓ orphan
</span></span><span style="display:flex;"><span>  &#34;site loads wrong server&#34; ──►    what does dig say?     ✓ stale DNS
</span></span><span style="display:flex;"><span>  &#34;one client 500s&#34;         ──►    force --http1.1        ✓ h2 quirk
</span></span><span style="display:flex;"><span>  &#34;big transfers hang&#34;      ──►    ping -M do -s 1472     ✓ MTU/MSS
</span></span><span style="display:flex;"><span>  &#34;intermittent dropouts&#34;   ──►    arp -a (two MACs?)     ✓ dup IP
</span></span><span style="display:flex;"><span>  &#34;works one direction&#34;     ──►    firewall state table   ✓ asymmetric
</span></span><span style="display:flex;"><span>  &#34;localhost can&#39;t connect&#34; ──►    loopback fw rule        ✓ blocked pipe
</span></span></code></pre></div><h2 id="the-investigation-compressed-into-a-checklist">The Investigation, Compressed Into a Checklist</h2>
<p><strong>1. The service that &ldquo;can&rsquo;t start.&rdquo;</strong> Symptom: it crashed, you restart it, and now it refuses to bind. The aha: the crashed process orphaned the port and the kernel still thinks it&rsquo;s owned. The check — find the squatter:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># Linux</span>
</span></span><span style="display:flex;"><span>ss -ltnp <span style="color:#f1fa8c">&#39;sport = :8080&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># Windows</span>
</span></span><span style="display:flex;"><span>netstat -ano | findstr :8080
</span></span></code></pre></div><p>Kill the orphan PID, the port frees, the service starts. No exorcism required.</p>
<p><strong>2. DNS serving a corpse.</strong> Symptom: you migrated a site, half the world sees the new box, this one client sees the old one. The check — ask what&rsquo;s <em>actually</em> resolving, not what should:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>dig +short app.example.com @1.1.1.1
</span></span><span style="display:flex;"><span>nslookup app.example.com
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># flush the local liar:</span>
</span></span><span style="display:flex;"><span>sudo resolvectl flush-caches      <span style="color:#6272a4"># systemd</span>
</span></span><span style="display:flex;"><span>ipconfig /flushdns                <span style="color:#6272a4"># Windows</span>
</span></span></code></pre></div><p>Stale cache, gone.</p>
<p><strong>3. The one client that 500s while everyone else is fine.</strong> That&rsquo;s an HTTP/2 downgrade or framing incompatibility. Pin the protocol and watch it heal:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl -v --http1.1 https://app.example.com/health
</span></span></code></pre></div><p>If <code>--http1.1</code> works and the default doesn&rsquo;t, you found your ghost.</p>
<p><strong>4. Small requests fine, big ones hang.</strong> Classic MTU/MSS mismatch — pings and tiny payloads sail through, the TLS handshake or a large transfer stalls forever. Probe for the real path MTU:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># 1472 + 28 overhead = 1500. Shrink until it stops fragmenting.</span>
</span></span><span style="display:flex;"><span>ping -M <span style="color:#ff79c6">do</span> -s <span style="color:#bd93f9">1472</span> 192.0.2.1
</span></span></code></pre></div><p>If 1472 fails but 1400 works, clamp your MSS and the hangs vanish.</p>
<figure style="text-align:center;margin:34px 0">
<svg viewBox="0 0 560 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="MTU mismatch: small packet passes, oversized packet gets dropped">
<rect x="0" y="0" width="560" height="220" rx="10" fill="#11111b"/>
<rect x="40" y="40" width="120" height="140" rx="8" fill="#1e1e2e" stroke="#45475a"/>
<text x="100" y="115" fill="#cdd6f4" font-family="monospace" font-size="14" text-anchor="middle">host A</text>
<rect x="400" y="40" width="120" height="140" rx="8" fill="#1e1e2e" stroke="#45475a"/>
<text x="460" y="115" fill="#cdd6f4" font-family="monospace" font-size="14" text-anchor="middle">host B</text>
<rect x="250" y="30" width="60" height="160" rx="6" fill="#313244" stroke="#45475a"/>
<text x="280" y="205" fill="#6c7086" font-family="monospace" font-size="12" text-anchor="middle">MTU 1400 link</text>
<rect x="170" y="70" width="70" height="14" rx="3" fill="#a6e3a1"/>
<text x="248" y="81" fill="#a6e3a1" font-family="monospace" font-size="13">small ✓</text>
<rect x="170" y="120" width="130" height="22" rx="3" fill="#f38ba8"/>
<text x="170" y="160" fill="#f38ba8" font-family="monospace" font-size="13">oversized ✗</text>
<line x1="300" y1="131" x2="252" y2="131" stroke="#f38ba8" stroke-width="2" stroke-dasharray="4 3"/>
<text x="252" y="118" fill="#f38ba8" font-family="monospace" font-size="16" text-anchor="middle">▼</text>
</svg>
<figcaption style="color:#6c7086;font-size:14px;margin-top:8px">The big frame doesn't fit through the narrow link — and nobody told the sender.</figcaption>
</figure>
<p><strong>5. The intermittent flapper.</strong> Symptom: connectivity that drops for seconds at random — a rogue DHCP server or a static IP collision. Check the ARP table for one IP claimed by two MACs:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>arp -a | sort        <span style="color:#6272a4"># two different MACs on the same IP = your culprit</span>
</span></span><span style="display:flex;"><span>ip neigh show
</span></span></code></pre></div><p><strong>6. Works one way, dies the other.</strong> Asymmetric routing where the return path takes a different door and a stateful firewall drops it for having no matching session. Look at the state table:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># Linux conntrack</span>
</span></span><span style="display:flex;"><span>conntrack -L | grep 192.0.2.50
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># pf</span>
</span></span><span style="display:flex;"><span>pfctl -ss | grep 192.0.2.50
</span></span></code></pre></div><p>No state entry for the return flow = there&rsquo;s your &ldquo;ghost.&rdquo;</p>
<p><strong>7. The app that &ldquo;should be local&rdquo; but can&rsquo;t reach itself.</strong> A host firewall rule or blocked named pipe is eating loopback. Confirm the listener and that localhost itself is allowed:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ss -ltnp | grep 127.0.0.1
</span></span><span style="display:flex;"><span>sudo iptables -L INPUT -n -v | grep -E <span style="color:#f1fa8c">&#39;lo|127.0.0.1&#39;</span>
</span></span></code></pre></div><p>That Tuesday &ldquo;possession&rdquo;? Number one. Crashed worker, orphaned port, <code>ss -ltnp</code> named the PID in four seconds. Ghost dispelled.</p>
<h2 id="why-it-happens">Why It Happens</h2>
<p>None of these are exotic. They&rsquo;re the natural failure modes of stateful systems: processes die untidily, caches outlive their truth, two devices want the same address, a firewall remembers a flow that no longer exists. The supernatural feeling comes entirely from <em>partial</em> symptoms — it works for some and not others, or for small things and not big ones. Partial failure reads as spooky. It&rsquo;s just state you haven&rsquo;t looked at yet.</p>
<h2 id="takeaways">Takeaways</h2>
<ul>
<li><strong>Run the seven-item checklist before you theorize.</strong> The boring answer is almost always right.</li>
<li><strong>&ldquo;Some clients work&rdquo; is a clue, not a contradiction</strong> — it points straight at DNS, HTTP/2, or MTU.</li>
<li><strong>&ldquo;Small works, big hangs&rdquo; is MTU/MSS until proven otherwise.</strong> Don&rsquo;t waste an hour on app logs.</li>
<li><strong>Check state, not config.</strong> <code>ss</code>, <code>dig</code>, <code>arp</code>, and the firewall state table tell you what <em>is</em>, not what <em>should be</em>.</li>
<li><strong>Reboots hide orphans; they don&rsquo;t explain them.</strong> Find the squatter PID before you cycle power and lose the evidence.</li>
</ul>
]]></content:encoded></item><item><title>The 99 Access Points That Didn't Exist</title><link>https://errorzap.com/posts/the-99-access-points-that-didnt-exist/</link><pubDate>Fri, 29 May 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/the-99-access-points-that-didnt-exist/</guid><description>An inventory report swore a site had 99 access points; the ceiling said twelve, and the database had been lying the whole time.</description><content:encoded><![CDATA[<figure style="text-align:center;margin:0 0 30px"><img src="hero.png" alt="The 99 Access Points That Didn't Exist" style="max-width:520px;width:100%;border-radius:14px"/></figure>
<p>The ticket landed with a number attached, and the number was wrong.</p>
<p>A monitoring report — the one we generate so nobody has to think too hard — claimed a site was running <strong>99 wireless access points</strong>. Ninety-nine. For a building that, last I checked, you could walk end to end during a coffee that hadn&rsquo;t gone cold yet.</p>
<p>I&rsquo;d been in that building. I knew the ceilings. There were not 99 APs up there. There weren&rsquo;t even a quarter of that.</p>
<p>So either the site had quietly become an enterprise campus overnight, or the report was hallucinating. Spoiler: reports don&rsquo;t hallucinate. They count exactly what you tell them to count, which is a different and worse problem.</p>
<h2 id="the-investigation">The investigation</h2>
<p>First rule when a number looks insane: don&rsquo;t argue with the number. Go find ground truth and make the number argue with <em>that</em>.</p>
<p>I pulled the controller&rsquo;s live device list — the actual adopted, talking-to-me-right-now hardware:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># adopted APs the controller is actively managing</span>
</span></span><span style="display:flex;"><span>curl -sk -b /tmp/controller.cookie <span style="color:#f1fa8c">\
</span></span></span><span style="display:flex;"><span>  https://controller.example.internal/proxy/network/api/s/default/stat/device <span style="color:#f1fa8c">\
</span></span></span><span style="display:flex;"><span>  | jq <span style="color:#f1fa8c">&#39;[.data[] | select(.type==&#34;uap&#34;)] | length&#39;</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>12
</span></span></code></pre></div><p>Twelve. Twelve adopted access points, online, real, with MAC addresses I could go put my hand on.</p>
<p>So where did the report get 99?</p>
<p>I went and read the thing that built the report instead of the report itself. The collector wasn&rsquo;t querying live adopted devices. It was scraping a broader chunk of the controller database — and it was counting <em>rows</em>, not <em>radios you could hang on a wall</em>.</p>
<h2 id="the-aha">The aha</h2>
<p>Three lies were stacked on top of each other, and the report added them all up with a straight face.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>  WHAT THE REPORT COUNTED            WHAT WAS ACTUALLY THERE
</span></span><span style="display:flex;"><span>  ─────────────────────────          ───────────────────────
</span></span><span style="display:flex;"><span>  ┌───────────────────────┐
</span></span><span style="display:flex;"><span>  │ adopted APs        12 │ ◄──────── ✓ real, on the ceiling
</span></span><span style="display:flex;"><span>  ├───────────────────────┤
</span></span><span style="display:flex;"><span>  │ pending / forgotten   │
</span></span><span style="display:flex;"><span>  │ ghost DB rows      ~30│ ◄──────── ✗ stale, never cleaned
</span></span><span style="display:flex;"><span>  ├───────────────────────┤
</span></span><span style="display:flex;"><span>  │ per-radio entries     │
</span></span><span style="display:flex;"><span>  │ (2.4 / 5 / 6 GHz)     │ ◄──────── ✗ one AP counted 3×
</span></span><span style="display:flex;"><span>  ├───────────────────────┤
</span></span><span style="display:flex;"><span>  │ per-SSID rows         │ ◄──────── ✗ counted broadcasts
</span></span><span style="display:flex;"><span>  └───────────────────────┘            as devices
</span></span><span style="display:flex;"><span>        Σ = 99                          Σ (real) = 12
</span></span></code></pre></div><p>Every tri-band AP showed up as three radio entries. Every SSID broadcast left its own footprint. And the controller DB still held a graveyard of devices that had been swapped out, RMA&rsquo;d, or &ldquo;forgotten&rdquo; in the UI months ago — rows that never actually leave the database, they just stop mattering. To a human. Not to a <code>COUNT(*)</code>.</p>
<p>Stack the real twelve on top of the radio multiplication on top of the ghost rows, and you arrive — with perfect arithmetic and zero truth — at 99.</p>
<figure style="text-align:center;margin:34px 0">
<svg viewBox="0 0 560 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="One real access point exploding into many counted rows">
  <rect x="0" y="0" width="560" height="220" rx="10" fill="#11111b"/>
  <rect x="34" y="78" width="120" height="64" rx="8" fill="#1e1e2e" stroke="#45475a"/>
  <circle cx="94" cy="110" r="18" fill="#1e1e2e" stroke="#a6e3a1" stroke-width="2"/>
  <path d="M82 110 a12 12 0 0 1 24 0" fill="none" stroke="#a6e3a1" stroke-width="2"/>
  <path d="M86 110 a8 8 0 0 1 16 0" fill="none" stroke="#a6e3a1" stroke-width="2"/>
  <circle cx="94" cy="110" r="2.5" fill="#a6e3a1"/>
  <text x="94" y="160" fill="#a6e3a1" font-family="monospace" font-size="13" text-anchor="middle">1 real AP</text>
  <path d="M160 110 H230" stroke="#6c7086" stroke-width="2" stroke-dasharray="5 4"/>
  <polygon points="230,110 222,105 222,115" fill="#6c7086"/>
  <g font-family="monospace" font-size="12">
    <rect x="240" y="34" width="150" height="26" rx="5" fill="#313244" stroke="#cba6f7"/>
    <text x="252" y="51" fill="#cba6f7">radio 2.4GHz row</text>
    <rect x="240" y="68" width="150" height="26" rx="5" fill="#313244" stroke="#89b4fa"/>
    <text x="252" y="85" fill="#89b4fa">radio 5GHz row</text>
    <rect x="240" y="102" width="150" height="26" rx="5" fill="#313244" stroke="#94e2d5"/>
    <text x="252" y="119" fill="#94e2d5">radio 6GHz row</text>
    <rect x="240" y="136" width="150" height="26" rx="5" fill="#313244" stroke="#fab387"/>
    <text x="252" y="153" fill="#fab387">per-SSID rows...</text>
    <rect x="240" y="170" width="150" height="26" rx="5" fill="#313244" stroke="#f38ba8"/>
    <text x="252" y="187" fill="#f38ba8">ghost / forgotten</text>
  </g>
  <path d="M398 130 H452" stroke="#6c7086" stroke-width="2"/>
  <polygon points="452,130 444,125 444,135" fill="#6c7086"/>
  <text x="506" y="124" fill="#f38ba8" font-family="monospace" font-size="34" text-anchor="middle" font-weight="bold">99</text>
  <text x="506" y="150" fill="#6c7086" font-family="monospace" font-size="12" text-anchor="middle">"APs"</text>
</svg>
<figcaption style="color:#6c7086;font-size:13px;margin-top:8px">One access point on the ceiling, multiplied by radios, SSIDs, and database ghosts into a number nobody could find.</figcaption>
</figure>
<h2 id="the-fix">The fix</h2>
<p>The fix wasn&rsquo;t clever. It was <em>narrowing the question</em>. Count adopted UAP-type devices, deduplicated by MAC, and exclude anything pending or forgotten:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl -sk -b /tmp/controller.cookie <span style="color:#f1fa8c">\
</span></span></span><span style="display:flex;"><span>  https://controller.example.internal/proxy/network/api/s/default/stat/device <span style="color:#f1fa8c">\
</span></span></span><span style="display:flex;"><span>  | jq <span style="color:#f1fa8c">&#39;[.data[]
</span></span></span><span style="display:flex;"><span><span style="color:#f1fa8c">         | select(.type==&#34;uap&#34;)
</span></span></span><span style="display:flex;"><span><span style="color:#f1fa8c">         | select(.adopted==true)
</span></span></span><span style="display:flex;"><span><span style="color:#f1fa8c">         | select(.state==1)          # 1 = connected
</span></span></span><span style="display:flex;"><span><span style="color:#f1fa8c">         | .mac]
</span></span></span><span style="display:flex;"><span><span style="color:#f1fa8c">        | unique | length&#39;</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>12
</span></span></code></pre></div><p>Then I did the thing the database can&rsquo;t: I walked the building and counted ceilings. Twelve.</p>
<p>The report now pulls that single reconciled number, and it carries a footnote: <em>adopted, connected, deduped by MAC.</em> If it ever disagrees with the live device list again, it&rsquo;s the report that has to explain itself.</p>
<h2 id="why-it-happened">Why it happened</h2>
<p>Because counting feels trivial, so nobody guards it. <code>length</code> on a JSON array looks like truth. But the controller DB isn&rsquo;t a list of access points — it&rsquo;s an event log of everything that ever <em>claimed</em> to be one: every radio, every SSID broadcast, every device that got swapped and never purged. Counting rows in that pile and calling it &ldquo;APs&rdquo; is like counting fingerprints and reporting the number of burglars.</p>
<p>The artifact survived for months because 99 was <em>plausible-adjacent</em>. Nobody who&rsquo;d been on-site ever read the report; nobody who read the report had ever been on-site. The number floated in the gap between them, fat and unchallenged, until somebody finally looked up.</p>
<h2 id="takeaways">Takeaways</h2>
<ul>
<li><strong>Never report an inventory number you can&rsquo;t reconcile to ground truth.</strong> A count needs two witnesses: the live device list <em>and</em> a physical walk.</li>
<li><strong>Rows are not things.</strong> Per-radio and per-SSID entries multiply one AP into many. Filter by device type and dedupe by MAC before you trust a total.</li>
<li><strong>Controller databases keep ghosts.</strong> Forgotten, pending, and RMA&rsquo;d devices linger in the DB long after they leave the ceiling. Scope your query to <em>adopted and connected</em>.</li>
<li><strong>Plausible-adjacent is the dangerous zone.</strong> A wrong number that&rsquo;s obviously insane gets caught. One that&rsquo;s merely &ldquo;a bit high&rdquo; rides for months.</li>
<li><strong>Read the query, not the report.</strong> When a metric lies, the bug is upstream in what you told it to count — go fix the question.</li>
</ul>
]]></content:encoded></item><item><title>The Smart Blind That Took Down an Entire Network</title><link>https://errorzap.com/posts/the-smart-blind-that-took-down-the-network/</link><pubDate>Thu, 28 May 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/the-smart-blind-that-took-down-the-network/</guid><description>A $30 Wi-Fi window blind decided it was the default gateway, and the whole site went dark whenever it felt like it.</description><content:encoded><![CDATA[<figure style="text-align:center;margin:0 0 30px"><img src="hero.png" alt="The Smart Blind That Took Down an Entire Network" style="max-width:520px;width:100%;border-radius:14px"/></figure>
<p>The ticket said &ldquo;internet is down.&rdquo; Then, four minutes later, &ldquo;never mind, it&rsquo;s back.&rdquo; Then, twenty minutes after that, &ldquo;it&rsquo;s down again.&rdquo;</p>
<p>That pattern is a special kind of hell. A clean outage you can chase. A flapping one that heals itself before you SSH in just laughs at you.</p>
<p>I got a client on the phone. Their whole site would blackhole — every desktop, every phone, the printer, all of it — for one to five minutes, then snap back like nothing happened. No schedule. No trigger anyone could name. The ISP swore the circuit was clean, and for once the ISP was right.</p>
<h2 id="the-investigation">The investigation</h2>
<p>I started where you start: at the edge. The gateway was up. WAN was up. The firewall logs were boring in the way you <em>want</em> them to be boring. No flapping interface, no CPU spike, no DHCP storm in the leases.</p>
<p>So I sat on a workstation and just hammered the gateway while I waited for the next outage.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ping -i 0.5 192.0.2.1
</span></span></code></pre></div><p>It ran fine for ten minutes. Then — dead air. Eight, nine, ten dropped replies. Total blackhole. Then it came back on its own.</p>
<p>The instant it died, I dumped the ARP table.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>arp -a 192.0.2.1
</span></span></code></pre></div><p>And there it was. The gateway&rsquo;s IP was mapped to a MAC address I did not recognize. Not the firewall&rsquo;s MAC. Some OUI I had to look up by hand.</p>
<p>I watched it flap in real time:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>┌─────────────────────────────────────────────────────────────┐
</span></span><span style="display:flex;"><span>│  arp -a 192.0.2.1   (watched over ~30s)                      │
</span></span><span style="display:flex;"><span>├─────────────────────────────────────────────────────────────┤
</span></span><span style="display:flex;"><span>│  t+0s   192.0.2.1  ►  aa:bb:cc:11:22:33   (firewall)   ✓     │
</span></span><span style="display:flex;"><span>│  t+6s   192.0.2.1  ►  de:ad:be:ef:00:99   (???)        ✗     │
</span></span><span style="display:flex;"><span>│  t+9s   192.0.2.1  ►  de:ad:be:ef:00:99   (???)        ✗     │
</span></span><span style="display:flex;"><span>│  t+14s  192.0.2.1  ►  aa:bb:cc:11:22:33   (firewall)   ✓     │
</span></span><span style="display:flex;"><span>│  t+19s  192.0.2.1  ►  de:ad:be:ef:00:99   (???)        ✗     │
</span></span><span style="display:flex;"><span>└─────────────────────────────────────────────────────────────┘
</span></span><span style="display:flex;"><span>         ▼ when the IP pointed at de:ad:..:99, traffic vanished
</span></span></code></pre></div><p>When the gateway IP pointed at the firewall, the network worked. When it pointed at that mystery MAC, every packet bound for the internet got handed to a device that did absolutely nothing with it. Classic ARP poisoning. The only question was <em>who</em>.</p>
<h2 id="the-aha">The &ldquo;aha&rdquo;</h2>
<p>I looked up the OUI. The mystery MAC belonged to a Tuya-based Wi-Fi module — the kind that ships inside cheap smart-home gadgets by the millions.</p>
<p>Then I cross-referenced the DHCP leases for that MAC and found a hostname that made me laugh out loud: a smart <strong>window blind</strong> controller. Somebody had put a $30 motorized blind on the corporate flat network.</p>
<p>That little NIC was broadcasting unsolicited ARP replies claiming to be <code>192.0.2.1</code>. Every client on the segment believed it, updated its ARP cache, and started shipping its default route straight into a window covering.</p>
<h2 id="the-fix">The fix</h2>
<p>First, stop the bleeding. I pinned the gateway&rsquo;s real MAC on the affected machines so they&rsquo;d stop trusting the lie while I worked.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># Linux</span>
</span></span><span style="display:flex;"><span>sudo ip neigh replace 192.0.2.1 lladdr aa:bb:cc:11:22:33 dev eth0 nud permanent
</span></span></code></pre></div><p>Then the real fix — get IoT off the flat network entirely and turn on the switch protections that should&rsquo;ve been on from day one.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>BEFORE                              AFTER
</span></span><span style="display:flex;"><span>──────                              ─────
</span></span><span style="display:flex;"><span>VLAN 1 (everything)                 VLAN 10  desktops/servers
</span></span><span style="display:flex;"><span>  ├─ desktops                       VLAN 20  voice
</span></span><span style="display:flex;"><span>  ├─ servers                        VLAN 90  IoT  ◄── blind lives here,
</span></span><span style="display:flex;"><span>  ├─ phones                                          firewalled, no L2
</span></span><span style="display:flex;"><span>  └─ smart blind  ◄── poisoner                       reach to clients
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># Cisco-style switch hardening (concept)</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># Trust only the uplink for DHCP; drop rogue offers elsewhere</span>
</span></span><span style="display:flex;"><span>ip dhcp snooping
</span></span><span style="display:flex;"><span>ip dhcp snooping vlan <span style="color:#bd93f9">90</span>
</span></span><span style="display:flex;"><span>interface Gi1/0/24
</span></span><span style="display:flex;"><span> ip dhcp snooping trust
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># Dynamic ARP Inspection — validate ARP against snooping bindings</span>
</span></span><span style="display:flex;"><span>ip arp inspection vlan <span style="color:#bd93f9">90</span>
</span></span><span style="display:flex;"><span>interface Gi1/0/24
</span></span><span style="display:flex;"><span> ip arp inspection trust
</span></span></code></pre></div><figure style="text-align:center;margin:34px 0">
<svg viewBox="0 0 560 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="A smart blind impersonating the gateway in the ARP table">
<rect x="0" y="0" width="560" height="220" rx="10" fill="#11111b"/>
<rect x="30" y="40" width="130" height="64" rx="8" fill="#1e1e2e" stroke="#45475a"/>
<text x="95" y="68" text-anchor="middle" font-family="monospace" font-size="13" fill="#cdd6f4">Workstation</text>
<text x="95" y="88" text-anchor="middle" font-family="monospace" font-size="11" fill="#6c7086">who has .1?</text>
<rect x="400" y="20" width="130" height="50" rx="8" fill="#1e1e2e" stroke="#a6e3a1"/>
<text x="465" y="42" text-anchor="middle" font-family="monospace" font-size="13" fill="#a6e3a1">Gateway .1</text>
<text x="465" y="59" text-anchor="middle" font-family="monospace" font-size="10" fill="#6c7086">aa:bb:cc...</text>
<rect x="400" y="140" width="130" height="56" rx="8" fill="#1e1e2e" stroke="#f38ba8"/>
<text x="465" y="164" text-anchor="middle" font-family="monospace" font-size="12" fill="#f38ba8">Smart Blind</text>
<text x="465" y="181" text-anchor="middle" font-family="monospace" font-size="10" fill="#fab387">"I am .1!"</text>
<line x1="160" y1="64" x2="400" y2="45" stroke="#45475a" stroke-width="2" stroke-dasharray="5 4"/>
<line x1="160" y1="80" x2="400" y2="160" stroke="#f38ba8" stroke-width="2.5"/>
<text x="270" y="120" text-anchor="middle" font-family="monospace" font-size="11" fill="#f38ba8">poisoned ARP ►</text>
<text x="280" y="210" text-anchor="middle" font-family="monospace" font-size="11" fill="#cba6f7">traffic blackholes into a window covering</text>
</svg>
<figcaption style="color:#6c7086;font-size:14px;margin-top:8px">The blind announces itself as the gateway; clients believe it and route to nowhere.</figcaption>
</figure>
<p>Last step: I yanked the blind, pushed its vendor firmware update, and parked it on the isolated IoT VLAN where it can lie about being the gateway all it wants — and nobody who matters will hear it.</p>
<h2 id="why-it-happened">Why it happened</h2>
<p>Cheap Tuya firmware is a grab bag. Some builds have buggy network stacks that emit malformed or unsolicited ARP — gratuitous ARP gone feral, sometimes triggered by a Wi-Fi reconnect, which explains the random timing. Could also be straight-up malicious; with no-name IoT you genuinely can&rsquo;t tell, and it doesn&rsquo;t change the remediation.</p>
<p>Either way, a flat L2 network <em>trusts every device equally</em>. ARP has no authentication. Whatever shouts loudest and last, wins. Put one mouthy gadget on that wire and it can pretend to be anything — including the door to the internet.</p>
<h2 id="takeaways">Takeaways</h2>
<ul>
<li><strong>On weird, intermittent, self-healing outages, check the gateway&rsquo;s ARP entry FIRST.</strong> <code>arp -a &lt;gateway-ip&gt;</code> flapping to an unknown MAC is a five-second smoking gun.</li>
<li><strong>Match a rogue MAC to its OUI and your DHCP leases.</strong> That&rsquo;s how a &ldquo;mystery device&rdquo; became &ldquo;the smart blind in the break room.&rdquo;</li>
<li><strong>IoT does not belong on your flat network. Ever.</strong> Its own VLAN/SSID, firewalled off from clients, no L2 reach to anything that matters.</li>
<li><strong>Turn on DHCP Snooping and Dynamic ARP Inspection.</strong> They exist precisely to kill rogue gateways and ARP lies at the switch.</li>
<li><strong>Cheap IoT is an untrusted host by default</strong> — bug or malice, you treat it the same: isolate, update, and never let it speak for the gateway.</li>
</ul>
]]></content:encoded></item><item><title>The Deploy Script That Ate the Production Database</title><link>https://errorzap.com/posts/the-deploy-script-that-ate-the-production-database/</link><pubDate>Sun, 24 May 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/the-deploy-script-that-ate-the-production-database/</guid><description>A homegrown deploy script copied the whole app folder to prod — database file and all — and quietly overwrote live data with an empty local copy.</description><content:encoded><![CDATA[<figure style="text-align:center;margin:0 0 30px"><img src="hero.png" alt="The Deploy Script That Ate the Production Database" style="max-width:520px;width:100%;border-radius:14px"/></figure>
<p>The chat ping came in at 4:47 PM. &ldquo;Hey, is the app down? All my stuff is gone.&rdquo;</p>
<p>I pulled up the site. It loaded fine. Login worked. Dashboard rendered. And it was empty. No records. No users. No history. A perfect, pristine, brand-new install — on a server that had been running for fourteen months.</p>
<p>Nothing was <em>broken</em>. That&rsquo;s what made my stomach drop. A crash leaves a corpse. This was worse. The app was alive and humming, cheerfully serving a database that had been born thirty seconds ago.</p>
<h2 id="the-investigation">The investigation</h2>
<p>First instinct: bad migration. Nope — no migrations had run. Second instinct: someone fat-fingered a <code>DELETE</code>. But the Postgres-shaped panic faded fast, because this app didn&rsquo;t use Postgres. It used a SQLite file. A single file, living in the app directory.</p>
<p>That detail mattered more than I realized.</p>
<p>I checked the logs. The last thing that happened before the data vanished was a deploy. Routine. Boring. A dev had pushed a tiny CSS fix and run the deploy script, same as always.</p>
<p>So I opened the deploy script. And there it was, line nine, smiling at me like a loaded gun:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>scp -r ./app/* deploy@198.51.100.10:/srv/app/
</span></span></code></pre></div><p><code>./app/*</code>. The whole directory. Including <code>app/data/app.db</code>.</p>
<p>The dev&rsquo;s local <code>app.db</code> was empty — fresh clone, never seeded. Every deploy had been copying that empty file up and stomping the live one. The only reason nobody noticed sooner was that <em>this</em> dev happened to have a blank local copy. Previous deploys from other machines had local data that masked the bug.</p>
<p>Here&rsquo;s the shape of the disaster:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>   DEV MACHINE                        PRODUCTION
</span></span><span style="display:flex;"><span> ┌──────────────┐                  ┌──────────────┐
</span></span><span style="display:flex;"><span> │ app/         │                  │ app/         │
</span></span><span style="display:flex;"><span> │  ├ main.py   │ ── scp -r ──►    │  ├ main.py   │
</span></span><span style="display:flex;"><span> │  ├ static/   │   &#34;deploy&#34;       │  ├ static/   │
</span></span><span style="display:flex;"><span> │  └ data/     │                  │  └ data/     │
</span></span><span style="display:flex;"><span> │     app.db   │ ════ EMPTY ════► │     app.db   │  ◄ 14 months
</span></span><span style="display:flex;"><span> │   (0 rows)   │    OVERWRITES    │   (was full) │     GONE
</span></span><span style="display:flex;"><span> └──────────────┘                  └──────────────┘
</span></span></code></pre></div><p>The fix wasn&rsquo;t on the live disk. But the box ran nightly off-site snapshots, and the most recent one was eleven hours old. We restored that, replayed the day&rsquo;s activity from request logs by hand, and lost almost nothing. Lucky. Stupid lucky.</p>
<figure style="text-align:center;margin:34px 0">
<svg viewBox="0 0 560 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Deploy code, not data">
<rect x="0" y="0" width="560" height="220" rx="10" fill="#1e1e2e"/>
<rect x="34" y="58" width="180" height="104" rx="8" fill="#313244" stroke="#45475a"/>
<text x="124" y="86" text-anchor="middle" fill="#cdd6f4" font-family="monospace" font-size="14">app code</text>
<text x="124" y="112" text-anchor="middle" fill="#a6e3a1" font-family="monospace" font-size="12">main.py</text>
<text x="124" y="132" text-anchor="middle" fill="#a6e3a1" font-family="monospace" font-size="12">static/</text>
<rect x="346" y="58" width="180" height="104" rx="8" fill="#313244" stroke="#f38ba8"/>
<text x="436" y="86" text-anchor="middle" fill="#cdd6f4" font-family="monospace" font-size="14">prod data</text>
<text x="436" y="112" text-anchor="middle" fill="#fab387" font-family="monospace" font-size="12">app.db</text>
<text x="436" y="132" text-anchor="middle" fill="#fab387" font-family="monospace" font-size="12">uploads/</text>
<line x1="214" y1="110" x2="346" y2="110" stroke="#89b4fa" stroke-width="3"/>
<polygon points="346,110 332,103 332,117" fill="#89b4fa"/>
<text x="280" y="100" text-anchor="middle" fill="#cba6f7" font-family="monospace" font-size="13">rsync</text>
<text x="280" y="148" text-anchor="middle" fill="#f38ba8" font-family="monospace" font-size="12">--exclude data/</text>
<text x="280" y="40" text-anchor="middle" fill="#94e2d5" font-family="monospace" font-size="15">code >> data & never the reverse</text>
</svg>
<figcaption style="color:#6c7086;font-size:14px;margin-top:8px">The deploy should push code over the wall and leave production's data untouched.</figcaption>
</figure>
<h2 id="the-fix">The fix</h2>
<p>The old script blanket-copied everything. The new one does three things it refused to do before: backs up prod data <em>first</em>, uses <code>rsync</code> with explicit excludes, and never touches the data paths at all.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#ff79c6">#!/usr/bin/env bash
</span></span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">set</span> -euo pipefail
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">HOST</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;deploy@198.51.100.10&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">APP_DIR</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;/srv/app&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">STAMP</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;</span><span style="color:#ff79c6">$(</span>date +%Y%m%d-%H%M%S<span style="color:#ff79c6">)</span><span style="color:#f1fa8c">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 1. Back up prod data BEFORE we touch anything.</span>
</span></span><span style="display:flex;"><span>ssh <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$HOST</span><span style="color:#f1fa8c">&#34;</span> <span style="color:#f1fa8c">&#34;tar czf /srv/backups/data-</span><span style="color:#f1fa8c">${</span><span style="color:#8be9fd;font-style:italic">STAMP</span><span style="color:#f1fa8c">}</span><span style="color:#f1fa8c">.tar.gz \
</span></span></span><span style="display:flex;"><span><span style="color:#f1fa8c">    -C </span><span style="color:#f1fa8c">${</span><span style="color:#8be9fd;font-style:italic">APP_DIR</span><span style="color:#f1fa8c">}</span><span style="color:#f1fa8c"> data uploads&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 2. Push CODE only. Exclude every path that holds state.</span>
</span></span><span style="display:flex;"><span>rsync -avz --delete <span style="color:#f1fa8c">\
</span></span></span><span style="display:flex;"><span>    --exclude<span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#39;data/&#39;</span> <span style="color:#f1fa8c">\
</span></span></span><span style="display:flex;"><span>    --exclude<span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#39;uploads/&#39;</span> <span style="color:#f1fa8c">\
</span></span></span><span style="display:flex;"><span>    --exclude<span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#39;*.db&#39;</span> <span style="color:#f1fa8c">\
</span></span></span><span style="display:flex;"><span>    --exclude<span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#39;*.sqlite&#39;</span> <span style="color:#f1fa8c">\
</span></span></span><span style="display:flex;"><span>    --exclude<span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#39;.env&#39;</span> <span style="color:#f1fa8c">\
</span></span></span><span style="display:flex;"><span>    ./app/ <span style="color:#f1fa8c">&#34;</span><span style="color:#f1fa8c">${</span><span style="color:#8be9fd;font-style:italic">HOST</span><span style="color:#f1fa8c">}</span><span style="color:#f1fa8c">:</span><span style="color:#f1fa8c">${</span><span style="color:#8be9fd;font-style:italic">APP_DIR</span><span style="color:#f1fa8c">}</span><span style="color:#f1fa8c">/&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34;Deployed. Pre-deploy snapshot: data-</span><span style="color:#f1fa8c">${</span><span style="color:#8be9fd;font-style:italic">STAMP</span><span style="color:#f1fa8c">}</span><span style="color:#f1fa8c">.tar.gz&#34;</span>
</span></span></code></pre></div><p>Note <code>--delete</code> is now safe <em>because</em> the data dirs are excluded — rsync won&rsquo;t reach in and prune <code>data/</code> if it never syncs <code>data/</code> in the first place. And the backup runs before the push, so even a botched exclude has a parachute.</p>
<h2 id="why-it-happened">Why it happened</h2>
<p><code>scp -r ./app/*</code> isn&rsquo;t a bug. It does exactly what it says. The bug was a design decision nobody made on purpose: <strong>letting state live inside the deploy artifact.</strong> The database sat in the same folder as the code, so &ldquo;deploy the code&rdquo; silently meant &ldquo;deploy the database too.&rdquo;</p>
<p>The destructive path wasn&rsquo;t unlikely. It ran on <em>every single deploy</em>. It just stayed invisible until a dev with an empty local copy pulled the trigger.</p>
<h2 id="takeaways">Takeaways</h2>
<ul>
<li><strong>Deploy code, not data.</strong> Application state — databases, uploads, user files — must live <em>outside</em> anything a deploy can overwrite.</li>
<li><strong>Any deploy that can touch the database is a loaded gun.</strong> Add explicit <code>--exclude</code> rules for every data and upload path, and verify them in a dry run (<code>rsync -n</code>).</li>
<li><strong>Back up before you deploy, not after.</strong> A snapshot taken the instant before the push is the cheapest insurance you&rsquo;ll ever buy.</li>
<li><strong>Make the destructive path impossible, not just unlikely.</strong> &ldquo;Be careful&rdquo; is not a control. Excludes and pre-deploy backups are.</li>
<li><strong>Boring near-misses are gifts.</strong> We got lucky with an eleven-hour-old snapshot. Don&rsquo;t design around luck — design so the gun can&rsquo;t fire.</li>
</ul>
]]></content:encoded></item><item><title>I Found a Secret in a Git Repo (It Was Mine)</title><link>https://errorzap.com/posts/i-found-a-secret-in-a-git-repo/</link><pubDate>Thu, 21 May 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/i-found-a-secret-in-a-git-repo/</guid><description>I went looking for a leaked credential in a client&amp;rsquo;s stack and found one staring back at me — committed by my own hand, baked into history forever.</description><content:encoded><![CDATA[<figure style="text-align:center;margin:0 0 30px"><img src="hero.png" alt="I Found a Secret in a Git Repo (It Was Mine)" style="max-width:520px;width:100%;border-radius:14px"/></figure>
<p>It was supposed to be a quick audit.</p>
<p>A client had asked me to sweep a private repo before they handed access to a new contractor. Routine. I cloned it, ran a scanner over the working tree, and started reading. Five minutes in, the scanner lit up red on a config helper.</p>
<p>A reusable credential. Plaintext. In the repo.</p>
<p>I felt that familiar little spike of righteous sysadmin adrenaline — <em>who the hell committed THIS</em> — opened the blame, and read the author line.</p>
<p>It was me.</p>
<h2 id="the-investigation">The investigation</h2>
<p>Here&rsquo;s the part everyone gets wrong, and the part I almost got wrong too.</p>
<p>My first instinct was to delete the line, commit &ldquo;remove secret,&rdquo; and move on. Crisis averted, right?</p>
<p>Wrong. The file in the working tree is the <em>least</em> important place that secret lives. Git is a time machine. Deleting it today does nothing about the dozens of commits where it sits, fat and happy, in the history. Anyone with <code>git log</code> and ten seconds can walk straight to it.</p>
<p>I checked how deep it went:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git log --all --oneline -S <span style="color:#f1fa8c">&#39;the_secret_string&#39;</span> -- path/to/config
</span></span><span style="display:flex;"><span>git rev-list --all --count
</span></span></code></pre></div><p>It went <em>deep</em>. The credential had been in the repo for months, across dozens of commits, surviving file renames and a refactor. Deleting the file would leave it perfectly intact in every parent commit.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>  the working tree (what you see)
</span></span><span style="display:flex;"><span>        │
</span></span><span style="display:flex;"><span>        ▼
</span></span><span style="display:flex;"><span>   ┌─────────┐   ┌─────────┐   ┌─────────┐   ┌─────────┐
</span></span><span style="display:flex;"><span>   │ commit  │◄──│ commit  │◄──│ commit  │◄──│ commit  │
</span></span><span style="display:flex;"><span>   │  HEAD   │   │  ~12    │   │  ~31    │   │  ~40    │
</span></span><span style="display:flex;"><span>   │  ✗ gone │   │ ▓ secret│   │ ▓ secret│   │ ▓ secret│
</span></span><span style="display:flex;"><span>   └─────────┘   └─────────┘   └─────────┘   └─────────┘
</span></span><span style="display:flex;"><span>        ▲              └──────────────┬──────────────┘
</span></span><span style="display:flex;"><span>   you &#34;fixed&#34; it          still right here, forever
</span></span></code></pre></div><h2 id="the-aha">The &ldquo;aha&rdquo;</h2>
<p>The realization isn&rsquo;t &ldquo;I need to scrub this.&rdquo; It&rsquo;s earlier and uglier than that:</p>
<p><strong>A secret is compromised the instant it&rsquo;s committed.</strong> Not when the repo goes public. Not when someone clones it. At commit time. Every backup, every fork, every laptop that ever pulled, every CI cache — assume the secret is <em>out</em>. You cannot un-ring that bell by editing history.</p>
<p>So scrubbing history is necessary, but it is not step one. Step one is to make the leaked value worthless.</p>
<figure style="text-align:center;margin:34px 0">
<svg viewBox="0 0 560 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Remediation order diagram">
  <rect x="0" y="0" width="560" height="220" fill="#11111b" rx="10"/>
  <text x="280" y="30" fill="#cdd6f4" font-family="monospace" font-size="15" text-anchor="middle">Remediation, in the only order that works</text>
  <rect x="24" y="58" width="118" height="100" rx="8" fill="#1e1e2e" stroke="#f38ba8" stroke-width="2"/>
  <text x="83" y="86" fill="#f38ba8" font-family="monospace" font-size="22" text-anchor="middle">1</text>
  <text x="83" y="112" fill="#cdd6f4" font-family="monospace" font-size="12" text-anchor="middle">ROTATE</text>
  <text x="83" y="132" fill="#6c7086" font-family="monospace" font-size="10" text-anchor="middle">kill the value</text>
  <rect x="156" y="58" width="118" height="100" rx="8" fill="#1e1e2e" stroke="#fab387" stroke-width="2"/>
  <text x="215" y="86" fill="#fab387" font-family="monospace" font-size="22" text-anchor="middle">2</text>
  <text x="215" y="112" fill="#cdd6f4" font-family="monospace" font-size="12" text-anchor="middle">SCRUB</text>
  <text x="215" y="132" fill="#6c7086" font-family="monospace" font-size="10" text-anchor="middle">purge history</text>
  <rect x="288" y="58" width="118" height="100" rx="8" fill="#1e1e2e" stroke="#89b4fa" stroke-width="2"/>
  <text x="347" y="86" fill="#89b4fa" font-family="monospace" font-size="22" text-anchor="middle">3</text>
  <text x="347" y="112" fill="#cdd6f4" font-family="monospace" font-size="12" text-anchor="middle">RELOCATE</text>
  <text x="347" y="132" fill="#6c7086" font-family="monospace" font-size="10" text-anchor="middle">secrets mgr</text>
  <rect x="420" y="58" width="118" height="100" rx="8" fill="#1e1e2e" stroke="#a6e3a1" stroke-width="2"/>
  <text x="479" y="86" fill="#a6e3a1" font-family="monospace" font-size="22" text-anchor="middle">4</text>
  <text x="479" y="112" fill="#cdd6f4" font-family="monospace" font-size="12" text-anchor="middle">PREVENT</text>
  <text x="479" y="132" fill="#6c7086" font-family="monospace" font-size="10" text-anchor="middle">pre-commit scan</text>
<p><text x="149" y="113" fill="#45475a" font-family="monospace" font-size="18" text-anchor="middle">►</text>
<text x="281" y="113" fill="#45475a" font-family="monospace" font-size="18" text-anchor="middle">►</text>
<text x="413" y="113" fill="#45475a" font-family="monospace" font-size="18" text-anchor="middle">►</text></p>
<p><text x="280" y="195" fill="#cba6f7" font-family="monospace" font-size="12" text-anchor="middle">skip step 1 &amp; steps 2-4 are theater</text>
</svg></p>
<figcaption style="color:#6c7086;font-size:14px;margin-top:8px">Deleting the file is not on this list. It was never remediation.</figcaption>
</figure>
<h2 id="the-fix">The fix</h2>
<p><strong>1. Rotate, immediately.</strong> Before anything else, I logged into the provider, revoked the credential, and issued a fresh one. The old value is now a dead string. Whoever has it has nothing.</p>
<p><strong>2. Purge it from history.</strong> With the value already dead, I scrubbed it so it stops scaring the next auditor (me, in six months). <code>git filter-repo</code> is the modern tool; BFG works too.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># install once: pip install git-filter-repo</span>
</span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#39;the_secret_string==&gt;REDACTED&#39;</span> &gt; replacements.txt
</span></span><span style="display:flex;"><span>git filter-repo --replace-text replacements.txt
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># then force the rewritten history out</span>
</span></span><span style="display:flex;"><span>git push --force --all
</span></span><span style="display:flex;"><span>git push --force --tags
</span></span></code></pre></div><p>Every collaborator re-clones after this. A force-push that rewrites history will wreck anyone&rsquo;s local copy — warn them first.</p>
<p><strong>3. Move secrets out of the repo for good.</strong> The config now reads from the environment, injected at runtime from a secrets manager. Nothing sensitive touches the tree.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># .env stays OUT of git</span>
</span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#39;.env&#39;</span> &gt;&gt; .gitignore
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># app reads from the environment, value lives in the secrets manager</span>
</span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">export</span> <span style="color:#8be9fd;font-style:italic">API_TOKEN</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;</span><span style="color:#ff79c6">$(</span>vault kv get -field<span style="color:#ff79c6">=</span>token secret/app<span style="color:#ff79c6">)</span><span style="color:#f1fa8c">&#34;</span>
</span></span></code></pre></div><p><strong>4. Make it impossible to repeat.</strong> A pre-commit hook scans every staged change so a secret never reaches a commit in the first place.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># .pre-commit-config.yaml</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">#  - repo: https://github.com/gitleaks/gitleaks</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">#    rev: v8.18.0</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">#    hooks: [{ id: gitleaks }]</span>
</span></span><span style="display:flex;"><span>pre-commit install
</span></span><span style="display:flex;"><span>gitleaks detect --source . --verbose
</span></span></code></pre></div><h2 id="why-it-happened">Why it happened</h2>
<p>No villain here. Just the most ordinary mistake in the trade.</p>
<p>Months ago, mid-deploy, I needed the thing to <em>work now</em>. I hardcoded the credential &ldquo;just to test,&rdquo; told myself I&rsquo;d pull it out before committing, got interrupted, and committed everything with a <code>git add .</code>. The TODO never came back. The repo was private, so it felt safe — and &ldquo;private&rdquo; is exactly the lie that lets these things rot in history for half a year.</p>
<p>Private isn&rsquo;t a control. It&rsquo;s a setting someone can flip.</p>
<h2 id="takeaways">Takeaways</h2>
<ul>
<li><strong>A secret is burned the moment it&rsquo;s committed.</strong> Rotate first, always. Treat the value as public from commit time forward — because effectively it is.</li>
<li><strong>Deleting the file is not remediation.</strong> The credential lives in every old commit. You must rewrite history (<code>git filter-repo</code> / BFG) and force-push, or it&rsquo;s still there.</li>
<li><strong>Secrets belong in a manager, not the tree.</strong> Env-injected values, <code>.gitignore</code> your <code>.env</code>, and never let credentials and code share a home.</li>
<li><strong>Automate the catch.</strong> A <code>gitleaks</code>/<code>trufflehog</code> pre-commit hook stops the next leak before it&rsquo;s a commit — humans forget, hooks don&rsquo;t.</li>
<li><strong>&ldquo;Private repo&rdquo; is not a security boundary.</strong> Audit your own repos like you&rsquo;d audit a stranger&rsquo;s. The author line might surprise you.</li>
</ul>
]]></content:encoded></item><item><title>90s Kid - The Day We Got 56k</title><link>https://errorzap.com/posts/90s-kid-the-56k-upgrade/</link><pubDate>Wed, 20 May 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/90s-kid-the-56k-upgrade/</guid><description>The afternoon the modem screamed a little faster and the whole house held its breath.</description><content:encoded><![CDATA[<figure style="margin:0 0 28px">
<svg viewBox="0 0 800 240" xmlns="http://www.w3.org/2000/svg" style="width:100%;border-radius:14px">
 <defs>
  <linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#1e1e2e"/><stop offset="1" stop-color="#11111b"/></linearGradient>
  <linearGradient id="sun" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f9e2af"/><stop offset="0.5" stop-color="#fab387"/><stop offset="1" stop-color="#f38ba8"/></linearGradient>
 </defs>
 <rect width="800" height="240" fill="url(#sky)"/>
 <circle cx="640" cy="150" r="74" fill="url(#sun)"/><rect x="566" y="118" width="148" height="4" fill="#11111b"/><rect x="566" y="127" width="148" height="4" fill="#11111b"/><rect x="566" y="136" width="148" height="4" fill="#11111b"/><rect x="566" y="145" width="148" height="4" fill="#11111b"/><rect x="566" y="154" width="148" height="4" fill="#11111b"/><rect x="566" y="163" width="148" height="4" fill="#11111b"/><rect x="566" y="172" width="148" height="4" fill="#11111b"/>
 <line x1="-200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="280" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="360" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="440" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="520" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="600" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="680" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="760" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="840" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="920" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="1000" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="0" y1="178" x2="800" y2="178" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="184" x2="800" y2="184" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="193" x2="800" y2="193" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="206" x2="800" y2="206" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="227" x2="800" y2="227" stroke="#94e2d5" stroke-width="1" opacity="0.22"/>
 <text x="48" y="150" font-size="78">📶</text>
 <text x="150" y="96" font-family="ui-monospace,monospace" font-size="14" fill="#cba6f7" letter-spacing="5">90s KID //</text>
 <text x="150" y="140" font-family="-apple-system,Segoe UI,sans-serif" font-size="30" font-weight="800" fill="#cdd6f4">The Day We Got 56k</text>
 <rect x="0" y="236" width="800" height="4" fill="#f38ba8"/>
</svg></figure>
<h2 id="the-afternoon-the-screaming-got-faster">The Afternoon the Screaming Got Faster</h2>
<p>You remember the day.</p>
<p>The beige box showed up.<br>
Smaller than you expected.<br>
A little plastic brick with a single blinking light, riding inside the family computer like a smuggled treasure.</p>
<p><strong>56k.</strong></p>
<p>You didn&rsquo;t know what a &ldquo;k&rdquo; was.<br>
You just knew the old one was a &ldquo;33.6&rdquo; and this one was a <em>bigger number</em>, and bigger numbers meant <strong>good</strong>.</p>
<p>Dad said it like a magic word.<br>
<em>&ldquo;We got 56k now.&rdquo;</em></p>
<p>The grown-ups nodded. Somebody whistled.<br>
You had no idea what just happened, but you knew it was a Big Deal, the kind of Big Deal that came with a receipt and a warning not to touch anything.</p>
<p>Here&rsquo;s what we thought it was:</p>
<ul>
<li>a <strong>machine that could go faster if you believed hard enough</strong></li>
<li>powered by a sound — that holy shriek, that robot gargling underwater</li>
<li>forbidden during dinner, sacred after homework</li>
<li>somehow connected to the <em>telephone</em>, which meant Grandma could ruin everything by picking up to call about a casserole</li>
</ul>
<p>You&rsquo;d lean in close while it connected.<br>
<em>Bee-doo. Bee-doo. Skreeeee. Kshhhhhhhh.</em><br>
And you&rsquo;d hold your breath like the noise might notice you and stop.</p>
<p>Here&rsquo;s what it <strong>actually</strong> was.</p>
<p>That beautiful screaming was a <strong>handshake</strong> — two modems negotiating, out loud, in real time. Your modem and the one at the other end were literally singing tones at each other to figure out the cleanest way to talk over a copper phone line built for <em>voices</em>, not data.</p>
<p>The 56k part? A gorgeous little cheat. The phone network had quietly gone digital in the middle, so your downloads could ride that clean digital signal almost all the way to your house — that&rsquo;s how you squeezed <strong>56 kilobits a second</strong> out of a wire designed for chitchat. (Uploads were stuck slower. The magic only ran one way.)</p>
<p>And that&rsquo;s why it tied up the phone: your <strong>modem</strong> was <em>modulating</em> and <em>demodulating</em> — turning ones and zeros into sound and back again — using the exact same line as the kitchen phone. One line. One conversation at a time. The internet literally <em>was</em> a phone call.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>  +-----------------------------+
</span></span><span style="display:flex;"><span>  |  U.S. Robotics  56K        o |   &lt;- the one blinky light
</span></span><span style="display:flex;"><span>  |  [CD][RD][SD][TR][MR][AA]    |
</span></span><span style="display:flex;"><span>  +--__----------------------__--+
</span></span><span style="display:flex;"><span>     ||  bee-doo  skreeee     ||
</span></span><span style="display:flex;"><span>   ~~||~~~~~~~~~~~~~~~~~~~~~~~~||~~
</span></span><span style="display:flex;"><span>     ||   ___        ___      ||
</span></span><span style="display:flex;"><span>     \\__/   \______/   \____ //   &lt;- the handshake, screaming
</span></span></code></pre></div><p>Sure, the picture loaded <strong>one agonizing horizontal strip at a time</strong>, top to bottom, and somebody always picked up the phone right before the last strip. Sure, &ldquo;going online&rdquo; was an <em>event</em> you scheduled around. But for one shining afternoon, you were the fastest house on the block.</p>
<p>And here&rsquo;s the thing — that handshake never really died.</p>
<p>Your phone still negotiates with a cell tower. Your laptop still does a little dance with the Wi-Fi router. Every &ldquo;connecting…&rdquo; spinner you&rsquo;ve ever cursed at is the same conversation, just gone silent. We made it faster, we made it invisible, we even made it wireless — but somewhere under all of it, two machines are still saying <em>hi, how do we talk?</em> before they trust each other with your data.</p>
<p>We just don&rsquo;t get to <strong>hear</strong> it scream anymore.</p>
<p>And honestly?</p>
<p>We kind of miss it.</p>
]]></content:encoded></item><item><title>The PWA That Froze When Offline</title><link>https://errorzap.com/posts/the-pwa-that-froze-when-offline/</link><pubDate>Wed, 20 May 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/the-pwa-that-froze-when-offline/</guid><description>A field tech opened an &amp;lsquo;offline-capable&amp;rsquo; app in a dead zone and got a blank screen that hung forever — because the service worker was politely waiting for a network that was never coming.</description><content:encoded><![CDATA[<figure style="text-align:center;margin:0 0 30px"><img src="hero.png" alt="The PWA That Froze When Offline" style="max-width:520px;width:100%;border-radius:14px"/></figure>
<p>A field tech messaged me from a basement with no signal: &ldquo;Your app just hangs. White screen. Forever.&rdquo;</p>
<p>That&rsquo;s a fun one to read on a Tuesday, because the entire point of that app was that it worked offline. Progressive Web App. Service worker. Cached shell. The whole pitch was &ldquo;open it anywhere, signal or not.&rdquo;</p>
<p>So I did what he did. I put my phone in airplane mode and tapped the icon.</p>
<p>Blank screen. Spinner that never spun. The app didn&rsquo;t crash — it just <em>waited</em>. Patiently. Forever. Like a golden retriever staring at a door that&rsquo;s never going to open.</p>
<h2 id="the-investigation">The investigation</h2>
<p>On a normal connection, everything was perfect. Load times snappy, cache populated, Lighthouse happy. The bug only showed up with the network truly gone — not slow, <em>gone</em>.</p>
<p>That asymmetry is the tell. If something works online and dies offline, you stop blaming the app and start blaming whatever sits between the app and the cache. For a PWA, that&rsquo;s the service worker&rsquo;s <code>fetch</code> handler.</p>
<p>I pulled it up. Here&rsquo;s the offending strategy, paraphrased:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-js" data-lang="js"><span style="display:flex;"><span><span style="color:#6272a4">// service-worker.js — the original sin
</span></span></span><span style="display:flex;"><span>self.addEventListener(<span style="color:#f1fa8c">&#39;fetch&#39;</span>, (event) =&gt; {
</span></span><span style="display:flex;"><span>  event.respondWith(
</span></span><span style="display:flex;"><span>    fetch(event.request)                 <span style="color:#6272a4">// always try the network first
</span></span></span><span style="display:flex;"><span>      .then((res) =&gt; {
</span></span><span style="display:flex;"><span>        <span style="color:#ff79c6">const</span> copy <span style="color:#ff79c6">=</span> res.clone();
</span></span><span style="display:flex;"><span>        caches.open(<span style="color:#f1fa8c">&#39;app-shell&#39;</span>).then((c) =&gt; c.put(event.request, copy));
</span></span><span style="display:flex;"><span>        <span style="color:#ff79c6">return</span> res;
</span></span><span style="display:flex;"><span>      })
</span></span><span style="display:flex;"><span>      .<span style="color:#ff79c6">catch</span>(() =&gt; caches.match(event.request))  <span style="color:#6272a4">// fall back to cache
</span></span></span><span style="display:flex;"><span>  );
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><p>Network-first. Looks reasonable. Hit the network, cache the response, and if the network fails, serve from cache. The <code>.catch()</code> is right there. So why doesn&rsquo;t it fall back?</p>
<h2 id="the-aha">The &ldquo;aha&rdquo;</h2>
<p>Because offline doesn&rsquo;t <em>fail</em>. Not quickly, anyway.</p>
<p>When you&rsquo;re truly offline, that <code>fetch()</code> doesn&rsquo;t reject in 20 milliseconds with a clean &ldquo;no route to host.&rdquo; Depending on the platform, it can sit there for tens of seconds — or effectively never resolve — before the OS decides it&rsquo;s done. The <code>.catch()</code> is waiting for a rejection that takes its sweet time. Meanwhile, the app shell never paints, because the navigation request for <code>index.html</code> is stuck in that same limbo.</p>
<p>The cache was fine. The fallback logic was fine. The problem was that the fallback never got to <em>run</em>, because nothing told the network attempt to give up.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>       ONLINE                          OFFLINE (the bug)
</span></span><span style="display:flex;"><span>  ┌──────────────┐                ┌──────────────────────┐
</span></span><span style="display:flex;"><span>  │  fetch()     │                │  fetch()             │
</span></span><span style="display:flex;"><span>  │     │        │                │     │                │
</span></span><span style="display:flex;"><span>  │     ▼ ~80ms  │                │     ▼                │
</span></span><span style="display:flex;"><span>  │  ✓ response  │                │   ...waiting...      │
</span></span><span style="display:flex;"><span>  │     │        │                │   ...waiting...      │
</span></span><span style="display:flex;"><span>  │     ▼        │                │   ...waiting...   ▓  │
</span></span><span style="display:flex;"><span>  │  cache.put   │                │   (never rejects) ▓  │
</span></span><span style="display:flex;"><span>  │     │        │                │     │             ▓  │
</span></span><span style="display:flex;"><span>  │     ▼        │                │     ✗ .catch()       │
</span></span><span style="display:flex;"><span>  │  paint ✓     │                │       never fires    │
</span></span><span style="display:flex;"><span>  └──────────────┘                └──────────────────────┘
</span></span><span style="display:flex;"><span>                                   app shell never paints
</span></span></code></pre></div><figure style="text-align:center;margin:34px 0">
<svg viewBox="0 0 560 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Fetch with a timeout racing against a cache fallback">
  <rect x="0" y="0" width="560" height="220" rx="10" fill="#1e1e2e"/>
  <text x="280" y="30" fill="#cdd6f4" font-family="monospace" font-size="15" text-anchor="middle">Promise.race — first one home wins</text>
  <!-- network branch -->
  <rect x="40" y="60" width="200" height="46" rx="8" fill="#313244" stroke="#45475a"/>
  <text x="140" y="82" fill="#89b4fa" font-family="monospace" font-size="13" text-anchor="middle">fetch(request)</text>
  <text x="140" y="98" fill="#6c7086" font-family="monospace" font-size="11" text-anchor="middle">may never resolve</text>
  <!-- timeout branch -->
  <rect x="40" y="120" width="200" height="46" rx="8" fill="#313244" stroke="#45475a"/>
  <text x="140" y="142" fill="#fab387" font-family="monospace" font-size="13" text-anchor="middle">timeout(3000ms)</text>
  <text x="140" y="158" fill="#6c7086" font-family="monospace" font-size="11" text-anchor="middle">rejects fast</text>
  <!-- race node -->
  <circle cx="330" cy="113" r="26" fill="#1e1e2e" stroke="#cba6f7" stroke-width="2"/>
  <text x="330" y="118" fill="#cba6f7" font-family="monospace" font-size="12" text-anchor="middle">race</text>
  <!-- arrows in -->
  <line x1="240" y1="83" x2="306" y2="105" stroke="#89b4fa" stroke-width="2"/>
  <line x1="240" y1="143" x2="306" y2="121" stroke="#fab387" stroke-width="2"/>
  <!-- fallback -->
  <rect x="400" y="90" width="130" height="46" rx="8" fill="#313244" stroke="#a6e3a1"/>
  <text x="465" y="112" fill="#a6e3a1" font-family="monospace" font-size="13" text-anchor="middle">cache.match ✓</text>
  <text x="465" y="128" fill="#6c7086" font-family="monospace" font-size="11" text-anchor="middle">paints now</text>
  <line x1="356" y1="113" x2="396" y2="113" stroke="#a6e3a1" stroke-width="2" marker-end="url(#a)"/>
  <defs>
    <marker id="a" markerWidth="8" markerHeight="8" refX="6" refY="4" orient="auto">
      <path d="M0,0 L8,4 L0,8 z" fill="#a6e3a1"/>
    </marker>
  </defs>
</svg>
<figcaption style="color:#6c7086;font-size:14px;margin-top:8px">Race the network against a short timeout — whichever finishes first decides whether you wait or serve the cache.</figcaption>
</figure>
<h2 id="the-fix">The fix</h2>
<p>Two changes. First, never let a <code>fetch</code> in the service worker run unbounded — race it against a timeout so it fails <em>fast</em> and the cache fallback actually fires:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-js" data-lang="js"><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">function</span> fetchWithTimeout(request, ms <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">3000</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">return</span> <span style="color:#8be9fd;font-style:italic">Promise</span>.race([
</span></span><span style="display:flex;"><span>    fetch(request),
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">new</span> <span style="color:#8be9fd;font-style:italic">Promise</span>((_, reject) =&gt;
</span></span><span style="display:flex;"><span>      setTimeout(() =&gt; reject(<span style="color:#ff79c6">new</span> <span style="color:#8be9fd;font-style:italic">Error</span>(<span style="color:#f1fa8c">&#39;sw-fetch-timeout&#39;</span>)), ms)
</span></span><span style="display:flex;"><span>    ),
</span></span><span style="display:flex;"><span>  ]);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Second, stop being network-first for the app shell. Serve from cache <em>immediately</em>, then refresh the cache in the background — classic stale-while-revalidate:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-js" data-lang="js"><span style="display:flex;"><span>self.addEventListener(<span style="color:#f1fa8c">&#39;fetch&#39;</span>, (event) =&gt; {
</span></span><span style="display:flex;"><span>  event.respondWith(
</span></span><span style="display:flex;"><span>    caches.open(<span style="color:#f1fa8c">&#39;app-shell&#39;</span>).then(<span style="color:#ff79c6">async</span> (cache) =&gt; {
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">const</span> cached <span style="color:#ff79c6">=</span> <span style="color:#ff79c6">await</span> cache.match(event.request);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">const</span> network <span style="color:#ff79c6">=</span> fetchWithTimeout(event.request)
</span></span><span style="display:flex;"><span>        .then((res) =&gt; {
</span></span><span style="display:flex;"><span>          cache.put(event.request, res.clone()); <span style="color:#6272a4">// refresh in background
</span></span></span><span style="display:flex;"><span>          <span style="color:#ff79c6">return</span> res;
</span></span><span style="display:flex;"><span>        })
</span></span><span style="display:flex;"><span>        .<span style="color:#ff79c6">catch</span>(() =&gt; cached);   <span style="color:#6272a4">// offline or slow? cached is already in hand
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#ff79c6">return</span> cached <span style="color:#ff79c6">||</span> network; <span style="color:#6272a4">// paint instantly if we have it
</span></span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>  );
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><p>Now the shell paints from cache on the first frame, online or off. The network update happens behind the scenes, and if it&rsquo;s slow or absent, the timeout cuts it loose in three seconds instead of never.</p>
<p>Airplane mode, tap the icon, instant load. The basement tech got his app back.</p>
<h2 id="why-it-happened">Why it happened</h2>
<p>Network-first <em>feels</em> safe — &ldquo;always get the freshest thing, fall back to cache if it breaks.&rdquo; But it quietly assumes the network fails <em>promptly</em>. Offline doesn&rsquo;t fail promptly. It stalls. And a stalled promise with no timeout is just an infinite loading screen wearing a <code>.catch()</code> as a disguise.</p>
<p>The app shell — the HTML, JS, CSS that make the thing <em>exist</em> — should never depend on a live connection to render. That&rsquo;s the entire premise of &ldquo;offline-capable.&rdquo; We&rsquo;d built the cache and then put a blocking network call in front of it.</p>
<h2 id="takeaways">Takeaways</h2>
<ul>
<li><strong>Never let a service-worker <code>fetch</code> run without a timeout.</strong> <code>Promise.race</code> it against a <code>setTimeout</code> reject so offline fails in milliseconds, not minutes.</li>
<li><strong>Serve the app shell cache-first or stale-while-revalidate</strong> — never network-first. The UI should paint from cache on the first frame, every time.</li>
<li><strong>Test with the network truly gone, not just throttled.</strong> Slow networks reject; dead networks stall. They&rsquo;re different failure modes and only the dead one exposes this bug.</li>
<li><strong>A <code>.catch()</code> is not a fallback if nothing triggers the rejection.</strong> Error handling only runs when the error actually fires — and &ldquo;offline&rdquo; often doesn&rsquo;t fire fast.</li>
<li><strong>&ldquo;Offline-capable&rdquo; is a claim you have to test in airplane mode,</strong> not a checkbox you tick because you registered a service worker.</li>
</ul>
]]></content:encoded></item><item><title>The API Loop That Ran Up the Bill Overnight</title><link>https://errorzap.com/posts/the-api-loop-that-ran-up-the-bill-overnight/</link><pubDate>Mon, 18 May 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/the-api-loop-that-ran-up-the-bill-overnight/</guid><description>An unattended automation found a paid LLM endpoint, lost its mind, and hammered it thousands of times while everyone slept.</description><content:encoded><![CDATA[<figure style="text-align:center;margin:0 0 30px"><img src="hero.png" alt="The API Loop That Ran Up the Bill Overnight" style="max-width:520px;width:100%;border-radius:14px"/></figure>
<p>The cron job ran at 11 PM. By the time I poured coffee the next morning, it had called a paid LLM API a few thousand times.</p>
<p>Nobody touched it. Nobody approved it. It just sat there in the dark, retrying, retrying, retrying — like a vending machine eating a stuck dollar bill, except the dollar bill was billable tokens.</p>
<p>I didn&rsquo;t find it because of a clever alert. I found it because the provider dashboard had a graph that looked like a cliff face.</p>
<h2 id="the-scene">The scene</h2>
<p>I run a small fleet of unattended automations for a client. One of them enriches records overnight by sending them through a metered LLM endpoint. Cheap per call. Boring. The kind of job you set up once and forget — which is exactly the problem.</p>
<p>That night, the upstream service it depended on got flaky. A handful of calls returned errors. The loop did what badly-written loops always do.</p>
<p>It tried again. Immediately. Forever.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>┌────────────────────────────────────────────────────┐
</span></span><span style="display:flex;"><span>│  THE LOOP (as written)                             │
</span></span><span style="display:flex;"><span>│                                                    │
</span></span><span style="display:flex;"><span>│   ┌──────────┐   error   ┌──────────────┐          │
</span></span><span style="display:flex;"><span>│   │ call API │ ────────► │ retry now    │          │
</span></span><span style="display:flex;"><span>│   └────┬─────┘           └──────┬───────┘          │
</span></span><span style="display:flex;"><span>│        ▲                        │                  │
</span></span><span style="display:flex;"><span>│        └────────────────────────┘                  │
</span></span><span style="display:flex;"><span>│        no backoff · no cap · no kill switch        │
</span></span><span style="display:flex;"><span>│                                                    │
</span></span><span style="display:flex;"><span>│   result: ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓  thousands of hits │
</span></span><span style="display:flex;"><span>└────────────────────────────────────────────────────┘
</span></span></code></pre></div><p>No <code>sleep</code>. No max-retry counter. No circuit breaker. Every failure became an instant request, and every instant request became another line on the invoice.</p>
<h2 id="the-investigation">The investigation</h2>
<p>First thing: confirm it&rsquo;s us and not a stolen key. I pulled the provider&rsquo;s usage view and bucketed by hour.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># usage exploded between 23:00 and 07:00 — exactly the cron window</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># logs from the job host told the same story</span>
</span></span><span style="display:flex;"><span>journalctl -u record-enrich.service --since <span style="color:#f1fa8c">&#34;yesterday 23:00&#34;</span> <span style="color:#f1fa8c">\
</span></span></span><span style="display:flex;"><span>  | grep -c <span style="color:#f1fa8c">&#34;POST /v1/&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># -&gt; 4,300-something. Overnight. For a job that should make ~80 calls.</span>
</span></span></code></pre></div><p>Same source IP. Same user-agent. Same service. Not a breach — a self-inflicted wound. Somehow that&rsquo;s worse, because it means the call was coming from inside the house.</p>
<h2 id="the-aha">The &ldquo;aha&rdquo;</h2>
<p>The smoking gun was four lines of code. Paraphrased:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#ff79c6">while</span> <span style="color:#ff79c6">not</span> done:
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">try</span>:
</span></span><span style="display:flex;"><span>        resp <span style="color:#ff79c6">=</span> client<span style="color:#ff79c6">.</span>complete(payload)
</span></span><span style="display:flex;"><span>        done <span style="color:#ff79c6">=</span> <span style="color:#ff79c6">True</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">except</span> Exception:
</span></span><span style="display:flex;"><span>        <span style="color:#ff79c6">continue</span>   <span style="color:#6272a4"># &lt;-- the whole disaster, right here</span>
</span></span></code></pre></div><p><code>continue</code>. No delay. No ceiling. The instant the upstream hiccuped, this turned into a tight spin loop firing paid requests as fast as the network would carry them. A retry without backoff isn&rsquo;t resilience — it&rsquo;s a denial-of-wallet attack you launch against yourself.</p>
<figure style="text-align:center;margin:34px 0">
<svg viewBox="0 0 560 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Spend rising overnight until a quota cap flattens it">
  <rect x="0" y="0" width="560" height="220" rx="10" fill="#11111b"/>
  <rect x="0" y="0" width="560" height="220" rx="10" fill="#1e1e2e" opacity="0.5"/>
  <line x1="60" y1="30" x2="60" y2="180" stroke="#45475a" stroke-width="1.5"/>
  <line x1="60" y1="180" x2="520" y2="180" stroke="#45475a" stroke-width="1.5"/>
  <text x="60" y="22" fill="#6c7086" font-family="monospace" font-size="11">$ spend</text>
  <text x="470" y="198" fill="#6c7086" font-family="monospace" font-size="11">time →</text>
  <polyline points="60,176 150,172 230,168 300,150 340,110 365,60 380,40" fill="none" stroke="#f38ba8" stroke-width="3"/>
  <line x1="380" y1="40" x2="520" y2="40" stroke="#fab387" stroke-width="2" stroke-dasharray="6 5"/>
  <text x="384" y="33" fill="#fab387" font-family="monospace" font-size="11">hard quota cap</text>
  <circle cx="380" cy="40" r="4.5" fill="#cba6f7"/>
  <text x="120" y="150" fill="#89b4fa" font-family="monospace" font-size="11">23:00 cron starts</text>
  <text x="300" y="200" fill="#a6e3a1" font-family="monospace" font-size="11">07:00 caught ✓</text>
</svg>
<figcaption style="color:#6c7086;font-size:14px;margin-top:8px">Cost climbs unattended all night; a provider-side quota cap is the only thing that would have flattened it before morning.</figcaption>
</figure>
<h2 id="the-fix">The fix</h2>
<p>I treated the key as compromised even though it wasn&rsquo;t, because the behavior was indistinguishable from a leak. Containment first, blame later.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># 1. Rotate the key immediately — old one dies on the spot</span>
</span></span><span style="display:flex;"><span>provider keys rotate --name record-enrich --revoke-old
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 2. Disable the API entirely at the provider while I fix the code.</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">#    Kill the bleeding before patching the artery.</span>
</span></span><span style="display:flex;"><span>provider api disable --service record-enrich
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 3. Re-enable WITH a hard cap + budget alert. The cap is the real fix.</span>
</span></span><span style="display:flex;"><span>provider budget <span style="color:#8be9fd;font-style:italic">set</span> --service record-enrich <span style="color:#f1fa8c">\
</span></span></span><span style="display:flex;"><span>  --hard-limit-usd <span style="color:#bd93f9">25</span> --period monthly
</span></span><span style="display:flex;"><span>provider alerts <span style="color:#8be9fd;font-style:italic">set</span> --service record-enrich <span style="color:#f1fa8c">\
</span></span></span><span style="display:flex;"><span>  --notify-at 50% --notify-at 90% --channel telegram
</span></span></code></pre></div><p>Then the code got the guardrails it should have shipped with:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#ff79c6">import</span> time
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>MAX_RETRIES <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">5</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">for</span> attempt <span style="color:#ff79c6">in</span> <span style="color:#8be9fd;font-style:italic">range</span>(MAX_RETRIES):
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">try</span>:
</span></span><span style="display:flex;"><span>        resp <span style="color:#ff79c6">=</span> client<span style="color:#ff79c6">.</span>complete(payload)
</span></span><span style="display:flex;"><span>        <span style="color:#ff79c6">break</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">except</span> TransientError:
</span></span><span style="display:flex;"><span>        time<span style="color:#ff79c6">.</span>sleep(<span style="color:#8be9fd;font-style:italic">min</span>(<span style="color:#bd93f9">2</span> <span style="color:#ff79c6">**</span> attempt, <span style="color:#bd93f9">30</span>))   <span style="color:#6272a4"># exponential backoff, capped</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">else</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">raise</span> RuntimeError(<span style="color:#f1fa8c">&#34;gave up after retries — failing loud, not looping&#34;</span>)
</span></span></code></pre></div><p>Backoff. A retry ceiling. And a loud failure instead of a silent infinite spin. The provider cap is the seatbelt; this is actually steering the car.</p>
<h2 id="why-it-happened">Why it happened</h2>
<p>It happened because &ldquo;it&rsquo;s just a small overnight job&rdquo; is the exact mindset that ships uncapped loops at paid endpoints. The cost-per-call was trivial, so nobody did the multiplication. Trivial times infinity is still a number you have to pay.</p>
<p>The code had no concept of &ldquo;too many.&rdquo; The provider had no concept of &ldquo;enough.&rdquo; With neither a ceiling in the app nor a ceiling at the wallet, the only limiter left was how fast the network could move — and that&rsquo;s a throttle on the damage, not a budget.</p>
<h2 id="takeaways">Takeaways</h2>
<ul>
<li><strong>Put a hard spend cap at the provider.</strong> It&rsquo;s the only limit a runaway loop can&rsquo;t out-code. Set it before you write the first request.</li>
<li><strong>Backoff and a max-retry count are not optional.</strong> A retry without delay or ceiling is a self-DoS. <code>continue</code> on an exception is a loaded gun.</li>
<li><strong>Wire usage alerts at 50% and 90%.</strong> You want a phone buzz at midnight, not a graph shaped like a cliff at breakfast.</li>
<li><strong>Treat weird usage as a leak until proven otherwise.</strong> Rotate the key and disable the endpoint first; debug the code second. Containment beats curiosity.</li>
<li><strong>&ldquo;Small overnight job&rdquo; is a smell.</strong> Anything unattended that touches a metered API gets a kill switch, a cap, and an alert — or it doesn&rsquo;t get deployed.</li>
</ul>
]]></content:encoded></item><item><title>One Caddyfile, Infinite Subdomains</title><link>https://errorzap.com/posts/one-caddyfile-infinite-subdomains/</link><pubDate>Sun, 17 May 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/one-caddyfile-infinite-subdomains/</guid><description>A dozen self-hosted apps, a dozen ugly ports, zero valid certs — until one wildcard cert turned &amp;rsquo;expose another app&amp;rsquo; into a three-line chore.</description><content:encoded><![CDATA[<figure style="text-align:center;margin:0 0 30px"><img src="hero.png" alt="One Caddyfile, Infinite Subdomains" style="max-width:520px;width:100%;border-radius:14px"/></figure>
<p>It started, like most of my regrets, with a bookmark folder.</p>
<p><code>app-one:3000</code>. <code>dashboard:8501</code>. <code>that-thing-i-forgot:8080</code>. A dozen self-hosted apps, each squatting on its own port, each throwing a browser security warning because I&rsquo;d long since stopped pretending to manage individual certs. Half were self-signed. Half were plain HTTP. All of them were ugly.</p>
<p>Every new app was the same ritual: pick a port, pray it&rsquo;s free, wire up TLS by hand, give up on TLS, add another <code>:PORT</code> bookmark, lose it. The homelab worked. It just looked like a ransom note.</p>
<p>So I sat down to do it properly. Once.</p>
<h2 id="the-investigation">The investigation</h2>
<p>The core problem wasn&rsquo;t the apps. It was that I had <strong>N apps and N TLS problems</strong>. Every service believed it was personally responsible for the open internet. None of them were good at it.</p>
<p>Here&rsquo;s the mess, drawn honestly:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>        BEFORE                              AFTER
</span></span><span style="display:flex;"><span>  ┌──────────────────┐            ┌──────────────────────────┐
</span></span><span style="display:flex;"><span>  │  browser         │            │  browser                 │
</span></span><span style="display:flex;"><span>  └───┬───┬───┬──────┘            └──────────┬───────────────┘
</span></span><span style="display:flex;"><span>      │   │   │   (which port?)              │  one name, valid TLS
</span></span><span style="display:flex;"><span>      ▼   ▼   ▼                              ▼
</span></span><span style="display:flex;"><span>   :3000 :8501 :8080                    ┌─────────┐
</span></span><span style="display:flex;"><span>   self-signed / http / ???            │  Caddy   │  *.lab.example.tld
</span></span><span style="display:flex;"><span>   each app owns its own cert          └──┬──┬──┬─┘  wildcard cert
</span></span><span style="display:flex;"><span>                                          │  │  │
</span></span><span style="display:flex;"><span>                                          ▼  ▼  ▼
</span></span><span style="display:flex;"><span>                                127.0.0.1:3000/8501/8080
</span></span><span style="display:flex;"><span>                                  (apps bound to localhost)
</span></span></code></pre></div><p>The fix wasn&rsquo;t to make every app better at certificates. It was to make <strong>none</strong> of them do certificates. Put one thing in front. Let it own TLS, routing, and renewal. Bind everything else to <code>127.0.0.1</code> so the only thing facing the world is the proxy.</p>
<p>That &ldquo;one thing&rdquo; is a reverse proxy. Mine&rsquo;s Caddy.</p>
<h2 id="the-aha">The &ldquo;aha&rdquo;</h2>
<p>The trick that makes it sing is a <strong>wildcard certificate</strong> — <code>*.lab.example.tld</code>, one cert that&rsquo;s valid for every subdomain I&rsquo;ll ever invent — obtained over a <strong>DNS-01 challenge</strong> instead of the usual HTTP-01.</p>
<p>DNS-01 matters here. HTTP-01 proves you own a name by serving a file on it, which means each name needs to be reachable. DNS-01 proves it by dropping a TXT record at your DNS provider, which means you can mint a wildcard without exposing a single app first. One challenge, infinite names.</p>
<p>Once that clicked, the whole bookmark folder collapsed into one file.</p>
<h2 id="the-fix">The fix</h2>
<p>Caddy needs a DNS-provider plugin to answer the DNS-01 challenge, so build it with the module for your provider:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>xcaddy build <span style="color:#f1fa8c">\
</span></span></span><span style="display:flex;"><span>  --with github.com/caddy-dns/cloudflare
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># verify the module is in there</span>
</span></span><span style="display:flex;"><span>./caddy list-modules | grep dns.providers
</span></span></code></pre></div><p>Then the global block tells Caddy how to get the wildcard, and each app is — genuinely — three lines:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-caddyfile" data-lang="caddyfile"><span style="display:flex;"><span><span style="color:#6272a4"># Caddyfile
</span></span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">email</span> <span style="color:#f1fa8c">admin@example.tld</span><span style="color:#6272a4">
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">    # credentials for the DNS-01 challenge
</span></span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">acme_dns</span> <span style="color:#f1fa8c">cloudflare</span> <span style="color:#f1fa8c">{env.CF_API_TOKEN}</span>
</span></span><span style="display:flex;"><span>}<span style="color:#6272a4">
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4">
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"># the wildcard, requested once, renewed forever
</span></span></span><span style="display:flex;"><span><span style="font-weight:bold">*.lab.example.tld</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">tls</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#ff79c6">dns</span> <span style="color:#f1fa8c">cloudflare</span> <span style="color:#f1fa8c">{env.CF_API_TOKEN}</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="font-weight:bold">app-one.lab.example.tld</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">reverse_proxy</span> 127.0.0.1:<span style="color:#bd93f9">3000</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="font-weight:bold">dashboard.lab.example.tld</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">reverse_proxy</span> 127.0.0.1:<span style="color:#bd93f9">8501</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Reload without dropping connections:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>caddy validate --config /etc/caddy/Caddyfile
</span></span><span style="display:flex;"><span>caddy reload  --config /etc/caddy/Caddyfile
</span></span></code></pre></div><p>Confirm a fresh subdomain actually got real HTTPS:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl -sI https://dashboard.lab.example.tld | head -n1
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># HTTP/2 200</span>
</span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">echo</span> | openssl s_client -connect dashboard.lab.example.tld:443 2&gt;/dev/null <span style="color:#f1fa8c">\
</span></span></span><span style="display:flex;"><span>  | openssl x509 -noout -subject
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># subject=CN = *.lab.example.tld</span>
</span></span></code></pre></div><p>Adding the next app is now: pick a port, add a three-line block, <code>caddy reload</code>. Valid cert, no thought required.</p>
<figure style="text-align:center;margin:34px 0">
<svg viewBox="0 0 560 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Browser to Caddy to localhost apps">
  <rect x="0" y="0" width="560" height="220" rx="10" fill="#11111b"/>
  <rect x="28" y="86" width="104" height="48" rx="8" fill="#1e1e2e" stroke="#45475a"/>
  <text x="80" y="115" fill="#cdd6f4" font-family="monospace" font-size="13" text-anchor="middle">browser</text>
  <rect x="220" y="78" width="120" height="64" rx="8" fill="#313244" stroke="#cba6f7" stroke-width="2"/>
  <text x="280" y="104" fill="#cba6f7" font-family="monospace" font-size="14" text-anchor="middle">Caddy</text>
  <text x="280" y="124" fill="#6c7086" font-family="monospace" font-size="10" text-anchor="middle">*.lab cert</text>
  <line x1="132" y1="110" x2="220" y2="110" stroke="#a6e3a1" stroke-width="2"/>
  <text x="176" y="100" fill="#a6e3a1" font-family="monospace" font-size="10" text-anchor="middle">HTTPS</text>
  <rect x="430" y="36" width="110" height="34" rx="6" fill="#1e1e2e" stroke="#89b4fa"/>
  <text x="485" y="58" fill="#89b4fa" font-family="monospace" font-size="11" text-anchor="middle">:3000</text>
  <rect x="430" y="93" width="110" height="34" rx="6" fill="#1e1e2e" stroke="#fab387"/>
  <text x="485" y="115" fill="#fab387" font-family="monospace" font-size="11" text-anchor="middle">:8501</text>
  <rect x="430" y="150" width="110" height="34" rx="6" fill="#1e1e2e" stroke="#94e2d5"/>
  <text x="485" y="172" fill="#94e2d5" font-family="monospace" font-size="11" text-anchor="middle">:8080</text>
  <line x1="340" y1="100" x2="430" y2="53" stroke="#45475a" stroke-width="1.5"/>
  <line x1="340" y1="110" x2="430" y2="110" stroke="#45475a" stroke-width="1.5"/>
  <line x1="340" y1="120" x2="430" y2="167" stroke="#45475a" stroke-width="1.5"/>
  <text x="404" y="206" fill="#6c7086" font-family="monospace" font-size="10" text-anchor="middle">127.0.0.1 — localhost only</text>
</svg>
<figcaption style="color:#6c7086;font-size:14px;margin-top:8px">One proxy faces the world; the apps stay on localhost and never touch TLS.</figcaption>
</figure>
<h2 id="why-it-happened">Why it happened</h2>
<p>The old setup wasn&rsquo;t dumb, it was just <em>additive</em>. Each app got stood up in isolation, made its own peace with HTTPS, and got a port-shaped bookmark. Nobody ever sat down to factor out the part every app shares: a public face and a certificate.</p>
<p>A reverse proxy is exactly that factoring. TLS, hostname routing, and renewal are cross-cutting concerns — solve them once, in front, and every app inherits the solution for free. The wildcard cert is what makes &ldquo;inherits for free&rdquo; literally true: no per-app issuance, no per-app renewal, no per-app anything.</p>
<h2 id="takeaways">Takeaways</h2>
<ul>
<li><strong>One TLS problem beats N TLS problems.</strong> Put a reverse proxy in front and let it own certs, routing, and renewal so your apps never have to.</li>
<li><strong>Wildcard + DNS-01 is the unlock.</strong> <code>*.domain</code> minted via a DNS TXT challenge covers every subdomain you&rsquo;ll ever invent — and you don&rsquo;t have to expose an app to prove ownership.</li>
<li><strong>Bind apps to <code>127.0.0.1</code>.</strong> If only the proxy faces the world, a misconfigured app can&rsquo;t accidentally serve itself raw to the internet.</li>
<li><strong>New app = three lines.</strong> A subdomain block that <code>reverse_proxy</code>s to a localhost port, a reload, done. Exposing a service stops being a project.</li>
<li><strong>Build the right Caddy.</strong> DNS-01 needs the provider plugin compiled in — <code>xcaddy build --with</code>, then <code>list-modules</code> to confirm before you wonder why the challenge hangs.</li>
</ul>
]]></content:encoded></item><item><title>90s Kid - The Dot-Matrix Printer</title><link>https://errorzap.com/posts/90s-kid-the-dot-matrix-printer/</link><pubDate>Fri, 15 May 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/90s-kid-the-dot-matrix-printer/</guid><description>The screaming beige monster that printed your book report one agonizing dot at a time, and the perforated paper-strips you weren&amp;rsquo;t allowed to tear off too early.</description><content:encoded><![CDATA[<figure style="margin:0 0 28px">
<svg viewBox="0 0 800 240" xmlns="http://www.w3.org/2000/svg" style="width:100%;border-radius:14px">
 <defs>
  <linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#1e1e2e"/><stop offset="1" stop-color="#11111b"/></linearGradient>
  <linearGradient id="sun" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f9e2af"/><stop offset="0.5" stop-color="#fab387"/><stop offset="1" stop-color="#f38ba8"/></linearGradient>
 </defs>
 <rect width="800" height="240" fill="url(#sky)"/>
 <circle cx="640" cy="150" r="74" fill="url(#sun)"/><rect x="566" y="118" width="148" height="4" fill="#11111b"/><rect x="566" y="127" width="148" height="4" fill="#11111b"/><rect x="566" y="136" width="148" height="4" fill="#11111b"/><rect x="566" y="145" width="148" height="4" fill="#11111b"/><rect x="566" y="154" width="148" height="4" fill="#11111b"/><rect x="566" y="163" width="148" height="4" fill="#11111b"/><rect x="566" y="172" width="148" height="4" fill="#11111b"/>
 <line x1="-200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="280" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="360" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="440" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="520" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="600" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="680" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="760" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="840" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="920" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="1000" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="0" y1="178" x2="800" y2="178" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="184" x2="800" y2="184" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="193" x2="800" y2="193" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="206" x2="800" y2="206" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="227" x2="800" y2="227" stroke="#94e2d5" stroke-width="1" opacity="0.22"/>
 <text x="48" y="150" font-size="78">🖨</text>
 <text x="150" y="96" font-family="ui-monospace,monospace" font-size="14" fill="#cba6f7" letter-spacing="5">90s KID //</text>
 <text x="150" y="140" font-family="-apple-system,Segoe UI,sans-serif" font-size="30" font-weight="800" fill="#cdd6f4">The Dot-Matrix Printer</text>
 <rect x="0" y="236" width="800" height="4" fill="#f38ba8"/>
</svg></figure>
<h2 id="the-machine-that-screamed-your-homework-into-existence">The machine that screamed your homework into existence</h2>
<p>You remember the dot-matrix printer.</p>
<p>The big beige one.<br>
On the floor, usually. Too heavy for the desk.<br>
With the green-and-white striped paper that fed up out of the back in one endless accordion ribbon.</p>
<p>And <strong>the noise.</strong><br>
Dear god, the noise.</p>
<p>It didn&rsquo;t print so much as it <em>announced.</em> A grinding, buzzing, sawing shriek that meant somebody, somewhere in the house, was getting their book report out the door at 9:48 PM the night before it was due.</p>
<p>You&rsquo;d stand there next to it, hypnotized.<br>
Watching the little head zip left.<br>
Then right.<br>
Then <strong>nudge</strong> the paper up a hair.<br>
Then left again.</p>
<p>The vibes were:</p>
<ul>
<li><strong>Forbidden.</strong> You were NOT allowed to tear the paper until it finished. <em>Do not touch it.</em></li>
<li><strong>Sacred ritual.</strong> The little holes down the sides. The perforated edges you peeled off after, like the wrapper on a candy.</li>
<li><strong>Tension.</strong> Would it jam? It always <em>might</em> jam.</li>
<li><strong>Loud = important.</strong> If it was screaming, real work was happening.</li>
<li>A faint smell of warm plastic and ink ribbon.</li>
</ul>
<p>And those side strips. The pin-feed holes. You&rsquo;d peel both ribbons off in one clean pull if you were lucky, and it felt like <strong>completing a quest.</strong></p>
<h3 id="what-it-actually-was">What it actually was</h3>
<p>Here&rsquo;s the nerdy part, and it&rsquo;s genuinely cool.</p>
<p>That print head wasn&rsquo;t spraying ink. It had a tiny grid of stiff little <strong>pins</strong> behind it. Tiny metal needles. As the head slid across, an electromagnet would <em>fire</em> individual pins forward, hammering them into an <strong>inked ribbon</strong>, which slapped that ink onto the paper.</p>
<p>A character was just a pattern of dots punched out fast. Nine pins. Sometimes twenty-four if you were fancy. <strong>Impact printing</strong> — literally tiny hammers, hundreds of times a second. That&rsquo;s where the scream came from.</p>
<p>And the green stripes and side holes? That was <strong>continuous-feed tractor paper.</strong> The sprocket holes hooked onto little spinning gears so the printer could pull a whole novel through without anyone reloading a single sheet. The machine fed itself.</p>
<p>The reason you couldn&rsquo;t tear it early: the paper was under tension on those gears. Yank it and you&rsquo;d skew the whole job.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>   _______________________________________
</span></span><span style="display:flex;"><span>  |  o  ___________________________   o  |
</span></span><span style="display:flex;"><span>  | o  | E r r o r   Z a p . . . . |   o |
</span></span><span style="display:flex;"><span>  | o  | ......  ...  .. .... ...  |   o |
</span></span><span style="display:flex;"><span>  | o  | .. ...  .... ... .  ....  |   o |
</span></span><span style="display:flex;"><span>  | o  |___________________________|   o |
</span></span><span style="display:flex;"><span>  |  o ____________________________   o  |
</span></span><span style="display:flex;"><span>  | o ||||||| BZZZT-GRRRT-ZZZP ||||   o |
</span></span><span style="display:flex;"><span>  |__o___________________________ __ o__|
</span></span><span style="display:flex;"><span>   &#39;-.___pin-feed___.-&#39; &#39;-.___holes_.-&#39;
</span></span></code></pre></div><h3 id="it-never-really-left">It never really left</h3>
<p>You think it&rsquo;s gone. It isn&rsquo;t.</p>
<p>That receipt curling out of the gas pump?<br>
The slip the doctor&rsquo;s office prints your appointment on?<br>
The carbon-copy form at the auto shop?</p>
<p><strong>Impact printing.</strong> Still alive. Because here&rsquo;s the secret a laser can&rsquo;t do: hammering through paper lets you print <em>carbon copies in one pass.</em> That&rsquo;s why warehouses, garages, and pharmacies never gave it up.</p>
<p>So the next time something prints with a buzz instead of a hum, lean in.</p>
<p>That&rsquo;s the old beige monster.<br>
Still screaming.<br>
Still working.</p>
<p>Don&rsquo;t tear the paper yet.</p>
]]></content:encoded></item><item><title>The Subdomain That Pointed at Nothing</title><link>https://errorzap.com/posts/the-subdomain-that-pointed-at-nothing/</link><pubDate>Tue, 12 May 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/the-subdomain-that-pointed-at-nothing/</guid><description>A routine DNS audit turned up subdomains aimed at servers that died months ago — quiet little doors anyone could walk through.</description><content:encoded><![CDATA[<figure style="text-align:center;margin:0 0 30px"><img src="hero.png" alt="The Subdomain That Pointed at Nothing" style="max-width:520px;width:100%;border-radius:14px"/></figure>
<p>It started with a boring task. Quarterly DNS audit. Coffee, a zone file, and a vague sense that everything was fine.</p>
<p>It was not fine.</p>
<p>Halfway down the export I hit a CNAME — a customer-facing subdomain — pointing at a cloud host we&rsquo;d decommissioned back in the winter. The instance was gone. The DNS record had never gotten the memo.</p>
<p>I sat there for a second. That subdomain still resolved. It still answered. It just answered <em>nothing</em> — a hosting account that no longer existed, on infrastructure we&rsquo;d long since stopped paying for.</p>
<p>That&rsquo;s not a typo. That&rsquo;s a loaded gun pointed at your own brand.</p>
<h2 id="the-investigation">The investigation</h2>
<p>A dangling DNS record is a pointer with no backing store. The record says &ldquo;this name lives over there.&rdquo; But &ldquo;over there&rdquo; got freed months ago — the VM was destroyed, the cloud bucket released, the hosting account closed.</p>
<p>The danger isn&rsquo;t that it&rsquo;s broken. The danger is that the resource is now <em>available for anyone to claim</em>.</p>
<p>I pulled the full zone and started checking targets one by one. Does the A record&rsquo;s IP still belong to us? Is the CNAME target a hostname we still control? Or is it a now-orphaned cloud resource sitting in a provider&rsquo;s free pool, waiting for the next person to grab it?</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>        Your DNS                      The Internet
</span></span><span style="display:flex;"><span>   ┌──────────────────┐         ┌────────────────────────┐
</span></span><span style="display:flex;"><span>   │ app.example.com  │         │  cloud host (DELETED)   │
</span></span><span style="display:flex;"><span>   │  CNAME ──────────┼────────▶│  account closed, freed  │
</span></span><span style="display:flex;"><span>   └──────────────────┘         │  ▒▒▒ up for grabs ▒▒▒  │
</span></span><span style="display:flex;"><span>                                └───────────┬────────────┘
</span></span><span style="display:flex;"><span>                                            │  attacker
</span></span><span style="display:flex;"><span>                                            ▼  re-claims it
</span></span><span style="display:flex;"><span>                                ┌────────────────────────┐
</span></span><span style="display:flex;"><span>                                │  serves THEIR content   │
</span></span><span style="display:flex;"><span>                                │  on YOUR subdomain      │
</span></span><span style="display:flex;"><span>                                └────────────────────────┘
</span></span></code></pre></div><h2 id="the-aha">The &ldquo;aha&rdquo;</h2>
<p>Here&rsquo;s the part that turns a stale record into a breach.</p>
<p>If an attacker registers that freed cloud resource — same provider, same dangling target — the provider hands <em>them</em> the keys. Now your subdomain serves their content. And because it&rsquo;s <em>your</em> subdomain, the blast radius is ugly:</p>
<ul>
<li>Pixel-perfect phishing on a domain your users already trust.</li>
<li>Cookies scoped to <code>*.example.com</code> get harvested — including session cookies from your real apps.</li>
<li>A free, fully valid TLS certificate, because the takeover host can pass the domain-control check for a name you literally pointed at it.</li>
</ul>
<p>No CVE. No exploit chain. Just a DNS record that outlived its server.</p>
<figure style="text-align:center;margin:34px 0">
<svg viewBox="0 0 560 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Subdomain takeover flow">
  <rect x="0" y="0" width="560" height="220" rx="10" fill="#11111b"/>
  <rect x="28" y="74" width="150" height="72" rx="8" fill="#1e1e2e" stroke="#45475a"/>
  <text x="103" y="104" text-anchor="middle" fill="#cdd6f4" font-family="monospace" font-size="13">app.example.com</text>
  <text x="103" y="126" text-anchor="middle" fill="#6c7086" font-family="monospace" font-size="11">CNAME ></text>
  <rect x="205" y="74" width="150" height="72" rx="8" fill="#1e1e2e" stroke="#f38ba8"/>
  <text x="280" y="100" text-anchor="middle" fill="#f38ba8" font-family="monospace" font-size="12">freed host</text>
  <text x="280" y="120" text-anchor="middle" fill="#6c7086" font-family="monospace" font-size="11">up for grabs</text>
  <rect x="382" y="74" width="150" height="72" rx="8" fill="#1e1e2e" stroke="#cba6f7"/>
  <text x="457" y="100" text-anchor="middle" fill="#cba6f7" font-family="monospace" font-size="12">attacker</text>
  <text x="457" y="120" text-anchor="middle" fill="#fab387" font-family="monospace" font-size="11">re-claims it</text>
  <line x1="178" y1="110" x2="203" y2="110" stroke="#89b4fa" stroke-width="2" marker-end="url(#a)"/>
  <line x1="355" y1="110" x2="380" y2="110" stroke="#f38ba8" stroke-width="2" marker-end="url(#a)"/>
  <text x="280" y="180" text-anchor="middle" fill="#94e2d5" font-family="monospace" font-size="12">valid TLS & your name = phishing on brand</text>
  <defs>
    <marker id="a" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
      <path d="M0,0 L6,3 L0,6 Z" fill="#89b4fa"/>
    </marker>
  </defs>
</svg>
<figcaption style="color:#6c7086;font-size:14px;margin-top:8px">A deleted host gets re-claimed — and your trusted subdomain serves someone else's payload.</figcaption>
</figure>
<h2 id="the-fix">The fix</h2>
<p>First, inventory everything. Pull the whole zone, not just the records you remember.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># Dump every A / AAAA / CNAME in the zone</span>
</span></span><span style="display:flex;"><span>dig @ns1.example.com example.com AXFR <span style="color:#f1fa8c">\
</span></span></span><span style="display:flex;"><span>  | awk <span style="color:#f1fa8c">&#39;$4 ~ /^(A|AAAA|CNAME)$/ {print $1, $4, $5}&#39;</span>
</span></span></code></pre></div><p>Then resolve each one and check whether the target still answers — and whether it&rsquo;s still <em>yours</em>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#ff79c6">while</span> <span style="color:#8be9fd;font-style:italic">read</span> -r name <span style="color:#8be9fd;font-style:italic">type</span> target; <span style="color:#ff79c6">do</span>
</span></span><span style="display:flex;"><span>  <span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34;=== </span><span style="color:#8be9fd;font-style:italic">$name</span><span style="color:#f1fa8c"> (</span><span style="color:#8be9fd;font-style:italic">$type</span><span style="color:#f1fa8c">) -&gt; </span><span style="color:#8be9fd;font-style:italic">$target</span><span style="color:#f1fa8c">&#34;</span>
</span></span><span style="display:flex;"><span>  dig +short <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$name</span><span style="color:#f1fa8c">&#34;</span>
</span></span><span style="display:flex;"><span>  curl -sS -o /dev/null -w <span style="color:#f1fa8c">&#34;  http=%{http_code} ip=%{remote_ip}\n&#34;</span> <span style="color:#f1fa8c">\
</span></span></span><span style="display:flex;"><span>    --max-time <span style="color:#bd93f9">8</span> <span style="color:#f1fa8c">&#34;https://</span><span style="color:#8be9fd;font-style:italic">$name</span><span style="color:#f1fa8c">&#34;</span> <span style="color:#ff79c6">||</span> <span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#34;  DEAD / no response&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">done</span> &lt; zone-records.txt
</span></span></code></pre></div><p>For the dangling ones, the rule is simple: if you don&rsquo;t control the target, the record dies or gets repointed.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># Delete the stale record (RFC 2136 dynamic update)</span>
</span></span><span style="display:flex;"><span>nsupdate -k /etc/dns/update.key <span style="color:#f1fa8c">&lt;&lt;&#39;EOF&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#f1fa8c">server ns1.example.com
</span></span></span><span style="display:flex;"><span><span style="color:#f1fa8c">zone example.com
</span></span></span><span style="display:flex;"><span><span style="color:#f1fa8c">update delete app.example.com. CNAME
</span></span></span><span style="display:flex;"><span><span style="color:#f1fa8c">send
</span></span></span><span style="display:flex;"><span><span style="color:#f1fa8c">EOF</span>
</span></span></code></pre></div><p>Repoint anything still in use at infrastructure <em>you</em> own — a host behind your own reverse proxy, not a third-party resource you might let lapse again.</p>
<p>Finally, make it a standing job. A nightly check that flags any record whose target stops resolving or stops belonging to you.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># cron: nightly dangling-DNS sweep, alert on dead targets</span>
</span></span><span style="display:flex;"><span><span style="color:#bd93f9">0</span> <span style="color:#bd93f9">4</span> * * *  /opt/dns/dangle-check.sh <span style="color:#ff79c6">&amp;&amp;</span> <span style="color:#f1fa8c">\
</span></span></span><span style="display:flex;"><span>  curl -fsS https://hc-ping.test/dns-sweep
</span></span></code></pre></div><h2 id="why-it-happened">Why it happened</h2>
<p>DNS outlives servers. Always.</p>
<p>When you spin a box down, you delete the box. You raise the ticket, you confirm it&rsquo;s gone, you move on. The DNS record — living in a different system, owned by a different runbook — just sits there pointing at a ghost. Nobody&rsquo;s job ends with &ldquo;and remove the CNAME.&rdquo;</p>
<p>Multiply that by a few years of &ldquo;temporary&rdquo; subdomains and deprovisioned cloud accounts, and you&rsquo;ve got a quiet pile of doors with no locks.</p>
<h2 id="takeaways">Takeaways</h2>
<ul>
<li><strong>Inventory every record.</strong> You can&rsquo;t audit a zone you&rsquo;ve only half-remembered. Pull the whole thing.</li>
<li><strong>Verify the target, not just the name.</strong> A record that resolves isn&rsquo;t safe — confirm the thing it points at is still alive <em>and still yours</em>.</li>
<li><strong>Prune the moment a server dies.</strong> Decommissioning a host isn&rsquo;t done until its DNS is gone too. Bake it into the runbook.</li>
<li><strong>Prefer targets you control.</strong> Point subdomains at your own infrastructure, not third-party resources you might forget to renew.</li>
<li><strong>Monitor continuously.</strong> Dangling DNS is created by routine. Catch it with routine — a nightly sweep beats a quarterly surprise.</li>
</ul>
]]></content:encoded></item><item><title>90s Kid - The Universal Remote</title><link>https://errorzap.com/posts/90s-kid-the-universal-remote/</link><pubDate>Sun, 10 May 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/90s-kid-the-universal-remote/</guid><description>You remember the fat black remote with too many buttons that somehow ran the whole living room.</description><content:encoded><![CDATA[<figure style="margin:0 0 28px">
<svg viewBox="0 0 800 240" xmlns="http://www.w3.org/2000/svg" style="width:100%;border-radius:14px">
 <defs>
  <linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#1e1e2e"/><stop offset="1" stop-color="#11111b"/></linearGradient>
  <linearGradient id="sun" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f9e2af"/><stop offset="0.5" stop-color="#fab387"/><stop offset="1" stop-color="#f38ba8"/></linearGradient>
 </defs>
 <rect width="800" height="240" fill="url(#sky)"/>
 <circle cx="640" cy="150" r="74" fill="url(#sun)"/><rect x="566" y="118" width="148" height="4" fill="#11111b"/><rect x="566" y="127" width="148" height="4" fill="#11111b"/><rect x="566" y="136" width="148" height="4" fill="#11111b"/><rect x="566" y="145" width="148" height="4" fill="#11111b"/><rect x="566" y="154" width="148" height="4" fill="#11111b"/><rect x="566" y="163" width="148" height="4" fill="#11111b"/><rect x="566" y="172" width="148" height="4" fill="#11111b"/>
 <line x1="-200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="280" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="360" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="440" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="520" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="600" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="680" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="760" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="840" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="920" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="1000" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="0" y1="178" x2="800" y2="178" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="184" x2="800" y2="184" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="193" x2="800" y2="193" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="206" x2="800" y2="206" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="227" x2="800" y2="227" stroke="#94e2d5" stroke-width="1" opacity="0.22"/>
 <text x="48" y="150" font-size="78">🏛</text>
 <text x="150" y="96" font-family="ui-monospace,monospace" font-size="14" fill="#cba6f7" letter-spacing="5">90s KID //</text>
 <text x="150" y="140" font-family="-apple-system,Segoe UI,sans-serif" font-size="30" font-weight="800" fill="#cdd6f4">The Universal Remote</text>
 <rect x="0" y="236" width="800" height="4" fill="#f38ba8"/>
</svg></figure>
<h2 id="the-clicker-that-ruled-the-living-room">The Clicker That Ruled the Living Room</h2>
<p>You remember <strong>the universal remote.</strong></p>
<p>Not the one that came with the TV. The <em>other</em> one.<br>
The fat black slab that lived on the arm of the couch.<br>
Heavier than it had any right to be.<br>
Three triple-A batteries deep.</p>
<p>It had <strong>way too many buttons.</strong><br>
Half of them did nothing.<br>
The other half did things nobody could explain.</p>
<p>And only Dad knew the magic sequence.</p>
<hr>
<p>Here&rsquo;s what we <em>thought</em> it was, as kids:</p>
<ul>
<li>a <strong>wizard&rsquo;s wand</strong> that talked to every box under the TV</li>
<li>a forbidden object — touch it wrong and the screen goes blue and Dad sighs</li>
<li>powered by a tiny, invisible laser you could never quite see</li>
<li>the reason the TV &ldquo;wasn&rsquo;t working&rdquo; was always <em>your</em> fault</li>
<li>the thing that took <strong>17 button presses</strong> just to watch a tape</li>
</ul>
<p>You&rsquo;d point it at the ceiling, the dog, your sister.<br>
You&rsquo;d press TV / VCR / CABLE / AUX hoping <em>something</em> would happen.</p>
<p>And when the channel finally changed?<br>
Pure, unearned power. <strong>You were a god for one second.</strong></p>
<hr>
<p><strong>Here&rsquo;s what it actually was.</strong></p>
<p>That &ldquo;invisible laser&rdquo; was real — sort of.</p>
<p>Every press fired a little <strong>infrared LED</strong> in the nose of the remote.<br>
Light your eyes can&rsquo;t see, blinking on and off <em>insanely</em> fast — flickering out a coded pattern, like Morse code made of glow.</p>
<p>The TV had a tiny IR receiver behind the front panel, watching for that exact blink-pattern. Volume up, channel down, power — each one a different rhythm of flashes.</p>
<p>The &ldquo;universal&rdquo; part was the clever trick.<br>
Sony, Panasonic, Zenith — every brand blinked in its own dialect.<br>
So the remote stored a <strong>codebook</strong> of them all.</p>
<p>That cursed setup ritual — <em>hold SET, punch in 0-0-4-7 from the little fold-out card</em> — that was you teaching it which dialect to speak.</p>
<p>It wasn&rsquo;t magic.<br>
It was a <strong>flashlight that knew how to talk.</strong></p>
<hr>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>        _________________
</span></span><span style="display:flex;"><span>       |   ___________   |
</span></span><span style="display:flex;"><span>       |  |  CH ▲     |  |
</span></span><span style="display:flex;"><span>       |  |  VOL +    |  |   ((·)) ← invisible
</span></span><span style="display:flex;"><span>       |  |___________|  |        IR blink
</span></span><span style="display:flex;"><span>       |  (1) (2) (3)    |
</span></span><span style="display:flex;"><span>       |  (4) (5) (6)    |
</span></span><span style="display:flex;"><span>       |  (7) (8) (9)    |
</span></span><span style="display:flex;"><span>       |  (TV)(VCR)(AUX) |
</span></span><span style="display:flex;"><span>       |_________________|
</span></span><span style="display:flex;"><span>        ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
</span></span><span style="display:flex;"><span>         the sacred clicker
</span></span></code></pre></div><hr>
<p>Here&rsquo;s the thing.</p>
<p><strong>It never left.</strong></p>
<p>That blinking infrared light is <em>still</em> in your living room right now.<br>
Your soundbar remote? IR.<br>
The cable box clicker buried in the couch? IR.<br>
Same flashlight. Same secret language. Same dead battery.</p>
<p>We just stopped looking at it.</p>
<p>Then your phone became the remote, and the remote became an app, and the app needed an update, and the update needed a login&hellip;</p>
<p>&hellip;and somewhere, a kid is pointing a glowing wand at a TV,<br>
pressing <strong>17 buttons</strong> to watch one show,<br>
feeling like a god for one whole second.</p>
<p>Some magic just changes batteries.</p>
]]></content:encoded></item><item><title>Getting Cameras Out of the Walled Garden</title><link>https://errorzap.com/posts/getting-cameras-out-of-the-walled-garden/</link><pubDate>Sun, 10 May 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/getting-cameras-out-of-the-walled-garden/</guid><description>A camera platform that only spoke encrypted RTSPS quietly refused to talk to the NVR — until I found the plain-RTSP back door it was hiding.</description><content:encoded><![CDATA[<figure style="text-align:center;margin:0 0 30px"><img src="hero.png" alt="Getting Cameras Out of the Walled Garden" style="max-width:520px;width:100%;border-radius:14px"/></figure>
<p>A customer wanted their shiny camera platform recording into a third-party NVR. Simple ask: pull the RTSP stream, point the recorder at it, walk away.</p>
<p>The recorder disagreed. Every stream it tried died on connect. No video, no error worth reading, just a connection that opened and then went cold.</p>
<p>I&rsquo;d wired up a hundred of these. This one fought me.</p>
<h2 id="the-investigation">The investigation</h2>
<p>First instinct: bad URL. So I copied the stream address straight out of the platform and fed it to <code>ffprobe</code>, the universal &ldquo;is this thing even alive&rdquo; test.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ffprobe <span style="color:#f1fa8c">&#34;rtsps://camera-host:7441/&lt;stream-token&gt;?enableSrtp&#34;</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>[tls] Error: handshake failure
</span></span><span style="display:flex;"><span>rtsps://camera-host:7441/...: Input/output error
</span></span></code></pre></div><p>There it is. <code>rtsps://</code>. Not <code>rtsp://</code>. That little <code>s</code> is the whole story.</p>
<p>The platform was advertising an <strong>encrypted</strong> stream — RTSP wrapped in TLS, with SRTP turned on for the media itself. A high port, a token in the path, the works. Very modern. Very secure.</p>
<p>And completely useless to a recorder that only speaks plain, unencrypted RTSP. The NVR reached out for a normal RTSP handshake and got a TLS negotiation shoved in its face. It had no idea what to do, so it hung up.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>┌──────────────┐     rtsps:// + SRTP      ┌──────────────┐
</span></span><span style="display:flex;"><span>│   Camera     │ ────────────────────────►│   NVR        │
</span></span><span style="display:flex;"><span>│  platform    │      (TLS handshake)     │ (plain RTSP) │
</span></span><span style="display:flex;"><span>└──────────────┘                          └──────┬───────┘
</span></span><span style="display:flex;"><span>                                                 │
</span></span><span style="display:flex;"><span>                                                 ▼
</span></span><span style="display:flex;"><span>                                          ✗ handshake failure
</span></span><span style="display:flex;"><span>                                          ✗ no video recorded
</span></span></code></pre></div><p>Two devices, both fluent in &ldquo;RTSP,&rdquo; neither able to hold a conversation. The walled garden, working exactly as designed.</p>
<figure style="text-align:center;margin:34px 0">
<svg viewBox="0 0 560 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="RTSPS walled garden versus plain RTSP path">
  <rect x="0" y="0" width="560" height="220" rx="10" fill="#11111b"/>
  <rect x="30" y="80" width="120" height="60" rx="8" fill="#1e1e2e" stroke="#45475a"/>
  <text x="90" y="106" text-anchor="middle" fill="#cdd6f4" font-family="monospace" font-size="13">Camera</text>
  <text x="90" y="124" text-anchor="middle" fill="#6c7086" font-family="monospace" font-size="11">platform</text>
  <rect x="410" y="80" width="120" height="60" rx="8" fill="#1e1e2e" stroke="#45475a"/>
  <text x="470" y="106" text-anchor="middle" fill="#cdd6f4" font-family="monospace" font-size="13">NVR</text>
  <text x="470" y="124" text-anchor="middle" fill="#6c7086" font-family="monospace" font-size="11">plain RTSP</text>
  <line x1="150" y1="98" x2="410" y2="98" stroke="#f38ba8" stroke-width="2" stroke-dasharray="6 5"/>
  <polygon points="410,98 398,93 398,103" fill="#f38ba8"/>
  <text x="280" y="90" text-anchor="middle" fill="#f38ba8" font-family="monospace" font-size="12">rtsps:// :7441  ✗ SRTP</text>
  <rect x="210" y="158" width="140" height="36" rx="8" fill="#313244" stroke="#cba6f7"/>
  <text x="280" y="181" text-anchor="middle" fill="#cba6f7" font-family="monospace" font-size="12">restreamer</text>
  <line x1="150" y1="124" x2="210" y2="172" stroke="#94e2d5" stroke-width="2"/>
  <line x1="350" y1="172" x2="410" y2="124" stroke="#a6e3a1" stroke-width="2"/>
  <polygon points="410,124 398,121 404,133" fill="#a6e3a1"/>
  <text x="280" y="212" text-anchor="middle" fill="#a6e3a1" font-family="monospace" font-size="12">rtsp:// :7447  ✓ plain</text>
</svg>
<figcaption style="color:#6c7086;font-size:14px;margin-top:8px">The TLS wall (red) blocks the NVR. The plain-RTSP path — direct or via a restreamer — gets through.</figcaption>
</figure>
<h2 id="the-aha">The &ldquo;aha&rdquo;</h2>
<p>Here&rsquo;s the thing about these platforms: the encrypted stream is what they <em>show</em> you. It&rsquo;s rarely the only thing they <em>offer</em>.</p>
<p>I dug into the device settings and found a toggle — a separate, unencrypted RTSP endpoint on a different port. Plain <code>rtsp://</code>, basic credentials, no TLS, no SRTP. The boring old protocol the recorder actually wanted, sitting right there behind a checkbox nobody enabled by default.</p>
<h2 id="the-fix">The fix</h2>
<p>Enable the plain endpoint, grab the new URL, and verify with <code>ffprobe</code> <strong>before</strong> touching the NVR config:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ffprobe -v error -show_streams <span style="color:#f1fa8c">\
</span></span></span><span style="display:flex;"><span>  <span style="color:#f1fa8c">&#34;rtsp://&lt;user&gt;:&lt;pass&gt;@camera-host:7447/&lt;stream-token&gt;&#34;</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>codec_name=h264
</span></span><span style="display:flex;"><span>width=2688
</span></span><span style="display:flex;"><span>height=1512
</span></span><span style="display:flex;"><span>✓ Stream #0:0  Video: h264, 2688x1512, 15 fps
</span></span></code></pre></div><p>Green across the board. Point the recorder at that URL and it records.</p>
<p>If the platform <em>won&rsquo;t</em> expose a plain endpoint, drop a restreamer in the middle — something like MediaMTX or a thin ffmpeg relay — to ingest the encrypted stream and re-publish it as plain RTSP on your own box:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ffmpeg -rtsp_transport tcp <span style="color:#f1fa8c">\
</span></span></span><span style="display:flex;"><span>  -i <span style="color:#f1fa8c">&#34;rtsps://camera-host:7441/&lt;stream-token&gt;?enableSrtp&#34;</span> <span style="color:#f1fa8c">\
</span></span></span><span style="display:flex;"><span>  -c copy -f rtsp <span style="color:#f1fa8c">\
</span></span></span><span style="display:flex;"><span>  rtsp://127.0.0.1:8554/cam1
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># verify the republished stream the NVR will actually consume</span>
</span></span><span style="display:flex;"><span>ffprobe <span style="color:#f1fa8c">&#34;rtsp://127.0.0.1:8554/cam1&#34;</span>
</span></span></code></pre></div><p>The NVR talks to your restreamer. Your restreamer eats the TLS for breakfast. Everybody&rsquo;s happy.</p>
<h2 id="why-it-happened">Why it happened</h2>
<p>Vendors are encrypting streams by default now, and good for them — that&rsquo;s the right call for traffic crossing untrusted networks. But &ldquo;secure by default&rdquo; and &ldquo;interoperable&rdquo; are different goals, and the camera folks optimized for the first one.</p>
<p>The encrypted URL is front and center. The plain one is buried, off, and undocumented. So the integration looks broken when it&rsquo;s really just locked. The garden has a gate; you have to go find it.</p>
<h2 id="takeaways">Takeaways</h2>
<ul>
<li><strong>Read the scheme.</strong> <code>rtsps://</code> on a high port with SRTP is a different animal than <code>rtsp://</code>. The handshake error is your first clue, not a riddle.</li>
<li><strong>Look for the plain-RTSP port.</strong> Most platforms still expose an unencrypted endpoint behind a toggle. Check device settings before you assume it can&rsquo;t be done.</li>
<li><strong><code>ffprobe</code> before you wire anything.</strong> If the probe can&rsquo;t pull codec and resolution, the NVR won&rsquo;t either.</li>
<li><strong>Put a restreamer in the middle</strong> when the vendor refuses to deobfuscate — MediaMTX or <code>ffmpeg -c copy</code> ingests RTSPS and republishes clean RTSP, no re-encode.</li>
<li><strong>A walled garden isn&rsquo;t a wall.</strong> It&rsquo;s a default. Find the gate the vendor left in the settings, and the lock-in mostly evaporates.</li>
</ul>
]]></content:encoded></item><item><title>90s Kid - The LAN Party</title><link>https://errorzap.com/posts/90s-kid-the-lan-party/</link><pubDate>Tue, 05 May 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/90s-kid-the-lan-party/</guid><description>You remember hauling your whole computer across town just to sit in a room and frag your friends.</description><content:encoded><![CDATA[<figure style="margin:0 0 28px">
<svg viewBox="0 0 800 240" xmlns="http://www.w3.org/2000/svg" style="width:100%;border-radius:14px">
 <defs>
  <linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#1e1e2e"/><stop offset="1" stop-color="#11111b"/></linearGradient>
  <linearGradient id="sun" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f9e2af"/><stop offset="0.5" stop-color="#fab387"/><stop offset="1" stop-color="#f38ba8"/></linearGradient>
 </defs>
 <rect width="800" height="240" fill="url(#sky)"/>
 <circle cx="640" cy="150" r="74" fill="url(#sun)"/><rect x="566" y="118" width="148" height="4" fill="#11111b"/><rect x="566" y="127" width="148" height="4" fill="#11111b"/><rect x="566" y="136" width="148" height="4" fill="#11111b"/><rect x="566" y="145" width="148" height="4" fill="#11111b"/><rect x="566" y="154" width="148" height="4" fill="#11111b"/><rect x="566" y="163" width="148" height="4" fill="#11111b"/><rect x="566" y="172" width="148" height="4" fill="#11111b"/>
 <line x1="-200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="280" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="360" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="440" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="520" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="600" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="680" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="760" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="840" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="920" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="1000" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="0" y1="178" x2="800" y2="178" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="184" x2="800" y2="184" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="193" x2="800" y2="193" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="206" x2="800" y2="206" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="227" x2="800" y2="227" stroke="#94e2d5" stroke-width="1" opacity="0.22"/>
 <text x="48" y="150" font-size="78">🕹</text>
 <text x="150" y="96" font-family="ui-monospace,monospace" font-size="14" fill="#cba6f7" letter-spacing="5">90s KID //</text>
 <text x="150" y="140" font-family="-apple-system,Segoe UI,sans-serif" font-size="30" font-weight="800" fill="#cdd6f4">The LAN Party</text>
 <rect x="0" y="236" width="800" height="4" fill="#f38ba8"/>
</svg></figure>
<h2 id="the-night-we-carried-our-computers-into-the-basement">The Night We Carried Our Computers Into the Basement</h2>
<p>You remember <strong>the LAN party.</strong></p>
<p>Not a download.<br>
Not a lobby.<br>
A <em>pilgrimage.</em></p>
<p>You unplugged your entire desktop — the beige tower, the CRT the size of a microwave, the rat&rsquo;s nest of cables — and you carried it, in your arms, like a wounded soldier, to somebody&rsquo;s basement.</p>
<p>The monitor weighed forty pounds.<br>
You did this <strong>on purpose.</strong><br>
For fun.</p>
<p>And when you got there, there was already a folding table, an extension cord doing something it was definitely not rated for, and a bowl of Doritos that would last exactly nine minutes.</p>
<p>Here&rsquo;s what it <em>felt</em> like as a kid:</p>
<ul>
<li><strong>Forbidden</strong> — six computers in one room felt like a hacker movie</li>
<li><strong>Sacred</strong> — nobody touched the host&rsquo;s machine. He was the king.</li>
<li><strong>Slightly dangerous</strong> — that many power bricks in one outlet? bold</li>
<li><strong>A little magic</strong> — your screen and <em>his</em> screen showing the <em>same world</em></li>
<li><strong>Eternal</strong> — it was 2am and nobody&rsquo;s mom had come downstairs yet</li>
</ul>
<p>You&rsquo;d hear someone yell &ldquo;<strong>WHO HAS THE GRAY CABLE</strong>&rdquo; and the whole night hinged on it.</p>
<h3 id="the-reveal">The reveal</h3>
<p>That gray cable was the whole trick.</p>
<p>It was a <strong>Cat-5 Ethernet cable</strong>, and it ran to a little blinking box in the middle of the table — a <strong>hub</strong> (later, the fancy kids had a <em>switch</em>). Every computer plugged into it, and that box let your machines gossip with each other at a blistering <strong>10 megabits a second</strong>.</p>
<p>No internet involved. None. The whole party was a tiny private island of computers — a <strong>Local Area Network</strong> — talking only to each other.</p>
<p>Before someone bought the hub, you did it the <em>cursed</em> way: a <strong>coaxial cable</strong> daisy-chained machine to machine, with a little metal <strong>terminator</strong> screwed onto each end. If one person tripped over the line, the <em>entire network died</em> — and everyone knew whose fault it was.</p>
<p>And the game finding everyone? That was your machine literally <strong>shouting into the wire</strong> — &ldquo;anybody out there hosting?&rdquo; — and a host shouting back. That&rsquo;s why someone always had to &ldquo;<strong>make the server</strong>.&rdquo; He wasn&rsquo;t being dramatic. He was being the server.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>   .------.   .------.   .------.
</span></span><span style="display:flex;"><span>   |  PC  |   |  PC  |   |  PC  |
</span></span><span style="display:flex;"><span>   &#39;--||--&#39;   &#39;--||--&#39;   &#39;--||--&#39;
</span></span><span style="display:flex;"><span>      ||         ||         ||
</span></span><span style="display:flex;"><span>   ===++=========++=========++===
</span></span><span style="display:flex;"><span>      |     [ H U B ]   .|.|.     |
</span></span><span style="display:flex;"><span>      &#39;------ blink blink --------&#39;
</span></span><span style="display:flex;"><span>        every box hears every box
</span></span></code></pre></div><h3 id="where-it-went">Where it went</h3>
<p>It never really died — it just <strong>stopped weighing forty pounds.</strong></p>
<p>That hub in the middle of the table? It moved into the little plastic router blinking in your hallway right now. That &ldquo;shout into the wire to find a host&rdquo;? That&rsquo;s <strong>matchmaking</strong>, except now the wire is the whole planet and the host is a warehouse in Virginia.</p>
<p>You still play with your friends.<br>
You just don&rsquo;t carry your computer to do it.</p>
<p>But some part of you misses it — the cables, the chaos, the bowl of Doritos, the <strong>gray cable</strong> that held the whole night together.</p>
<p>We didn&rsquo;t have multiplayer.</p>
<p>We <em>built</em> it, in a basement, every single time.</p>
]]></content:encoded></item><item><title>90s Kid - The CRT and the Static Zap</title><link>https://errorzap.com/posts/90s-kid-the-crt-and-the-zap/</link><pubDate>Thu, 30 Apr 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/90s-kid-the-crt-and-the-zap/</guid><description>You remember the fat glass monitor that hummed, glowed, and bit your knuckle with a tiny lightning bolt.</description><content:encoded><![CDATA[<figure style="margin:0 0 28px">
<svg viewBox="0 0 800 240" xmlns="http://www.w3.org/2000/svg" style="width:100%;border-radius:14px">
 <defs>
  <linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#1e1e2e"/><stop offset="1" stop-color="#11111b"/></linearGradient>
  <linearGradient id="sun" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f9e2af"/><stop offset="0.5" stop-color="#fab387"/><stop offset="1" stop-color="#f38ba8"/></linearGradient>
 </defs>
 <rect width="800" height="240" fill="url(#sky)"/>
 <circle cx="640" cy="150" r="74" fill="url(#sun)"/><rect x="566" y="118" width="148" height="4" fill="#11111b"/><rect x="566" y="127" width="148" height="4" fill="#11111b"/><rect x="566" y="136" width="148" height="4" fill="#11111b"/><rect x="566" y="145" width="148" height="4" fill="#11111b"/><rect x="566" y="154" width="148" height="4" fill="#11111b"/><rect x="566" y="163" width="148" height="4" fill="#11111b"/><rect x="566" y="172" width="148" height="4" fill="#11111b"/>
 <line x1="-200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="280" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="360" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="440" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="520" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="600" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="680" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="760" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="840" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="920" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="1000" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="0" y1="178" x2="800" y2="178" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="184" x2="800" y2="184" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="193" x2="800" y2="193" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="206" x2="800" y2="206" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="227" x2="800" y2="227" stroke="#94e2d5" stroke-width="1" opacity="0.22"/>
 <text x="48" y="150" font-size="78">⚡</text>
 <text x="150" y="96" font-family="ui-monospace,monospace" font-size="14" fill="#cba6f7" letter-spacing="5">90s KID //</text>
 <text x="150" y="140" font-family="-apple-system,Segoe UI,sans-serif" font-size="30" font-weight="800" fill="#cdd6f4">The CRT and the Static Zap</text>
 <rect x="0" y="236" width="800" height="4" fill="#f38ba8"/>
</svg></figure>
<h2 id="the-glass-box-that-bit-back">The Glass Box That Bit Back</h2>
<p>You remember <strong>the monitor</strong>.</p>
<p>Not a screen. A <em>monitor</em>.</p>
<p>A beige cube so deep it needed its own zip code on the desk.<br>
It hummed before it showed you anything.<br>
That little rising <em>whine</em> — the sound of the thing <strong>waking up</strong>.</p>
<p>And then the moment.<br>
You walked past it, reached out a finger, and —</p>
<p><strong>ZAP.</strong></p>
<p>A tiny lightning bolt jumped from the glass straight into your knuckle.</p>
<p>You weren&rsquo;t even <em>trying</em> to touch it. The monitor reached out and got you first.</p>
<p>The kid logic was airtight:</p>
<ul>
<li>the screen was <strong>alive</strong></li>
<li>it could <em>see</em> you</li>
<li>the zap was a warning</li>
<li>if you put your face too close you&rsquo;d get <strong>radiation</strong> (Mom said so)</li>
<li>the dust on it was somehow <em>magnetic dust</em></li>
<li>pressing your palm flat on it made your hair stand up like a wizard</li>
</ul>
<p>You&rsquo;d run a comb through your hair, hold it near the glass, and watch the strands lean in.</p>
<p><strong>Forbidden science.</strong> No lab coat required.</p>
<p>And the colors. Oh, the colors.</p>
<p>That deep, glowing, slightly-curved picture that pulled into the corners like a fishbowl.<br>
When you turned it off, the image collapsed into a single bright dot in the middle —<br>
<em>pew</em> — and then a ghost stayed there for a second, like the screen was holding its breath.</p>
<hr>
<p>Here&rsquo;s what was actually going on.</p>
<p>That fat box was a <strong>Cathode Ray Tube</strong>, and it was basically a tiny, tamed particle accelerator pointed at your face.</p>
<p>At the back, an electron gun fired a beam of electrons.<br>
Magnetic coils — the <em>deflection yoke</em> — whipped that beam left-to-right, top-to-bottom, <strong>fifty to seventy times a second</strong>, painting one glowing line at a time across a phosphor coating on the inside of the glass.</p>
<p>The whine you heard? That was the <strong>flyback transformer</strong>, singing at about 15 kilohertz — right at the edge of what young ears could catch and old ears couldn&rsquo;t. A genuine 90s-kid superpower.</p>
<p>And the zap?</p>
<p>The whole front of that tube was carrying a <strong>static charge</strong> from all those electrons slamming into phosphor. You weren&rsquo;t being warned. You were just the nearest path to ground.</p>
<p>The dot when you powered off was the beam collapsing as the magnetics died.<br>
The ghost image was phosphor <strong>persistence</strong> — the glow literally fading.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>        ____________________________
</span></span><span style="display:flex;"><span>       /                            /|
</span></span><span style="display:flex;"><span>      /   .--------------------.    / |
</span></span><span style="display:flex;"><span>     /   |   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓   |   /  |
</span></span><span style="display:flex;"><span>    /    |   ▓  &gt; C:\&gt;_     ▓   |  /   |
</span></span><span style="display:flex;"><span>   /     |   ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓   | /   /
</span></span><span style="display:flex;"><span>  /      &#39;--------------------&#39;  |/  /
</span></span><span style="display:flex;"><span> /___________________________ __/  /
</span></span><span style="display:flex;"><span> |   (o)   POWER   ~~hummmm~~  |  /
</span></span><span style="display:flex;"><span> |____________________________|/
</span></span><span style="display:flex;"><span>        ((( zap! )))  -&gt; your knuckle
</span></span></code></pre></div><p>It never really left, you know.</p>
<p>That curved-glass glow lives on in every <strong>retro filter</strong>, every scanline overlay on a streaming game, every &ldquo;CRT mode&rdquo; toggle that fakes the fishbowl on a screen thin as a credit card.</p>
<p>We spent twenty years chasing flat, sharp, and silent.<br>
Now we pay good money for software that puts the <strong>fuzz and the curve back in.</strong></p>
<p>But the zap?</p>
<p>The zap is gone.</p>
<p>Nothing flat ever loved you enough to bite.</p>
]]></content:encoded></item><item><title>90s Kid - The Boot Disk</title><link>https://errorzap.com/posts/90s-kid-the-boot-disk/</link><pubDate>Sat, 25 Apr 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/90s-kid-the-boot-disk/</guid><description>It was a chunky little square of plastic that decided whether the computer would even wake up that day.</description><content:encoded><![CDATA[<figure style="margin:0 0 28px">
<svg viewBox="0 0 800 240" xmlns="http://www.w3.org/2000/svg" style="width:100%;border-radius:14px">
 <defs>
  <linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#1e1e2e"/><stop offset="1" stop-color="#11111b"/></linearGradient>
  <linearGradient id="sun" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f9e2af"/><stop offset="0.5" stop-color="#fab387"/><stop offset="1" stop-color="#f38ba8"/></linearGradient>
 </defs>
 <rect width="800" height="240" fill="url(#sky)"/>
 <circle cx="640" cy="150" r="74" fill="url(#sun)"/><rect x="566" y="118" width="148" height="4" fill="#11111b"/><rect x="566" y="127" width="148" height="4" fill="#11111b"/><rect x="566" y="136" width="148" height="4" fill="#11111b"/><rect x="566" y="145" width="148" height="4" fill="#11111b"/><rect x="566" y="154" width="148" height="4" fill="#11111b"/><rect x="566" y="163" width="148" height="4" fill="#11111b"/><rect x="566" y="172" width="148" height="4" fill="#11111b"/>
 <line x1="-200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="280" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="360" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="440" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="520" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="600" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="680" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="760" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="840" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="920" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="1000" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="0" y1="178" x2="800" y2="178" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="184" x2="800" y2="184" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="193" x2="800" y2="193" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="206" x2="800" y2="206" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="227" x2="800" y2="227" stroke="#94e2d5" stroke-width="1" opacity="0.22"/>
 <text x="48" y="150" font-size="78">💾</text>
 <text x="150" y="96" font-family="ui-monospace,monospace" font-size="14" fill="#cba6f7" letter-spacing="5">90s KID //</text>
 <text x="150" y="140" font-family="-apple-system,Segoe UI,sans-serif" font-size="30" font-weight="800" fill="#cdd6f4">The Boot Disk</text>
 <rect x="0" y="236" width="800" height="4" fill="#f38ba8"/>
</svg></figure>
<h2 id="the-square-that-could-resurrect-the-whole-machine">The Square That Could Resurrect the Whole Machine</h2>
<p>You remember <strong>the boot disk.</strong></p>
<p>Not a regular floppy.<br>
A <em>special</em> one.</p>
<p>It lived in a little paper sleeve, or maybe a sandwich bag, somewhere near the computer.<br>
Sometimes it had a label written in marker.<br>
Sometimes the label just said <strong>&ldquo;BOOT — DO NOT ERASE.&rdquo;</strong></p>
<p>And you believed it.</p>
<p>You did NOT erase it.</p>
<hr>
<p>To us, this thing had <strong>powers.</strong></p>
<p>The vibes:</p>
<ul>
<li>It was the disk you only touched in an <strong>emergency</strong></li>
<li>Dad slid it in when the computer &ldquo;wouldn&rsquo;t go&rdquo;</li>
<li>The screen would go black and serious and full of words</li>
<li>It smelled faintly like plastic and authority</li>
<li>Touching the shiny brown circle inside = forbidden, basically a crime</li>
<li>It made the drive go <em>grrr-CHUNK-grrr</em> like it was thinking really hard</li>
</ul>
<p>It felt less like a tool and more like a <strong>magic key.</strong><br>
The computer was a sleeping dragon, and this little square was the only thing that knew its true name.</p>
<hr>
<p>Here&rsquo;s what it actually was.</p>
<p>A computer can&rsquo;t pull itself up by its own bootstraps from nothing — it needs the tiniest starter program to teach it how to load everything else. That&rsquo;s literally where <strong>&ldquo;booting&rdquo;</strong> comes from.</p>
<p>That special floppy held a minimal copy of <strong>DOS</strong>: just enough operating system to wake the machine, talk to the keyboard, see the drives, and give you a <code>C:\&gt;</code> prompt.</p>
<p>When the hard drive got corrupted, or a nasty boot-sector virus moved in, or Windows face-planted on startup — the hard drive couldn&rsquo;t boot itself anymore.</p>
<p>So you fed the computer a clean brain from the <strong>A: drive</strong> instead.</p>
<p>From that humble prompt, the grown-ups ran the real magic spells:<br>
<code>scandisk</code>. <code>fdisk</code>. <code>format</code>. <code>sys c:</code>.</p>
<p>640 KB of pure &ldquo;I will fix you whether you like it or not.&rdquo;</p>
<hr>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>        _________________________
</span></span><span style="display:flex;"><span>       |  _____________________  |
</span></span><span style="display:flex;"><span>       | |  BOOT - DO NOT ERASE| |
</span></span><span style="display:flex;"><span>       | |_____________________| |
</span></span><span style="display:flex;"><span>       |  ___________________    |
</span></span><span style="display:flex;"><span>       | |::|             |  |   |
</span></span><span style="display:flex;"><span>       | |::|   A:\&gt;_      |  |   |
</span></span><span style="display:flex;"><span>       | |::|_____________ |  |   |
</span></span><span style="display:flex;"><span>       |_______________________ _|
</span></span><span style="display:flex;"><span>       |  __ |##############|    |
</span></span><span style="display:flex;"><span>       | |  ||##############|    |
</span></span><span style="display:flex;"><span>       | |__||##############|    |
</span></span><span style="display:flex;"><span>       |_______________________ _|
</span></span><span style="display:flex;"><span>              [ 3.5&#34;  1.44 MB ]
</span></span></code></pre></div><hr>
<p>The boot disk never really died — it just stopped being a <em>square.</em></p>
<p>That clean little rescue brain became your Windows recovery USB.<br>
It became <strong>F8</strong>, and Safe Mode, and &ldquo;Startup Repair.&rdquo;<br>
It became the install media you make when a laptop won&rsquo;t wake up.</p>
<p>Every time you&rsquo;ve held a key during startup to summon a menu out of a dead machine, you were doing the exact same ritual.</p>
<p>Same dragon.<br>
Same magic key.</p>
<p>It just fits in your pocket now — and nobody tells you not to touch the shiny part.</p>
]]></content:encoded></item><item><title>The Backup That Hung Forever on OneDrive</title><link>https://errorzap.com/posts/the-backup-that-hung-forever-on-onedrive/</link><pubDate>Wed, 22 Apr 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/the-backup-that-hung-forever-on-onedrive/</guid><description>A Kopia job sat at 0% all night because it tried to read files that didn&amp;rsquo;t actually exist on disk yet.</description><content:encoded><![CDATA[<figure style="text-align:center;margin:0 0 30px"><img src="hero.png" alt="The Backup That Hung Forever on OneDrive" style="max-width:520px;width:100%;border-radius:14px"/></figure>
<p>The backup was &ldquo;running.&rdquo; It had been running for nine hours.</p>
<p>A client&rsquo;s nightly Kopia job pointed at a user&rsquo;s documents folder. Normally it finished in twenty minutes. This morning it was still chewing: no error, no progress, just a cursor blinking next to a snapshot that had hashed maybe a few hundred files and then gone silent.</p>
<p>No CPU. No disk I/O to speak of. No network throughput. Just a process sitting there, perfectly calm, doing absolutely nothing while claiming to be busy.</p>
<p>That&rsquo;s the worst kind of hang. A crash leaves a body. This left a process that would happily wait until the heat death of the universe.</p>
<h2 id="the-investigation">The investigation</h2>
<p>First instinct: blame the repository. Maybe the backend was wedged, maybe a lock was stuck. Nope. A fresh <code>kopia snapshot create</code> against a different folder ran clean in seconds.</p>
<p>So it wasn&rsquo;t Kopia. It was <em>this folder</em>.</p>
<p>I watched the process with Resource Monitor and caught it: every time the scan touched a particular file, the thread parked in a wait state. Not reading. Not erroring. Waiting on the filesystem to hand back bytes that never came.</p>
<p>Then I actually <em>looked</em> at the folder.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>┌────────────────────────────────────────────────┐
</span></span><span style="display:flex;"><span>│  C:\Users\user\Documents  (OneDrive-synced)     │
</span></span><span style="display:flex;"><span>├──────────────┬─────────────────────────────────┤
</span></span><span style="display:flex;"><span>│  Q1-report   │  ✓  green check  (on this PC)    │
</span></span><span style="display:flex;"><span>│  budget.xlsx │  ✓  green check  (on this PC)    │
</span></span><span style="display:flex;"><span>│  archive\    │  ☁  cloud icon   (online-only)   │ ◄── placeholder
</span></span><span style="display:flex;"><span>│  photos\     │  ☁  cloud icon   (online-only)   │ ◄── placeholder
</span></span><span style="display:flex;"><span>└──────────────┴─────────────────────────────────┘
</span></span><span style="display:flex;"><span>        the cloud-icon files have no bytes on disk
</span></span></code></pre></div><p>Half the tree had little cloud icons instead of green checks. OneDrive <strong>Files On-Demand</strong>. Those files looked real — name, size, timestamp — but the actual content lived in the cloud. On disk they were placeholders.</p>
<h2 id="the-aha">The &ldquo;aha&rdquo;</h2>
<p>OneDrive&rsquo;s on-demand placeholders are implemented with the Windows <strong>cloud files filter driver</strong>, <code>cldflt</code>. When any process opens a placeholder and tries to read it, <code>cldflt</code> intercepts the read and <em>hydrates</em> the file — downloads it from the cloud, transparently, before returning a single byte.</p>
<p>A backup tool is the most naive reader on earth. It opens every file and reads it end to end. So Kopia walks into a folder full of placeholders and triggers a hydration storm: thousands of files getting pulled down on demand, throttled, retried, and in some cases just&hellip; stalling. The read call blocks. The thread parks. Forever.</p>
<p>The backup wasn&rsquo;t broken. It was politely waiting for the entire cloud to fall back onto the disk, one file at a time, through a sync client that had no idea it was under attack.</p>
<figure style="text-align:center;margin:34px 0">
<svg viewBox="0 0 560 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Kopia read blocked by cldflt hydration versus VSS snapshot bypass">
<rect x="0" y="0" width="560" height="220" rx="10" fill="#11111b"/>
<rect x="28" y="40" width="120" height="54" rx="8" fill="#1e1e2e" stroke="#45475a"/>
<text x="88" y="64" fill="#cdd6f4" font-family="monospace" font-size="13" text-anchor="middle">Kopia</text>
<text x="88" y="82" fill="#6c7086" font-family="monospace" font-size="11" text-anchor="middle">read()</text>
<rect x="220" y="40" width="130" height="54" rx="8" fill="#1e1e2e" stroke="#f38ba8"/>
<text x="285" y="64" fill="#f38ba8" font-family="monospace" font-size="13" text-anchor="middle">cldflt</text>
<text x="285" y="82" fill="#6c7086" font-family="monospace" font-size="11" text-anchor="middle">hydrate...</text>
<rect x="420" y="40" width="115" height="54" rx="8" fill="#313244" stroke="#45475a"/>
<text x="477" y="64" fill="#fab387" font-family="monospace" font-size="12" text-anchor="middle">cloud</text>
<text x="477" y="82" fill="#6c7086" font-family="monospace" font-size="11" text-anchor="middle">(stalls)</text>
<line x1="148" y1="67" x2="218" y2="67" stroke="#89b4fa" stroke-width="2"/>
<polygon points="218,67 210,63 210,71" fill="#89b4fa"/>
<line x1="350" y1="67" x2="418" y2="67" stroke="#f38ba8" stroke-width="2" stroke-dasharray="5 4"/>
<polygon points="418,67 410,63 410,71" fill="#f38ba8"/>
<text x="285" y="120" fill="#f38ba8" font-family="monospace" font-size="12" text-anchor="middle">x blocks forever</text>
<rect x="28" y="150" width="120" height="46" rx="8" fill="#1e1e2e" stroke="#45475a"/>
<text x="88" y="178" fill="#cdd6f4" font-family="monospace" font-size="13" text-anchor="middle">Kopia</text>
<rect x="220" y="150" width="130" height="46" rx="8" fill="#1e1e2e" stroke="#a6e3a1"/>
<text x="285" y="172" fill="#a6e3a1" font-family="monospace" font-size="13" text-anchor="middle">VSS shadow</text>
<text x="285" y="188" fill="#6c7086" font-family="monospace" font-size="10" text-anchor="middle">stable view</text>
<line x1="148" y1="173" x2="218" y2="173" stroke="#a6e3a1" stroke-width="2"/>
<polygon points="218,173 210,169 210,177" fill="#a6e3a1"/>
<text x="430" y="178" fill="#a6e3a1" font-family="monospace" font-size="12" text-anchor="start">ok: no hydration</text>
</svg>
<figcaption style="color:#6c7086;font-size:14px;margin-top:8px">Reading the live folder triggers cldflt hydration and hangs; reading a VSS shadow skips it entirely.</figcaption>
</figure>
<h2 id="the-fix">The fix</h2>
<p>You don&rsquo;t want to read the live folder. You want to read a <strong>snapshot</strong> of it — a frozen, point-in-time view that the cloud filter doesn&rsquo;t get to intercept. That&rsquo;s exactly what a Volume Shadow Copy gives you. The VSS snapshot presents the volume&rsquo;s state as committed bytes, with no on-demand hydration path.</p>
<p>Kopia has hooks for precisely this: run an action before it walks the source root, and another after. Create the shadow before, tear it down after.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-powershell" data-lang="powershell"><span style="display:flex;"><span><span style="color:#6272a4"># before-snapshot: create a shadow copy and expose it via symlink</span>
</span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">$vol</span> = (vssadmin create shadow /<span style="color:#ff79c6">for</span>=C: | <span style="color:#8be9fd;font-style:italic">Select-String</span> <span style="color:#f1fa8c">&#39;Shadow Copy Volume Name&#39;</span>).ToString().Split(<span style="color:#f1fa8c">&#39; &#39;</span>)[<span style="color:#bd93f9">-1</span>]
</span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">$env:KOPIA_SNAPSHOT_PATH</span> = <span style="color:#8be9fd;font-style:italic">$vol</span>
</span></span><span style="display:flex;"><span>cmd /c <span style="color:#f1fa8c">&#34;mklink /d C:\kopia-shadow </span><span style="color:#8be9fd;font-style:italic">$vol</span><span style="color:#f1fa8c">\&#34;</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-powershell" data-lang="powershell"><span style="display:flex;"><span><span style="color:#6272a4"># after-snapshot: clean up the link and drop the shadow</span>
</span></span><span style="display:flex;"><span>cmd /c <span style="color:#f1fa8c">&#34;rmdir C:\kopia-shadow&#34;</span>
</span></span><span style="display:flex;"><span>vssadmin delete shadows /<span style="color:#ff79c6">for</span>=C: /oldest /quiet
</span></span></code></pre></div><p>Then point the source at the shadow instead of the live path and wire the hooks in:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>kopia snapshot create C:<span style="color:#f1fa8c">\k</span>opia-shadow<span style="color:#f1fa8c">\U</span>sers<span style="color:#f1fa8c">\u</span>ser<span style="color:#f1fa8c">\D</span>ocuments <span style="color:#f1fa8c">\
</span></span></span><span style="display:flex;"><span>  --before-snapshot-root-action<span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;powershell -File C:\scripts\vss-before.ps1&#34;</span> <span style="color:#f1fa8c">\
</span></span></span><span style="display:flex;"><span>  --after-snapshot-root-action<span style="color:#ff79c6">=</span><span style="color:#f1fa8c">&#34;powershell -File C:\scripts\vss-after.ps1&#34;</span>
</span></span></code></pre></div><p>The next run finished in eighteen minutes. Every placeholder read off the shadow returned instantly, because the snapshot view doesn&rsquo;t route through <code>cldflt</code>. No hydration, no stalls, no nine-hour ghost.</p>
<h2 id="why-it-happened">Why it happened</h2>
<p>OneDrive Files On-Demand exists so users don&rsquo;t sync 400GB of vacation photos onto a 256GB laptop. Great feature for humans. Terrible surprise for any tool that assumes &ldquo;the file is on this disk.&rdquo;</p>
<p>Kopia did nothing wrong. It read files. The trap is that on a cloud-synced volume, &ldquo;open and read&rdquo; is a network operation in disguise, gated by a filter driver that will block your thread while it phones home. Backups that walk thousands of placeholders turn into a self-inflicted denial of service.</p>
<p>Snapshotting through VSS sidesteps the whole mess, and as a bonus you get a consistent point-in-time copy instead of backing up a folder that&rsquo;s mutating under you.</p>
<h2 id="takeaways">Takeaways</h2>
<ul>
<li><strong>Cloud on-demand files are placeholders, not files.</strong> OneDrive, Dropbox Smart Sync, and friends all use a filter driver (<code>cldflt</code> on Windows) that hydrates on read.</li>
<li><strong>A naive reader hangs on hydration.</strong> Backups, indexers, and AV scanners that open every file will trigger downloads — and stall when the cloud throttles or times out.</li>
<li><strong>Back up from a VSS shadow, not the live tree.</strong> The snapshot view bypasses the cloud filter and returns committed bytes instantly.</li>
<li><strong>Use Kopia&rsquo;s before/after root-action hooks</strong> to create and tear down the shadow around each run — no manual babysitting.</li>
<li><strong>A backup at &ldquo;0% for hours&rdquo; with no I/O isn&rsquo;t slow, it&rsquo;s blocked.</strong> Watch the thread state; a parked read on a cloud volume is the tell.</li>
</ul>
]]></content:encoded></item><item><title>90s Kid - Flying Toasters and the Screen Saver</title><link>https://errorzap.com/posts/90s-kid-flying-toasters/</link><pubDate>Mon, 20 Apr 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/90s-kid-flying-toasters/</guid><description>You walked away from the computer for ten minutes and came back to find it had grown wings.</description><content:encoded><![CDATA[<figure style="margin:0 0 28px">
<svg viewBox="0 0 800 240" xmlns="http://www.w3.org/2000/svg" style="width:100%;border-radius:14px">
 <defs>
  <linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#1e1e2e"/><stop offset="1" stop-color="#11111b"/></linearGradient>
  <linearGradient id="sun" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f9e2af"/><stop offset="0.5" stop-color="#fab387"/><stop offset="1" stop-color="#f38ba8"/></linearGradient>
 </defs>
 <rect width="800" height="240" fill="url(#sky)"/>
 <circle cx="640" cy="150" r="74" fill="url(#sun)"/><rect x="566" y="118" width="148" height="4" fill="#11111b"/><rect x="566" y="127" width="148" height="4" fill="#11111b"/><rect x="566" y="136" width="148" height="4" fill="#11111b"/><rect x="566" y="145" width="148" height="4" fill="#11111b"/><rect x="566" y="154" width="148" height="4" fill="#11111b"/><rect x="566" y="163" width="148" height="4" fill="#11111b"/><rect x="566" y="172" width="148" height="4" fill="#11111b"/>
 <line x1="-200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="280" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="360" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="440" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="520" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="600" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="680" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="760" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="840" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="920" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="1000" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="0" y1="178" x2="800" y2="178" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="184" x2="800" y2="184" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="193" x2="800" y2="193" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="206" x2="800" y2="206" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="227" x2="800" y2="227" stroke="#94e2d5" stroke-width="1" opacity="0.22"/>
 <text x="48" y="150" font-size="78">🍞</text>
 <text x="150" y="96" font-family="ui-monospace,monospace" font-size="14" fill="#cba6f7" letter-spacing="5">90s KID //</text>
 <text x="150" y="140" font-family="-apple-system,Segoe UI,sans-serif" font-size="30" font-weight="800" fill="#cdd6f4">Flying Toasters and the Screen Sav</text>
 <rect x="0" y="236" width="800" height="4" fill="#f38ba8"/>
</svg></figure>
<h2 id="the-toasters-that-guarded-the-machine">The Toasters That Guarded the Machine</h2>
<p>You remember the flying toasters.</p>
<p>You weren&rsquo;t even using the computer.<br>
Nobody was.<br>
It just sat there in the den, humming, and then — winged toasters.<br>
Flapping. Drifting. With toast.</p>
<p>Cruising across a black void like tiny chrome angels carrying breakfast to heaven.</p>
<p>And you would just&hellip; <strong>watch it.</strong><br>
For way too long.</p>
<p>Here&rsquo;s what we knew about it as kids:</p>
<ul>
<li>It only showed up when <strong>everybody left</strong></li>
<li>It meant the computer was <em>asleep but still alive</em></li>
<li>Touching the mouse made it vanish, which felt like waking something</li>
<li>The toast was non-negotiable. The toast was sacred.</li>
<li>Your friend&rsquo;s house had <strong>a flying-pipes maze</strong> instead, and that felt fancier somehow</li>
</ul>
<p>It wasn&rsquo;t a program you opened.<br>
It was a thing the machine <em>became</em> when nobody was looking.</p>
<p>That was the magic part.<br>
The computer had a secret life and the toasters were proof.</p>
<hr>
<p>Here&rsquo;s what was actually going on.</p>
<p>Back then, your monitor was a <strong>CRT</strong> — a big heavy glass tube that fired electron beams at a phosphor coating to make the picture glow.</p>
<p>And those phosphors had a problem: if the <strong>exact same image</strong> sat on the screen too long, it could literally <em>burn in.</em> A ghost of that image would stain the glass forever. A spreadsheet grid. A toolbar. A menu bar, etched into the dark like a fossil.</p>
<p>So the screen saver was doing exactly what the name says — <strong>saving the screen.</strong></p>
<p>Move the pixels around constantly, never let one spot sit still, and nothing gets burned. The toasters weren&rsquo;t decoration.<br>
They were a tiny patrol.<br>
Keeping the picture moving so the glass stayed clean.</p>
<p>The famous flapping breakfast came from a software company called <strong>Berkeley Systems</strong>, in a pack named <em>After Dark.</em> They even got sued once over those toasters, which is the most 90s sentence ever written.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>        .-----------.
</span></span><span style="display:flex;"><span>       /  o   o   o  \   ~ ~ ~
</span></span><span style="display:flex;"><span>      |  [=========]  |   o  o
</span></span><span style="display:flex;"><span>      |  | T O A S |  |  flap
</span></span><span style="display:flex;"><span>       \ |  T  ~  T | /   flap
</span></span><span style="display:flex;"><span>        `-----------&#39;
</span></span><span style="display:flex;"><span>           \\   //
</span></span><span style="display:flex;"><span>          --\\-//--   pop! 🍞
</span></span><span style="display:flex;"><span>            `-&#39;-&#39;
</span></span><span style="display:flex;"><span>   the patrol is now airborne
</span></span></code></pre></div><hr>
<p>The funny part? Screen burn-in basically went away.</p>
<p>Flat LCDs didn&rsquo;t have those needy phosphors, so the danger faded.<br>
The toasters had won. They could rest.</p>
<p>Except they never actually left.</p>
<p>Your phone dims and floats a clock around the lock screen.<br>
Your TV nudges its glowing logo a few pixels every minute — because <strong>OLEDs</strong> can burn in, so we quietly reinvented the whole idea.<br>
Your work laptop snaps to a slideshow of blurry photos the second you stand up.</p>
<p>We don&rsquo;t call it a screen saver anymore.<br>
But every time a screen drifts gently to keep itself from getting hurt —</p>
<p>that&rsquo;s a toaster.<br>
Still flapping.<br>
Still carrying the toast.</p>
]]></content:encoded></item><item><title>Two Apps, One Port</title><link>https://errorzap.com/posts/two-apps-one-port/</link><pubDate>Sat, 18 Apr 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/two-apps-one-port/</guid><description>Edits vanished, reads lied, and nothing in the logs was wrong — because two copies of the same app were quietly knife-fighting over the same TCP port.</description><content:encoded><![CDATA[<figure style="text-align:center;margin:0 0 30px"><img src="hero.png" alt="Two Apps, One Port" style="max-width:520px;width:100%;border-radius:14px"/></figure>
<p>Someone pinged me with the kind of bug that makes your skin crawl: &ldquo;I save a note, it saves fine, no error. But when I come back later it&rsquo;s gone. And sometimes I open a file and it&rsquo;s the <em>wrong</em> file entirely.&rdquo;</p>
<p>No errors. No crash. Data just&hellip; wrong. The worst kind of ticket — because &ldquo;wrong data, no error&rdquo; means the system is lying to you with a straight face.</p>
<p>I assumed corruption. Maybe a bad sync, a stale cache, a half-written file on disk. I was wrong about all of it.</p>
<h2 id="the-investigation">The investigation</h2>
<p>First thing I did was stop trusting the app and start trusting the wire. The app talked to itself over a local REST API on a fixed port. So I asked the only question that matters when data is &ldquo;wrong&rdquo;:</p>
<p><strong>Am I even talking to the thing I think I&rsquo;m talking to?</strong></p>
<p>The user ran two separate profiles of the same application — two distinct workspaces, two configs, two processes. Different data, different windows. But same binary, same defaults.</p>
<p>I checked what was actually listening.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>        Profile A (started first)        Profile B (started second)
</span></span><span style="display:flex;"><span>        ┌──────────────────────┐         ┌──────────────────────┐
</span></span><span style="display:flex;"><span>        │  app instance A      │         │  app instance B      │
</span></span><span style="display:flex;"><span>        │  workspace: WORK     │         │  workspace: PERSONAL │
</span></span><span style="display:flex;"><span>        └──────────┬───────────┘         └──────────┬───────────┘
</span></span><span style="display:flex;"><span>                   │ bind :8200 OK                  │ bind :8200 FAIL
</span></span><span style="display:flex;"><span>                   ▼                                ▼
</span></span><span style="display:flex;"><span>            ┌─────────────┐                  (silent: port taken)
</span></span><span style="display:flex;"><span>            │  TCP :8200  │ ◄──── requests land HERE
</span></span><span style="display:flex;"><span>            └─────────────┘        regardless of which UI you used
</span></span><span style="display:flex;"><span>                   ▲
</span></span><span style="display:flex;"><span>                   │
</span></span><span style="display:flex;"><span>            edits in B&#39;s UI → API call → answered by A
</span></span></code></pre></div><p>Both instances were configured to expose their REST API on the <strong>same hard-coded port</strong>. Whichever process won the startup race grabbed the socket. The loser tried to bind, failed, and — because the failure was non-fatal — just shrugged and kept running its UI as if nothing happened.</p>
<p>So you&rsquo;d type into Profile B&rsquo;s window. The UI would fire an API write. That write went to <strong>port 8200</strong>, which Profile A owned. Profile A happily saved it into the <em>wrong workspace</em>. Reads came back from A too. Profile B&rsquo;s UI was a puppet with cut strings.</p>
<h2 id="the-aha">The &ldquo;aha&rdquo;</h2>
<p>The tell was <code>netstat</code>. One PID owned the port. Not two. Not a conflict logged anywhere — just one quiet winner.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># Windows: who actually holds the port?</span>
</span></span><span style="display:flex;"><span>netstat -ano | findstr :8200
</span></span><span style="display:flex;"><span><span style="color:#6272a4">#   TCP    127.0.0.1:8200    0.0.0.0:0    LISTENING    18044</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>tasklist /fi <span style="color:#f1fa8c">&#34;PID eq 18044&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">#   app.exe    18044   ...   &lt;- this is Profile A, not B</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># Linux/macOS equivalent</span>
</span></span><span style="display:flex;"><span>lsof -nP -iTCP:8200 -sTCP:LISTEN
</span></span><span style="display:flex;"><span>ss -ltnp <span style="color:#f1fa8c">&#39;sport = :8200&#39;</span>
</span></span></code></pre></div><p>One listener. Two apps. The data wasn&rsquo;t corrupt — it was just going to the wrong house, and the mailman never complained.</p>
<figure style="text-align:center;margin:34px 0">
<svg viewBox="0 0 560 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Two app instances both pointing at one shared port">
  <rect x="0" y="0" width="560" height="220" rx="10" fill="#11111b"/>
  <rect x="32" y="44" width="170" height="64" rx="10" fill="#1e1e2e" stroke="#45475a"/>
  <text x="117" y="72" text-anchor="middle" fill="#cdd6f4" font-family="monospace" font-size="14">Profile A</text>
  <text x="117" y="92" text-anchor="middle" fill="#6c7086" font-family="monospace" font-size="11">workspace: WORK</text>
  <rect x="358" y="44" width="170" height="64" rx="10" fill="#1e1e2e" stroke="#45475a"/>
  <text x="443" y="72" text-anchor="middle" fill="#cdd6f4" font-family="monospace" font-size="14">Profile B</text>
  <text x="443" y="92" text-anchor="middle" fill="#6c7086" font-family="monospace" font-size="11">workspace: PERSONAL</text>
  <rect x="210" y="150" width="140" height="46" rx="10" fill="#313244" stroke="#cba6f7"/>
  <text x="280" y="171" text-anchor="middle" fill="#cba6f7" font-family="monospace" font-size="13">:8200</text>
  <text x="280" y="187" text-anchor="middle" fill="#6c7086" font-family="monospace" font-size="10">one listener</text>
  <line x1="117" y1="108" x2="250" y2="150" stroke="#a6e3a1" stroke-width="2"/>
  <text x="150" y="135" fill="#a6e3a1" font-family="monospace" font-size="12">bind OK</text>
  <line x1="443" y1="108" x2="310" y2="150" stroke="#f38ba8" stroke-width="2" stroke-dasharray="5 4"/>
  <text x="372" y="135" fill="#f38ba8" font-family="monospace" font-size="12">bind FAIL → routes to A</text>
</svg>
<figcaption style="color:#6c7086;font-size:14px;margin-top:8px">Both UIs wired to one fixed port. The first process to start owns it; the second silently misroutes.</figcaption>
</figure>
<h2 id="the-fix">The fix</h2>
<p>Stop them from racing for the same socket. Give each instance its own port.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># 1. Edit each profile&#39;s config — distinct ports per instance</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">#    Profile A</span>
</span></span><span style="display:flex;"><span><span style="color:#f1fa8c">&#34;api&#34;</span>: <span style="color:#ff79c6">{</span> <span style="color:#f1fa8c">&#34;enabled&#34;</span>: true, <span style="color:#f1fa8c">&#34;port&#34;</span>: <span style="color:#bd93f9">8200</span> <span style="color:#ff79c6">}</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">#    Profile B</span>
</span></span><span style="display:flex;"><span><span style="color:#f1fa8c">&#34;api&#34;</span>: <span style="color:#ff79c6">{</span> <span style="color:#f1fa8c">&#34;enabled&#34;</span>: true, <span style="color:#f1fa8c">&#34;port&#34;</span>: <span style="color:#bd93f9">8201</span> <span style="color:#ff79c6">}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 2. Fully stop both instances (no orphan holding the old socket)</span>
</span></span><span style="display:flex;"><span>taskkill /f /im app.exe        <span style="color:#6272a4"># Windows</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># pkill -f app                 # Linux/macOS</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 3. Confirm the ports are actually free before restart</span>
</span></span><span style="display:flex;"><span>netstat -ano | findstr <span style="color:#f1fa8c">&#34;8200 8201&#34;</span>   <span style="color:#6272a4"># expect: nothing</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 4. Start both, then verify ownership — two PIDs, two ports</span>
</span></span><span style="display:flex;"><span>netstat -ano | findstr <span style="color:#f1fa8c">&#34;8200 8201&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">#   TCP  127.0.0.1:8200  LISTENING  20110   &lt;- Profile A</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">#   TCP  127.0.0.1:8201  LISTENING  20338   &lt;- Profile B</span>
</span></span></code></pre></div><p>Two listeners, two PIDs, two ports. Writes to B stayed in B. Reads told the truth. The &ldquo;disappearing edits&rdquo; were gone because they&rsquo;d never disappeared — they&rsquo;d just been filed in the wrong cabinet.</p>
<h2 id="why-it-happened">Why it happened</h2>
<p>Defaults. The app shipped with a single hard-coded API port and assumed exactly one instance would ever run. Reasonable for the common case, fatal for power users running multiple profiles. There was no port-conflict warning because a failed bind on a <em>convenience</em> API isn&rsquo;t fatal — the app degrades silently instead of refusing to start. Silent degradation is how a config bug masquerades as data loss.</p>
<h2 id="takeaways">Takeaways</h2>
<ul>
<li><strong>When data looks &ldquo;wrong&rdquo; but nothing errors, question the endpoint first.</strong> Confirm you&rsquo;re talking to the instance you <em>think</em> you are before you go disk-spelunking for corruption.</li>
<li><strong><code>netstat -ano</code> / <code>lsof -i</code> is ground truth for &ldquo;who owns this port.&rdquo;</strong> One listener where you expected two is the whole bug, right there.</li>
<li><strong>Multiple instances of the same app will knife-fight over hard-coded ports.</strong> Give every instance a unique port in its config — don&rsquo;t trust defaults to coexist.</li>
<li><strong>A non-fatal bind failure is a trap.</strong> Silent degradation turns a 5-minute config fix into a week of phantom data-loss tickets.</li>
<li><strong>Pre-flight before restart:</strong> kill all instances, verify the port is actually free, <em>then</em> start — and read back the listeners to prove it stuck.</li>
</ul>
]]></content:encoded></item><item><title>90s Kid - The Surge Protector Everything Plugged Into</title><link>https://errorzap.com/posts/90s-kid-the-surge-protector/</link><pubDate>Wed, 15 Apr 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/90s-kid-the-surge-protector/</guid><description>You remember the long beige bar on the floor with the little glowing button, the one that ran the entire family universe.</description><content:encoded><![CDATA[<figure style="margin:0 0 28px">
<svg viewBox="0 0 800 240" xmlns="http://www.w3.org/2000/svg" style="width:100%;border-radius:14px">
 <defs>
  <linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#1e1e2e"/><stop offset="1" stop-color="#11111b"/></linearGradient>
  <linearGradient id="sun" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f9e2af"/><stop offset="0.5" stop-color="#fab387"/><stop offset="1" stop-color="#f38ba8"/></linearGradient>
 </defs>
 <rect width="800" height="240" fill="url(#sky)"/>
 <circle cx="640" cy="150" r="74" fill="url(#sun)"/><rect x="566" y="118" width="148" height="4" fill="#11111b"/><rect x="566" y="127" width="148" height="4" fill="#11111b"/><rect x="566" y="136" width="148" height="4" fill="#11111b"/><rect x="566" y="145" width="148" height="4" fill="#11111b"/><rect x="566" y="154" width="148" height="4" fill="#11111b"/><rect x="566" y="163" width="148" height="4" fill="#11111b"/><rect x="566" y="172" width="148" height="4" fill="#11111b"/>
 <line x1="-200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="280" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="360" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="440" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="520" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="600" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="680" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="760" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="840" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="920" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="1000" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="0" y1="178" x2="800" y2="178" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="184" x2="800" y2="184" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="193" x2="800" y2="193" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="206" x2="800" y2="206" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="227" x2="800" y2="227" stroke="#94e2d5" stroke-width="1" opacity="0.22"/>
 <text x="48" y="150" font-size="78">🔌</text>
 <text x="150" y="96" font-family="ui-monospace,monospace" font-size="14" fill="#cba6f7" letter-spacing="5">90s KID //</text>
 <text x="150" y="140" font-family="-apple-system,Segoe UI,sans-serif" font-size="30" font-weight="800" fill="#cdd6f4">The Surge Protector Everything Plu</text>
 <rect x="0" y="236" width="800" height="4" fill="#f38ba8"/>
</svg></figure>
<h2 id="the-beige-bar-that-held-up-the-whole-house">The Beige Bar That Held Up The Whole House</h2>
<p>You remember the <strong>surge protector</strong>.</p>
<p>The long beige slab that lived on the floor behind the desk.<br>
The one with the orange glow at one end.<br>
The one nobody was allowed to step on.</p>
<p>It hummed in a way you could feel more than hear.</p>
<p>And it had <strong>the switch</strong>. That big rocker with the tiny red light, the one that turned the <em>entire</em> family computer empire on and off in a single satisfying <em>clunk</em>.</p>
<hr>
<p>You didn&rsquo;t know what it did.</p>
<p>You just knew it was important.</p>
<p>The vibes were:</p>
<ul>
<li><strong>Forbidden.</strong> Dad said never unplug it. So you never unplugged it.</li>
<li><strong>A little dangerous.</strong> It said WARNING on it. In yellow. With a lightning bolt.</li>
<li><strong>Overloaded.</strong> Six outlets, somehow eleven things plugged in, a tangle like a nest of beige snakes.</li>
<li><strong>The boss of everything.</strong> Modem, monitor, the printer that screamed, the tower, the speakers with the subwoofer that lived under the desk. All of it answered to the bar.</li>
<li><strong>Magic.</strong> Press the switch and the whole room <em>woke up.</em> CRTs ticked, fans spun, the modem blinked. You were a wizard.</li>
</ul>
<p>It was, honestly, the closest thing your bedroom had to a circuit breaker, and you were eight, and you respected it.</p>
<hr>
<p>Here&rsquo;s what it actually was.</p>
<p>Inside that beige slab was a tiny, heroic component called a <strong>MOV</strong> — a metal oxide varistor.</p>
<p>Most of the time it does <strong>nothing.</strong> It just sits there. But the moment the voltage from the wall spikes — a lightning strike down the block, the AC compressor kicking on, the fridge cycling — the MOV&rsquo;s resistance suddenly <strong>drops</strong> and it shunts that extra energy away from your computer before it can fry the motherboard.</p>
<p>It&rsquo;s a sacrificial guard. It literally throws itself on the grenade.</p>
<p>That&rsquo;s also why old surge protectors quietly stop protecting. Each spike wears the MOV down a little. The strip still passes power, the light still glows — but the <em>protection</em> is long dead. It became a fancy extension cord and nobody told you.</p>
<p>The glowing light? Often just &ldquo;there is power here.&rdquo; Not &ldquo;you are safe.&rdquo;</p>
<p>The fancier ones had a second light — <strong>&ldquo;PROTECTED&rdquo;</strong> / <strong>&ldquo;GROUNDED&rdquo;</strong> — and <em>that</em> one going dark was the part that actually mattered.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>   ___________________________________________
</span></span><span style="display:flex;"><span>  |  o   [|] [|] [|] [|] [|] [|]            (~) |   &lt;- the glowing switch
</span></span><span style="display:flex;"><span>  |  o    |   |   |   |   |   |          POWER  |
</span></span><span style="display:flex;"><span>  |_______|___|___|___|___|___|________________|
</span></span><span style="display:flex;"><span>          |   |   |   |   |   |
</span></span><span style="display:flex;"><span>        modem mon tower spkr prnt  ...and one mystery plug
</span></span><span style="display:flex;"><span>              \__ J O I N E D   F O R E V E R __/
</span></span></code></pre></div><hr>
<p>The funny thing is, it <strong>never left.</strong></p>
<p>It&rsquo;s still down there behind your desk <em>right now.</em> Different shape, same job. Maybe it grew a coax port for the cable line, or a couple USB-A holes, or a little app that emails you when a fuse pops.</p>
<p>We just stopped calling it the boss.</p>
<p>But every time your whole battlestation winks on with one press — the monitors, the dock, the mechanical keyboard with its own light show — that&rsquo;s the same beige magic.</p>
<p>Still sitting on the floor.<br>
Still quietly ready to take the hit for you.</p>
<p>You just don&rsquo;t say thank you anymore.</p>
]]></content:encoded></item><item><title>The Bot That Hung Because of IPv6</title><link>https://errorzap.com/posts/the-bot-that-hung-because-of-ipv6/</link><pubDate>Wed, 15 Apr 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/the-bot-that-hung-because-of-ipv6/</guid><description>A Telegram bot kept freezing on outbound calls with no error — turns out a NIC with IPv6 addresses but no working IPv6 routing was quietly killing every request.</description><content:encoded><![CDATA[<figure style="text-align:center;margin:0 0 30px"><img src="hero.png" alt="The Bot That Hung Because of IPv6" style="max-width:520px;width:100%;border-radius:14px"/></figure>
<p>The bot worked. Then it didn&rsquo;t. Then it worked again.</p>
<p>It was a small Node.js Telegram bot running on a client&rsquo;s server. Its whole job was to fire off outbound API calls — <code>sendMessage</code>, <code>getUpdates</code>, the usual. Most of the time it was instant. But every so often a call would just&hellip; stop. No exception. No 500. No DNS error. The promise sat there until the request timeout finally put it out of its misery seconds later.</p>
<p>Intermittent. Silent. Multi-second. The worst kind of bug there is.</p>
<h2 id="the-investigation">The investigation</h2>
<p>First instinct was the obvious one: the upstream API is flaky. So I logged timing around every call. The hangs weren&rsquo;t correlated with anything — not load, not time of day, not a specific endpoint. Same call, same payload, same host. Sometimes 80ms, sometimes 5,000ms-then-timeout.</p>
<p>I pulled <code>strace</code> on the process during a hang and watched the syscalls. The bot wasn&rsquo;t busy. It was sitting in a <code>connect()</code> that never came back. It wasn&rsquo;t waiting on the API — it was waiting on a socket that was never going to open.</p>
<p>That reframed everything. This wasn&rsquo;t a slow server. This was a connection attempt going into the void.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>   bot ──► undici (happy-eyeballs)
</span></span><span style="display:flex;"><span>              │
</span></span><span style="display:flex;"><span>              ├─► resolve host
</span></span><span style="display:flex;"><span>              │     ├─ AAAA (IPv6)  ─► ::  ►  connect() ──► ✗ black hole (no route)
</span></span><span style="display:flex;"><span>              │     └─ A    (IPv4)  ─► .   ►  connect() ──► ✓ would work fine
</span></span><span style="display:flex;"><span>              │
</span></span><span style="display:flex;"><span>              ▼
</span></span><span style="display:flex;"><span>        races both... but stalls on the v6 attempt
</span></span></code></pre></div><p>The host had <strong>IPv6 addresses configured</strong> on its primary NIC. It just had <strong>no working IPv6 route</strong> to the outside world. Addresses present, connectivity dead. The packets went out and nothing ever came back.</p>
<h2 id="the-aha">The &ldquo;aha&rdquo;</h2>
<p>The bot used <code>undici</code> as its HTTP client. Modern undici does Happy Eyeballs (RFC 8305) — it resolves both A and AAAA records and races IPv4 and IPv6 connection attempts, preferring the v6 path.</p>
<p>On a healthy host that&rsquo;s great. On this host it was poison. undici would get an AAAA record, try the IPv6 path first, and that <code>connect()</code> would sail into a black hole. The race timer is supposed to fall back to IPv4 — but with a route that silently drops packets instead of cleanly rejecting them, the fallback timing slipped and the whole call stalled until the app-level timeout.</p>
<p>A quick <code>curl</code> proved it cold:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># hangs forever — there&#39;s an AAAA record and broken v6 routing</span>
</span></span><span style="display:flex;"><span>curl -v https://api.example.com/
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># instant — forced down IPv4</span>
</span></span><span style="display:flex;"><span>curl -4 -v https://api.example.com/
</span></span></code></pre></div><p><code>-6</code> hung. <code>-4</code> flew. That&rsquo;s your smoking gun.</p>
<figure style="text-align:center;margin:34px 0">
<svg viewBox="0 0 560 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="IPv6 path to a black hole, IPv4 path succeeds">
  <rect x="0" y="0" width="560" height="220" rx="10" fill="#11111b"/>
  <rect x="28" y="86" width="120" height="48" rx="8" fill="#1e1e2e" stroke="#45475a"/>
  <text x="88" y="115" text-anchor="middle" font-family="monospace" font-size="14" fill="#cdd6f4">the bot</text>
  <path d="M148 100 H290" stroke="#f38ba8" stroke-width="2.5" fill="none"/>
  <polygon points="300,100 288,94 288,106" fill="#f38ba8"/>
  <text x="222" y="90" text-anchor="middle" font-family="monospace" font-size="12" fill="#f38ba8">AAAA / IPv6</text>
  <circle cx="360" cy="100" r="34" fill="#1e1e2e" stroke="#f38ba8" stroke-width="2.5"/>
  <text x="360" y="96" text-anchor="middle" font-family="monospace" font-size="11" fill="#f38ba8">black</text>
  <text x="360" y="110" text-anchor="middle" font-family="monospace" font-size="11" fill="#f38ba8">hole ✗</text>
  <path d="M148 120 H300 Q330 120 340 150 H460" stroke="#a6e3a1" stroke-width="2.5" fill="none"/>
  <polygon points="470,150 458,144 458,156" fill="#a6e3a1"/>
  <text x="250" y="172" text-anchor="middle" font-family="monospace" font-size="12" fill="#a6e3a1">A / IPv4</text>
  <rect x="470" y="128" width="68" height="44" rx="8" fill="#1e1e2e" stroke="#a6e3a1" stroke-width="2"/>
  <text x="504" y="155" text-anchor="middle" font-family="monospace" font-size="13" fill="#a6e3a1">API ✓</text>
</svg>
<figcaption style="color:#6c7086;font-size:14px;margin-top:8px">Happy Eyeballs races both paths — but a route that drops packets stalls the v6 attempt instead of failing fast.</figcaption>
</figure>
<h2 id="the-fix">The fix</h2>
<p>Two layers. First, kill IPv6 on the one NIC that had it broken — while leaving it alone on the VPN and VM interfaces that genuinely use it.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># disable IPv6 on the offending interface only (e.g. eth0), keep it elsewhere</span>
</span></span><span style="display:flex;"><span>sudo sysctl -w net.ipv6.conf.eth0.disable_ipv6<span style="color:#ff79c6">=</span><span style="color:#bd93f9">1</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># make it stick across reboots</span>
</span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#39;net.ipv6.conf.eth0.disable_ipv6 = 1&#39;</span> | sudo tee /etc/sysctl.d/99-disable-ipv6-eth0.conf
</span></span><span style="display:flex;"><span>sudo sysctl --system
</span></span></code></pre></div><p>Second, belt and suspenders at the app layer — force the resolver IPv4-first so even a half-broken host can&rsquo;t bite us again:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-js" data-lang="js"><span style="display:flex;"><span><span style="color:#6272a4">// top of the entrypoint, before any outbound calls
</span></span></span><span style="display:flex;"><span><span style="color:#ff79c6">const</span> dns <span style="color:#ff79c6">=</span> require(<span style="color:#f1fa8c">&#39;node:dns&#39;</span>)
</span></span><span style="display:flex;"><span>dns.setDefaultResultOrder(<span style="color:#f1fa8c">&#39;ipv4first&#39;</span>)
</span></span></code></pre></div><p><code>curl -4</code> already proved the IPv4 path was healthy, so this was a safe, deterministic pin. Restarted the bot, hammered it with calls for ten minutes. Zero hangs. Every request back under 200ms.</p>
<h2 id="why-it-happened">Why it happened</h2>
<p>&ldquo;Broken but present&rdquo; IPv6 is worse than no IPv6 at all. If the host had <strong>no</strong> AAAA-resolvable path, the client never tries v6 and you never notice. But give it IPv6 addresses with no real route, and every modern HTTP client cheerfully tries the dead path first — then waits on a <code>connect()</code> that silently drops instead of refusing.</p>
<p>No RST means no fast failure. No fast failure means a stall. And because Happy Eyeballs only <em>sometimes</em> loses the race badly, the stall is intermittent. That&rsquo;s why it looked like a flaky upstream when it was a flaky local NIC the whole time.</p>
<h2 id="takeaways">Takeaways</h2>
<ul>
<li><strong>If something hangs only <em>sometimes</em> on outbound HTTP, suspect IPv6.</strong> Test it in one command: <code>curl -4</code> vs <code>curl -6</code>. If <code>-6</code> hangs, you found it.</li>
<li><strong>Addresses ≠ connectivity.</strong> A NIC can hold a perfectly valid IPv6 address and still have zero working route to the internet.</li>
<li><strong>Disable IPv6 per-interface, not globally.</strong> Kill it on the broken NIC; leave it on VPN/VM links that actually need it.</li>
<li><strong>Pin IPv4-first at the app layer too</strong> (<code>dns.setDefaultResultOrder('ipv4first')</code>) — defense in depth for hosts you don&rsquo;t fully control.</li>
<li><strong>A <code>connect()</code> that never returns is a routing problem, not a server problem.</strong> <code>strace</code> the hang before you blame the API.</li>
</ul>
]]></content:encoded></item><item><title>How a Mesh VPN Killed My SSH Jump Host</title><link>https://errorzap.com/posts/how-a-mesh-vpn-killed-my-ssh-jump-host/</link><pubDate>Sat, 11 Apr 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/how-a-mesh-vpn-killed-my-ssh-jump-host/</guid><description>The bastion broke every time an IP changed, so I deleted the bastion.</description><content:encoded><![CDATA[<figure style="text-align:center;margin:0 0 30px"><img src="hero.png" alt="How a Mesh VPN Killed My SSH Jump Host" style="max-width:520px;width:100%;border-radius:14px"/></figure>
<p>There&rsquo;s a box at a remote site. It lives behind NAT, behind a router I don&rsquo;t control, on a connection that hands out a new public IP whenever the ISP feels like it.</p>
<p>To reach it, I had a ritual. SSH into a bastion. From the bastion, SSH into the target. Type two passphrases, pray nothing changed, and hope the WAN IP I memorized last month was still the WAN IP.</p>
<pre tabindex="0"><code>ssh -J bastion target
</code></pre><p>That one flag hid a small empire of fragility.</p>
<h2 id="the-investigation">The investigation</h2>
<p>It broke on a Tuesday, the way these things do. <code>ssh: connect to host ... port 22: Connection timed out</code>.</p>
<p>First instinct: the target is down. It wasn&rsquo;t — a colleague on-site confirmed it was humming along, recording, serving, fine.</p>
<p>Second instinct: the bastion is down. It wasn&rsquo;t either. I could land on the bastion clean.</p>
<pre tabindex="0"><code>ssh bastion &#39;echo alive&#39;
alive
</code></pre><p>So the bastion was up, the target was up, and the hop between them was dead. I jumped onto the bastion and tried the second leg by hand.</p>
<pre tabindex="0"><code>ssh me@198.51.100.x
ssh: connect to host 198.51.100.x port 22: No route to host
</code></pre><p>There it was. The remote site&rsquo;s WAN IP had rotated overnight. The <code>198.51.100.x</code> baked into my SSH config — and into the bastion&rsquo;s <code>known_hosts</code>, and into a port-forward rule, and into my muscle memory — now pointed at some stranger&rsquo;s DHCP lease.</p>
<h2 id="the-aha">The &ldquo;aha&rdquo;</h2>
<p>I&rsquo;d built a chain of three brittle links to solve one problem: <em>the target has no stable address I can reach from outside.</em></p>
<p>Every fix I&rsquo;d ever applied — dynamic DNS, a forwarded port, a second hop — was just another way of chasing an address that refused to hold still. The bastion wasn&rsquo;t a solution. It was a workaround for a missing fact.</p>
<p>What if the box just <em>had</em> a stable address? One that didn&rsquo;t care about NAT, ISP whims, or which coffee shop my laptop was sitting in?</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>   BEFORE                                AFTER
</span></span><span style="display:flex;"><span> ┌─────────┐                          ┌─────────┐
</span></span><span style="display:flex;"><span> │ laptop  │                          │ laptop  │
</span></span><span style="display:flex;"><span> └────┬────┘                          └────┬────┘
</span></span><span style="display:flex;"><span>      │ ssh                                │ ssh
</span></span><span style="display:flex;"><span>      ▼                                    │ (encrypted mesh)
</span></span><span style="display:flex;"><span> ┌─────────┐                               │
</span></span><span style="display:flex;"><span> │ bastion │  ◄── public IP that          │
</span></span><span style="display:flex;"><span> └────┬────┘      keeps rotating          │
</span></span><span style="display:flex;"><span>      │ ssh                                ▼
</span></span><span style="display:flex;"><span>      ▼                                ┌─────────┐
</span></span><span style="display:flex;"><span> ┌─────────┐   NAT / CGNAT             │ target  │
</span></span><span style="display:flex;"><span> │ target  │   no inbound port         │ 100.x.. │ stable tailnet IP
</span></span><span style="display:flex;"><span> └─────────┘                           └─────────┘
</span></span></code></pre></div><p>That address is exactly what a WireGuard-based mesh VPN hands you. Put the laptop and the target on the same tailnet, and each device gets a private IP that follows it everywhere — through NAT, through CGNAT, through IP rotations — because the mesh handles NAT traversal for you.</p>
<h2 id="the-fix">The fix</h2>
<p>Install the mesh on both ends. (Generic Tailscale-style flow below.)</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># On the target — behind NAT, no inbound ports needed</span>
</span></span><span style="display:flex;"><span>curl -fsSL https://tailscale.com/install.sh | sh
</span></span><span style="display:flex;"><span>sudo tailscale up --ssh --hostname remote-target
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># On my laptop</span>
</span></span><span style="display:flex;"><span>tailscale up
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># Confirm the target shows a stable tailnet IP</span>
</span></span><span style="display:flex;"><span>tailscale status
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 100.x.y.z   remote-target   linux   active; direct</span>
</span></span></code></pre></div><p>Now SSH goes straight there. No bastion, no <code>-J</code>, no rotating address.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ssh me@100.x.y.z
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># or, by MagicDNS name</span>
</span></span><span style="display:flex;"><span>ssh me@remote-target
</span></span></code></pre></div><p>Lock it down with an ACL so only my devices can reach port 22 on that box:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#ff79c6">&#34;acls&#34;</span>: [
</span></span><span style="display:flex;"><span>    { <span style="color:#ff79c6">&#34;action&#34;</span>: <span style="color:#f1fa8c">&#34;accept&#34;</span>, <span style="color:#ff79c6">&#34;src&#34;</span>: [<span style="color:#f1fa8c">&#34;tag:admin&#34;</span>], <span style="color:#ff79c6">&#34;dst&#34;</span>: [<span style="color:#f1fa8c">&#34;tag:remote:22&#34;</span>] }
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Then I deleted the bastion&rsquo;s forward rule, pulled the stale host key, and retired the jump chain entirely.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ssh-keygen -R 198.51.100.x
</span></span></code></pre></div><figure style="text-align:center;margin:34px 0">
<svg viewBox="0 0 560 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Mesh VPN topology replacing a bastion hop">
  <rect x="0" y="0" width="560" height="220" rx="10" fill="#11111b"/>
  <text x="280" y="28" fill="#cdd6f4" font-family="monospace" font-size="15" text-anchor="middle">one tailnet, every device gets a stable address</text>
  <!-- laptop -->
  <rect x="40" y="80" width="120" height="60" rx="8" fill="#1e1e2e" stroke="#45475a"/>
  <text x="100" y="106" fill="#89b4fa" font-family="monospace" font-size="13" text-anchor="middle">laptop</text>
  <text x="100" y="126" fill="#6c7086" font-family="monospace" font-size="11" text-anchor="middle">100.64.0.2</text>
  <!-- target -->
  <rect x="400" y="80" width="120" height="60" rx="8" fill="#1e1e2e" stroke="#45475a"/>
  <text x="460" y="106" fill="#a6e3a1" font-family="monospace" font-size="13" text-anchor="middle">target</text>
  <text x="460" y="126" fill="#6c7086" font-family="monospace" font-size="11" text-anchor="middle">100.64.0.7</text>
  <!-- NAT cloud in middle, bypassed -->
  <rect x="230" y="150" width="100" height="40" rx="8" fill="#313244" stroke="#45475a"/>
  <text x="280" y="175" fill="#f38ba8" font-family="monospace" font-size="12" text-anchor="middle">NAT / CGNAT</text>
  <!-- direct encrypted line -->
  <line x1="160" y1="110" x2="400" y2="110" stroke="#cba6f7" stroke-width="3"/>
  <circle cx="280" cy="110" r="6" fill="#94e2d5"/>
  <text x="280" y="66" fill="#fab387" font-family="monospace" font-size="12" text-anchor="middle">WireGuard tunnel · ACL-gated</text>
  <!-- dashed line showing NAT is bypassed -->
  <line x1="280" y1="116" x2="280" y2="150" stroke="#6c7086" stroke-width="1.5" stroke-dasharray="4 4"/>
</svg>
<figcaption style="color:#6c7086;font-size:14px;margin-top:8px">The hop is gone. The tunnel goes straight through NAT, ACL-gated, end to end.</figcaption>
</figure>
<h2 id="why-it-happened">Why it happened</h2>
<p>The bastion was never the point. It existed because the target had no reachable, durable address — so I borrowed one from a machine that did, then chained a second SSH session on top.</p>
<p>Every link in that chain depended on a public IP staying put. One of them rotated, and the whole thing collapsed. That&rsquo;s not a bug in SSH. That&rsquo;s the architecture telling you it was held together with assumptions.</p>
<p>A mesh VPN moves the addressing problem off the public internet entirely. The tailnet IP is yours, it&rsquo;s stable, and it doesn&rsquo;t care what the ISP does at 3 a.m.</p>
<h2 id="takeaways">Takeaways</h2>
<ul>
<li><strong>If you&rsquo;re chaining <code>ssh -J</code> to reach a NAT&rsquo;d box, that&rsquo;s a missing stable address, not a routing puzzle.</strong> Fix the address, delete the chain.</li>
<li><strong>Mesh VPNs (WireGuard-based) give every device a durable private IP</strong> that survives NAT, CGNAT, and ISP rotations — no inbound ports to forward.</li>
<li><strong>Stable addressing kills a whole class of failures</strong>: stale <code>known_hosts</code>, dead port-forwards, dynamic-DNS lag, memorized WAN IPs.</li>
<li><strong>Gate it with ACLs.</strong> A flat tailnet where everything reaches everything is just a bigger blast radius. Tag, scope, restrict to port 22.</li>
<li><strong>The most reliable hop is the one you deleted.</strong> Fewer moving parts, fewer 3 a.m. timeouts.</li>
</ul>
]]></content:encoded></item><item><title>90s Kid - The Pager</title><link>https://errorzap.com/posts/90s-kid-the-pager/</link><pubDate>Fri, 10 Apr 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/90s-kid-the-pager/</guid><description>You remember the little black box on your dad&amp;rsquo;s belt that buzzed once and changed his whole face.</description><content:encoded><![CDATA[<figure style="margin:0 0 28px">
<svg viewBox="0 0 800 240" xmlns="http://www.w3.org/2000/svg" style="width:100%;border-radius:14px">
 <defs>
  <linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#1e1e2e"/><stop offset="1" stop-color="#11111b"/></linearGradient>
  <linearGradient id="sun" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f9e2af"/><stop offset="0.5" stop-color="#fab387"/><stop offset="1" stop-color="#f38ba8"/></linearGradient>
 </defs>
 <rect width="800" height="240" fill="url(#sky)"/>
 <circle cx="640" cy="150" r="74" fill="url(#sun)"/><rect x="566" y="118" width="148" height="4" fill="#11111b"/><rect x="566" y="127" width="148" height="4" fill="#11111b"/><rect x="566" y="136" width="148" height="4" fill="#11111b"/><rect x="566" y="145" width="148" height="4" fill="#11111b"/><rect x="566" y="154" width="148" height="4" fill="#11111b"/><rect x="566" y="163" width="148" height="4" fill="#11111b"/><rect x="566" y="172" width="148" height="4" fill="#11111b"/>
 <line x1="-200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="280" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="360" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="440" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="520" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="600" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="680" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="760" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="840" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="920" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="1000" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="0" y1="178" x2="800" y2="178" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="184" x2="800" y2="184" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="193" x2="800" y2="193" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="206" x2="800" y2="206" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="227" x2="800" y2="227" stroke="#94e2d5" stroke-width="1" opacity="0.22"/>
 <text x="48" y="150" font-size="78">📟</text>
 <text x="150" y="96" font-family="ui-monospace,monospace" font-size="14" fill="#cba6f7" letter-spacing="5">90s KID //</text>
 <text x="150" y="140" font-family="-apple-system,Segoe UI,sans-serif" font-size="30" font-weight="800" fill="#cdd6f4">The Pager</text>
 <rect x="0" y="236" width="800" height="4" fill="#f38ba8"/>
</svg></figure>
<h2 id="the-little-black-box-that-owned-the-grown-ups">The Little Black Box That Owned The Grown-Ups</h2>
<p>You remember the pager.</p>
<p>Clipped to a belt.<br>
Hooked to a waistband.<br>
Riding shotgun in a fanny pack like it paid rent.</p>
<p>It was <strong>small</strong>. It was <strong>plastic</strong>. It was, somehow, the most important object in the house.</p>
<p>When it buzzed, the room changed.</p>
<p>Dad would tilt it toward the light, squint at a row of numbers, and go <em>&ldquo;hm.&rdquo;</em></p>
<p>Just <strong>&ldquo;hm.&rdquo;</strong></p>
<p>And then he&rsquo;d disappear to the kitchen phone.</p>
<hr>
<p>You didn&rsquo;t know what it did. Not really.</p>
<p>Here&rsquo;s what you knew:</p>
<ul>
<li>It only spoke in <strong>numbers</strong>, and the numbers were a secret</li>
<li><strong>You</strong> were not allowed to touch it</li>
<li>It made a noise that meant <em>the day was about to change</em></li>
<li>Important people had one. <strong>Doctors.</strong> Drug dealers on TV. Your uncle, for some reason.</li>
<li>Sometimes it said <strong>07734</strong> and that was the funniest thing humans had ever done</li>
</ul>
<p>It felt like a tiny walkie-talkie that only listened.<br>
A beeper that knew things you didn&rsquo;t.<br>
A pocket oracle that whispered to adults in a code you&rsquo;d crack <em>someday.</em></p>
<p>You&rsquo;d press the little button just to watch the screen light up green.<br>
Then put it down fast, like it was hot.</p>
<hr>
<p>Here&rsquo;s what it actually was.</p>
<p>A pager was a <strong>one-way radio receiver.</strong> That&rsquo;s it. No screen full of apps. No internet. Just a dumb, brilliant little antenna listening for <strong>its own number</strong> on a wide radio frequency, broadcast from tall towers across the whole region.</p>
<p>Somebody dialed the pager&rsquo;s phone number, typed digits, and a <strong>paging network</strong> flung that message out over the air to <em>every tower at once.</em> Your dad&rsquo;s beeper heard its unique address in the stream, grabbed the digits meant for it, and buzzed.</p>
<p>The &ldquo;code&rdquo;? It was just a callback phone number. Sometimes with a <code>911</code> tacked on for <em>&ldquo;call me NOW.&rdquo;</em></p>
<p>And it worked when nothing else did. Pagers ran on a frequency that <strong>soaks into buildings</strong> where cell signal still face-plants to this day. One AA battery. Weeks of standby. Basically unkillable.</p>
<p>It wasn&rsquo;t magic.</p>
<p>It was just <strong>really, really good engineering</strong> wearing a tiny green screen.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>        ________________________
</span></span><span style="display:flex;"><span>       |  .------------------.   |
</span></span><span style="display:flex;"><span>       | |                    |  |
</span></span><span style="display:flex;"><span>       | |   1-555-04 07734   |  |
</span></span><span style="display:flex;"><span>       | |       *911*        |  |
</span></span><span style="display:flex;"><span>       | |____________________|  |
</span></span><span style="display:flex;"><span>       |     [MENU]  [READ]      |
</span></span><span style="display:flex;"><span>       |   .__.   o o o   .__.   |
</span></span><span style="display:flex;"><span>       |__/====\___________/===\_|
</span></span><span style="display:flex;"><span>          BEEP · BEEP · BEEP
</span></span></code></pre></div><p>The pager never really died. It just took off the belt and moved into your pocket.</p>
<p>That buzz against your leg? <strong>Still there.</strong> Every text, every DM, every push notification is a pager alert with a marketing budget. Hospitals <em>still</em> run pagers, because when the network&rsquo;s on fire, the dumb little radio is the one thing that still gets through.</p>
<p>So next time your phone buzzes and your whole face changes —</p>
<p>just know your dad did that first.</p>
<p>With a beeper.</p>
<p>And a callback number that probably said <strong>07734</strong>.</p>
]]></content:encoded></item><item><title>The Speaker That Caused a Network Storm</title><link>https://errorzap.com/posts/the-speaker-that-caused-a-network-storm/</link><pubDate>Wed, 08 Apr 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/the-speaker-that-caused-a-network-storm/</guid><description>A wired network melting down at a customer site, traced to a pair of consumer speakers quietly braiding a layer-2 loop out of copper and mesh.</description><content:encoded><![CDATA[<figure style="text-align:center;margin:0 0 30px"><img src="hero.png" alt="The Speaker That Caused a Network Storm" style="max-width:520px;width:100%;border-radius:14px"/></figure>
<p>The call came in the way these always do: &ldquo;the whole network is slow.&rdquo;</p>
<p>Not down. Slow. Which is worse, because slow means it&rsquo;s still trying.</p>
<p>I pulled up the switch stats from the site and the picture was ugly. Broadcast counters spinning like a gas pump. Port LEDs across the access switches strobing in unison — that synchronized, hypnotic blink that should make the hair on your neck stand up. CPU on the core switch pinned. Every wired device crawling, half the VLANs timing out.</p>
<p>That&rsquo;s not congestion. That&rsquo;s a storm.</p>
<h2 id="the-investigation">The investigation</h2>
<p>A broadcast storm has a signature. One frame gets flooded out every port, comes back in another port, gets flooded again, forever, at line rate. The switches aren&rsquo;t broken — they&rsquo;re doing exactly what they&rsquo;re told, infinitely.</p>
<p>So the question is never &ldquo;what&rsquo;s slow.&rdquo; It&rsquo;s &ldquo;where&rsquo;s the loop.&rdquo;</p>
<p>I started where loops live: the edge. I pulled the MAC address table and watched a handful of addresses flapping between ports — the same MAC showing up on port 14, then port 9, then port 14, dozens of times a second. A stable MAC doesn&rsquo;t teleport. A looped one does.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>show mac address-table | the same MAC, two ports, flapping
</span></span><span style="display:flex;"><span>─────────────────────────────────────────────────────────
</span></span><span style="display:flex;"><span>  02:00:00:aa:bb:cc   Gi0/14   ◄─┐
</span></span><span style="display:flex;"><span>  02:00:00:aa:bb:cc   Gi0/9    ◄─┤  same address,
</span></span><span style="display:flex;"><span>  02:00:00:aa:bb:cc   Gi0/14   ◄─┤  two ports,
</span></span><span style="display:flex;"><span>  02:00:00:aa:bb:cc   Gi0/9    ◄─┘  ~40x/sec  ✗
</span></span></code></pre></div><p>I traced both ports. Port 14 and port 9 went to two units of a consumer multi-room speaker system — the kind that builds its own little wireless mesh between speakers so they stay in sync.</p>
<p>And somebody had wired <em>both</em> of them into the LAN.</p>
<h2 id="the-aha">The &ldquo;aha&rdquo;</h2>
<p>There it was. Each speaker had an Ethernet drop. Each speaker was <em>also</em> bridging to its sibling over the proprietary wireless mesh. So a frame could leave the switch on port 14, ride into speaker A, hop the wireless mesh to speaker B, and come right back into the switch on port 9.</p>
<p>Copper out, radio across, copper back in. A loop with one leg made of Wi-Fi.</p>
<figure style="text-align:center;margin:34px 0">
<svg viewBox="0 0 560 220" xmlns="http://www.w3.org/2000/svg" font-family="ui-monospace,monospace" role="img" aria-label="Two wired speakers bridged by a wireless mesh forming a layer-2 loop">
<rect x="0" y="0" width="560" height="220" fill="#11111b" rx="10"/>
<rect x="210" y="20" width="140" height="44" rx="8" fill="#1e1e2e" stroke="#45475a"/>
<text x="280" y="47" fill="#cdd6f4" font-size="14" text-anchor="middle">Switch</text>
<rect x="70" y="140" width="130" height="44" rx="8" fill="#313244" stroke="#45475a"/>
<text x="135" y="167" fill="#cba6f7" font-size="13" text-anchor="middle">Speaker A</text>
<rect x="360" y="140" width="130" height="44" rx="8" fill="#313244" stroke="#45475a"/>
<text x="425" y="167" fill="#cba6f7" font-size="13" text-anchor="middle">Speaker B</text>
<line x1="240" y1="64" x2="135" y2="140" stroke="#89b4fa" stroke-width="2.5"/>
<line x1="320" y1="64" x2="425" y2="140" stroke="#89b4fa" stroke-width="2.5"/>
<text x="150" y="108" fill="#89b4fa" font-size="11" text-anchor="middle">copper</text>
<text x="410" y="108" fill="#89b4fa" font-size="11" text-anchor="middle">copper</text>
<line x1="200" y1="162" x2="360" y2="162" stroke="#fab387" stroke-width="2.5" stroke-dasharray="6 5"/>
<text x="280" y="153" fill="#fab387" font-size="11" text-anchor="middle">wireless mesh</text>
<text x="280" y="205" fill="#f38ba8" font-size="13" text-anchor="middle">► layer-2 loop ◄ — frames circulate forever</text>
</svg>
<figcaption style="color:#6c7086;font-size:14px;margin-top:8px">Two wired speakers, one wireless bridge between them: a loop the switch can't see coming.</figcaption>
</figure>
<p>The reason it had run fine for months and then exploded? Doesn&rsquo;t matter much. Someone re-cabled a closet, both speakers ended up plugged in, the mesh did its job, and nothing on those access ports was watching for a loop.</p>
<h2 id="the-fix">The fix</h2>
<p>The speaker was the trigger. The real bug was a switch fabric with no loop protection. So we fixed both.</p>
<p>First, the immediate bleeding — pull one of the two speaker uplinks. One leg of copper gone, loop broken, network breathes.</p>
<p>Then, the actual fix. Turn on spanning tree, and <strong>pick the root on purpose</strong> instead of letting MAC addresses elect a random one:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># Core switch — make it the root, deliberately. Lowest priority wins.</span>
</span></span><span style="display:flex;"><span>spanning-tree mode rstp
</span></span><span style="display:flex;"><span>spanning-tree vlan 1-4094 priority <span style="color:#bd93f9">4096</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># Distribution — second in line</span>
</span></span><span style="display:flex;"><span>spanning-tree vlan 1-4094 priority <span style="color:#bd93f9">8192</span>
</span></span></code></pre></div><p>Then lock down the edge so an end-device port can never become a transit path:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># Access port to the speaker / any end device</span>
</span></span><span style="display:flex;"><span>interface GigabitEthernet0/14
</span></span><span style="display:flex;"><span> spanning-tree portfast
</span></span><span style="display:flex;"><span> spanning-tree bpduguard <span style="color:#8be9fd;font-style:italic">enable</span>     <span style="color:#6272a4"># any BPDU here = err-disable the port</span>
</span></span><span style="display:flex;"><span> spanning-tree guard root            <span style="color:#6272a4"># this port may never be a path to root</span>
</span></span><span style="display:flex;"><span> storm-control broadcast level 1.00  <span style="color:#6272a4"># cap broadcast at 1% of line rate</span>
</span></span><span style="display:flex;"><span> storm-control action shutdown
</span></span></code></pre></div><p>Mesh-style managed gear is the same idea in different clothes — flip on <strong>STP</strong>, set tiered priorities so the core is root, and enable <strong>Loop Protection</strong> plus <strong>BPDU Guard</strong> on access ports.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>edge port discipline
</span></span><span style="display:flex;"><span>─────────────────────────────────
</span></span><span style="display:flex;"><span>  PortFast      ✓  fast end-device link
</span></span><span style="display:flex;"><span>  BPDU Guard    ✓  sees a switch → kills port
</span></span><span style="display:flex;"><span>  Loop Guard    ✓  detects the loop
</span></span><span style="display:flex;"><span>  Storm-control ✓  broadcast ceiling
</span></span><span style="display:flex;"><span>─────────────────────────────────
</span></span><span style="display:flex;"><span>  result: ▓▓▓▓▓ blast radius = one port
</span></span></code></pre></div><p>We re-cabled the speakers to a single drop each, verified one stayed on copper and the rest synced over mesh, and watched the broadcast counter flatline.</p>
<h2 id="why-it-happened">Why it happened</h2>
<p>Consumer mesh gear is a loop machine wearing a friendly face. It will happily bridge two of its own units, and it has no idea it&rsquo;s also sitting on your switched LAN. Plug two of them into copper and you&rsquo;ve handed your network a second path it never agreed to.</p>
<p>The switch didn&rsquo;t fail. It had no spanning tree configured, so it had no mechanism to notice the loop and break it. The default config trusted every port to behave. Edge ports never behave.</p>
<h2 id="takeaways">Takeaways</h2>
<ul>
<li><strong>Always run STP/RSTP.</strong> A modern loop-free topology still needs spanning tree on standby for the day someone hands you a loop.</li>
<li><strong>Set the root bridge on purpose.</strong> Lowest priority value wins; don&rsquo;t let a random MAC address decide where your traffic converges.</li>
<li><strong>Guard the edge.</strong> BPDU Guard, Loop Protection, and storm-control on access ports turn a network-wide meltdown into one dead port.</li>
<li><strong>Treat consumer mesh gear as hostile to your switch fabric.</strong> Never wire two mesh-bridging devices into the same LAN.</li>
<li><strong>A flapping MAC is a loop until proven otherwise.</strong> The mac-address-table is your fastest path from &ldquo;it&rsquo;s slow&rdquo; to &ldquo;it&rsquo;s port 9 and 14.&rdquo;</li>
</ul>
]]></content:encoded></item><item><title>Making a Python Script a Real Service</title><link>https://errorzap.com/posts/making-a-python-script-a-real-service/</link><pubDate>Mon, 06 Apr 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/making-a-python-script-a-real-service/</guid><description>The script worked perfectly — right up until I closed the terminal and watched it die.</description><content:encoded><![CDATA[<figure style="text-align:center;margin:0 0 30px"><img src="hero.png" alt="Making a Python Script a Real Service" style="max-width:520px;width:100%;border-radius:14px"/></figure>
<p>The pager went off at 6:14 a.m. The morning sync hadn&rsquo;t run.</p>
<p>I knew that script. I <em>wrote</em> that script. It was good. It pulled data, massaged it, pushed it where it needed to go. It worked flawlessly every single time I ran it.</p>
<p>That was the problem. It only worked when <em>I</em> ran it.</p>
<h2 id="the-scene">The scene</h2>
<p>Here&rsquo;s the embarrassing truth about that automation: it lived inside an SSH session. I&rsquo;d connect to the box at <code>sync-host.internal.example</code>, fire it off, watch it succeed, and disconnect feeling like a wizard.</p>
<p>The moment my terminal closed, the kernel reaped it. SIGHUP, lights out. No process, no cron-shaped safety net, nothing.</p>
<p>On the Windows box it was worse. I&rsquo;d &ldquo;scheduled&rdquo; it — except every run popped a console window that stole focus from whoever was using the machine. People started killing the window because it interrupted them. Can&rsquo;t blame them.</p>
<p>So three different failure modes, one script:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>   ┌──────────────────────────────────────────────┐
</span></span><span style="display:flex;"><span>   │  &#34;It works when I run it manually&#34;            │
</span></span><span style="display:flex;"><span>   ├──────────────────────────────────────────────┤
</span></span><span style="display:flex;"><span>   │  close SSH session   ──►  SIGHUP, process dies│
</span></span><span style="display:flex;"><span>   │  server reboots      ──►  nothing comes back  │
</span></span><span style="display:flex;"><span>   │  scheduled on win    ──►  window steals focus │
</span></span><span style="display:flex;"><span>   └──────────────────────────────────────────────┘
</span></span><span style="display:flex;"><span>                    │
</span></span><span style="display:flex;"><span>                    ▼
</span></span><span style="display:flex;"><span>            NOT actually deployed
</span></span></code></pre></div><h2 id="the-investigation">The investigation</h2>
<p>I tailed the journal. Nothing — because nothing was logging anywhere. The script wrote to stdout, and stdout was attached to a terminal that no longer existed.</p>
<p>I checked the process list. Empty. I checked for a cron entry. None — I&rsquo;d never made one, because &ldquo;I&rsquo;ll just run it&rdquo; had quietly become the deployment strategy.</p>
<p>That&rsquo;s the aha, and it&rsquo;s a dumb one: <strong>manual execution was masquerading as deployment.</strong> The script was perfect and completely undeployed at the same time. Every successful manual run was me hand-holding a service that had no manager.</p>
<p>A long-running, scheduled, headless job has three needs I&rsquo;d ignored: survive a logout, survive a reboot, and write its logs somewhere a human can read them later. A terminal gives you none of those.</p>
<h2 id="the-fix">The fix</h2>
<p>Give it a service manager. On Linux, a systemd unit. On Windows, NSSM. Both get auto-restart, log redirection, and start-on-boot — and both run headless.</p>
<p>Linux — <code>/etc/systemd/system/morning-sync.service</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#ff79c6">[Unit]</span>
</span></span><span style="display:flex;"><span><span style="color:#50fa7b">Description</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">Morning data sync</span>
</span></span><span style="display:flex;"><span><span style="color:#50fa7b">After</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">network-online.target</span>
</span></span><span style="display:flex;"><span><span style="color:#50fa7b">Wants</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">network-online.target</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">[Service]</span>
</span></span><span style="display:flex;"><span><span style="color:#50fa7b">Type</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">simple</span>
</span></span><span style="display:flex;"><span><span style="color:#50fa7b">User</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">svc-sync</span>
</span></span><span style="display:flex;"><span><span style="color:#50fa7b">WorkingDirectory</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">/opt/morning-sync</span>
</span></span><span style="display:flex;"><span><span style="color:#50fa7b">ExecStart</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">/opt/morning-sync/.venv/bin/python /opt/morning-sync/sync.py</span>
</span></span><span style="display:flex;"><span><span style="color:#50fa7b">Restart</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">on-failure</span>
</span></span><span style="display:flex;"><span><span style="color:#50fa7b">RestartSec</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">10</span>
</span></span><span style="display:flex;"><span><span style="color:#50fa7b">StandardOutput</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">append:/var/log/morning-sync/out.log</span>
</span></span><span style="display:flex;"><span><span style="color:#50fa7b">StandardError</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">append:/var/log/morning-sync/err.log</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">[Install]</span>
</span></span><span style="display:flex;"><span><span style="color:#50fa7b">WantedBy</span><span style="color:#ff79c6">=</span><span style="color:#f1fa8c">multi-user.target</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo mkdir -p /var/log/morning-sync
</span></span><span style="display:flex;"><span>sudo systemctl daemon-reload
</span></span><span style="display:flex;"><span>sudo systemctl <span style="color:#8be9fd;font-style:italic">enable</span> --now morning-sync.service
</span></span><span style="display:flex;"><span>systemctl status morning-sync.service
</span></span><span style="display:flex;"><span>journalctl -u morning-sync.service -f
</span></span></code></pre></div><p>Windows — same job, NSSM does the babysitting and runs it with no visible window:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bat" data-lang="bat"><span style="display:flex;"><span>nssm install MorningSync <span style="color:#f1fa8c">&#34;C:\apps\morning-sync\.venv\Scripts\python.exe&#34;</span> <span style="color:#f1fa8c">&#34;C:\apps\morning-sync\sync.py&#34;</span>
</span></span><span style="display:flex;"><span>nssm set MorningSync AppDirectory <span style="color:#f1fa8c">&#34;C:\apps\morning-sync&#34;</span>
</span></span><span style="display:flex;"><span>nssm set MorningSync AppStdout <span style="color:#f1fa8c">&#34;C:\logs\morning-sync\out.log&#34;</span>
</span></span><span style="display:flex;"><span>nssm set MorningSync AppStderr <span style="color:#f1fa8c">&#34;C:\logs\morning-sync\err.log&#34;</span>
</span></span><span style="display:flex;"><span>nssm set MorningSync AppExit Default Restart
</span></span><span style="display:flex;"><span>nssm set MorningSync Start SERVICE_AUTO_START
</span></span><span style="display:flex;"><span>nssm start MorningSync
</span></span></code></pre></div><p>Now I close my laptop and it keeps running. The box reboots at 3 a.m. for patches and the sync comes back on its own. The logs sit in a file instead of evaporating with my SSH session.</p>
<figure style="text-align:center;margin:34px 0">
<svg viewBox="0 0 560 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Service manager supervising a Python script">
<rect x="0" y="0" width="560" height="220" rx="10" fill="#11111b"/>
<rect x="40" y="70" width="150" height="80" rx="10" fill="#1e1e2e" stroke="#45475a"/>
<text x="115" y="105" text-anchor="middle" fill="#cba6f7" font-family="monospace" font-size="14">service</text>
<text x="115" y="125" text-anchor="middle" fill="#6c7086" font-family="monospace" font-size="11">systemd / NSSM</text>
<rect x="370" y="70" width="150" height="80" rx="10" fill="#1e1e2e" stroke="#45475a"/>
<text x="445" y="105" text-anchor="middle" fill="#89b4fa" font-family="monospace" font-size="14">sync.py</text>
<text x="445" y="125" text-anchor="middle" fill="#6c7086" font-family="monospace" font-size="11">headless</text>
<line x1="190" y1="110" x2="370" y2="110" stroke="#a6e3a1" stroke-width="2"/>
<polygon points="370,110 358,104 358,116" fill="#a6e3a1"/>
<text x="280" y="100" text-anchor="middle" fill="#a6e3a1" font-family="monospace" font-size="11">start / supervise</text>
<path d="M 370 130 C 300 175, 260 175, 190 130" stroke="#fab387" stroke-width="2" fill="none"/>
<polygon points="190,130 202,128 196,139" fill="#fab387"/>
<text x="280" y="172" text-anchor="middle" fill="#fab387" font-family="monospace" font-size="11">Restart=on-failure</text>
<text x="280" y="40" text-anchor="middle" fill="#cdd6f4" font-family="monospace" font-size="13">boot ──► auto-start ──► logs to file</text>
<rect x="210" y="185" width="140" height="22" rx="6" fill="#313244" stroke="#45475a"/>
<text x="280" y="200" text-anchor="middle" fill="#94e2d5" font-family="monospace" font-size="11">survives logout + reboot</text>
</svg>
<figcaption style="color:#6c7086;font-size:14px;margin-top:8px">A service manager turns "I ran it once" into "it runs forever."</figcaption>
</figure>
<h2 id="why-it-happened">Why it happened</h2>
<p>Because the script worked. That&rsquo;s it. When something works on the first manual run, the brain files it under &ldquo;done&rdquo; and quietly skips the boring part — the part where a supervisor owns the process, restarts it when it dies, and persists across boots.</p>
<p>A terminal is not a runtime. Cron-with-a-popup is not headless. &ldquo;Done&rdquo; is not &ldquo;deployed.&rdquo;</p>
<h2 id="takeaways">Takeaways</h2>
<ul>
<li><strong>&ldquo;It works when I run it manually&rdquo; is not deployed.</strong> A successful manual run proves the logic, not the operation.</li>
<li><strong>Long-running automation belongs in a service manager</strong> — systemd on Linux, NSSM on Windows — not a terminal you forgot to close.</li>
<li><strong>Always set auto-restart and boot persistence.</strong> <code>Restart=on-failure</code> plus <code>enable --now</code> (or <code>SERVICE_AUTO_START</code>) means reboots and crashes self-heal.</li>
<li><strong>Redirect stdout/stderr to a file.</strong> Logs that live inside an SSH session die with the session, and you&rsquo;ll be debugging blind at 6 a.m.</li>
<li><strong>Run it headless.</strong> Background jobs that pop windows get killed by annoyed users. No UI, no focus theft, no problem.</li>
</ul>
]]></content:encoded></item><item><title>90s Kid - Encarta and the Encyclopedia on a Disc</title><link>https://errorzap.com/posts/90s-kid-encarta/</link><pubDate>Sun, 05 Apr 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/90s-kid-encarta/</guid><description>You remember when all the knowledge in the world fit on one shiny disc, and the loading screen had its own theme music.</description><content:encoded><![CDATA[<figure style="margin:0 0 28px">
<svg viewBox="0 0 800 240" xmlns="http://www.w3.org/2000/svg" style="width:100%;border-radius:14px">
 <defs>
  <linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#1e1e2e"/><stop offset="1" stop-color="#11111b"/></linearGradient>
  <linearGradient id="sun" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f9e2af"/><stop offset="0.5" stop-color="#fab387"/><stop offset="1" stop-color="#f38ba8"/></linearGradient>
 </defs>
 <rect width="800" height="240" fill="url(#sky)"/>
 <circle cx="640" cy="150" r="74" fill="url(#sun)"/><rect x="566" y="118" width="148" height="4" fill="#11111b"/><rect x="566" y="127" width="148" height="4" fill="#11111b"/><rect x="566" y="136" width="148" height="4" fill="#11111b"/><rect x="566" y="145" width="148" height="4" fill="#11111b"/><rect x="566" y="154" width="148" height="4" fill="#11111b"/><rect x="566" y="163" width="148" height="4" fill="#11111b"/><rect x="566" y="172" width="148" height="4" fill="#11111b"/>
 <line x1="-200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="280" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="360" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="440" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="520" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="600" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="680" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="760" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="840" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="920" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="1000" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="0" y1="178" x2="800" y2="178" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="184" x2="800" y2="184" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="193" x2="800" y2="193" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="206" x2="800" y2="206" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="227" x2="800" y2="227" stroke="#94e2d5" stroke-width="1" opacity="0.22"/>
 <text x="48" y="150" font-size="78">💿</text>
 <text x="150" y="96" font-family="ui-monospace,monospace" font-size="14" fill="#cba6f7" letter-spacing="5">90s KID //</text>
 <text x="150" y="140" font-family="-apple-system,Segoe UI,sans-serif" font-size="30" font-weight="800" fill="#cdd6f4">Encarta and the Encyclopedia on a </text>
 <rect x="0" y="236" width="800" height="4" fill="#f38ba8"/>
</svg></figure>
<h2 id="all-the-worlds-knowledge-and-a-loading-spinner">All the World&rsquo;s Knowledge, and a Loading Spinner</h2>
<p>You remember <strong>Encarta.</strong></p>
<p>That one disc.<br>
Silver. Heavier than it should&rsquo;ve been.<br>
Lived in a jewel case next to the family computer like it was a relic.</p>
<p>You remember the moment it loaded — the swirl, the little <strong>fanfare</strong>, the screen going dark for a second like the machine was <em>bracing itself.</em></p>
<p>And then: everything.<br>
Animals. Volcanoes. Mozart. The solar system.<br>
All of it, <strong>right there</strong>, glowing.</p>
<hr>
<p>You were eight.<br>
You did not understand what you were holding.<br>
You just knew the vibes:</p>
<ul>
<li>It was <strong>the smart disc</strong> — the one Dad said cost real money</li>
<li>It had a <strong>video of a frog</strong>, and you watched that frog forty times</li>
<li>There was a <strong>globe you could spin</strong> and it felt like piloting a spaceship</li>
<li>The narrator voice was so calm it was basically a wizard</li>
<li>Typing your report? No. You <strong>copied it word for word</strong> and called it research</li>
</ul>
<p>It felt like the entire library got crushed down into a coaster.<br>
Forbidden, almost. Too much power for one kid and one beige tower.</p>
<hr>
<p>Here&rsquo;s what it actually was.</p>
<p>That disc was a <strong>CD-ROM</strong> — about <strong>650 megabytes</strong> of read-only storage, pressed once at a factory and never changed again. (Your phone&rsquo;s wallpaper is bigger than that now.)</p>
<p>To fit a whole encyclopedia on it, they pulled off some real magic:<br>
the text was <strong>compressed</strong> down tiny, the photos were squeezed into early JPEGs, and the audio and video were chopped into low-res clips just big enough to feel <em>amazing</em> on a 14-inch CRT.</p>
<p>The frog video? Maybe <strong>two seconds</strong> of footage at a resolution you&rsquo;d laugh at today. But it was <em>moving pictures, on demand, from a disc.</em> In <strong>1995</strong> that was witchcraft.</p>
<p>The whole thing ran off your CD drive at a blistering <strong>1.2 megabytes a second</strong> — which is why the globe took a moment to think before it spun.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>        _.--&#34;&#34;&#34;&#34;--._
</span></span><span style="display:flex;"><span>      .&#39;  _    _   &#39;.
</span></span><span style="display:flex;"><span>     /   (_)  (_)    \      ENCARTA &#39;95
</span></span><span style="display:flex;"><span>    |    .-.  .-.     |    .-------------.
</span></span><span style="display:flex;"><span>    |   (   )(   )    |    | [o] CD-ROM  |
</span></span><span style="display:flex;"><span>     \   &#39;-&#39;  &#39;-&#39;    /     &#39;-------------&#39;
</span></span><span style="display:flex;"><span>      &#39;._        _.&#39;        all of human
</span></span><span style="display:flex;"><span>         &#39;------&#39;           knowledge™
</span></span><span style="display:flex;"><span>                            (insert disc 1)
</span></span></code></pre></div><p>The wild part?<br>
It never really left.</p>
<p>You just stopped noticing.</p>
<p>The frog video became <strong>YouTube.</strong><br>
The spinning globe became <strong>Maps.</strong><br>
The calm narrator wizard became <strong>the little box you type questions into at 2 a.m.</strong></p>
<p>The whole pressed-on-a-disc encyclopedia got vaporized into something that lives nowhere and everywhere at once, updates itself while you sleep, and never makes you swap to <strong>disc 2.</strong></p>
<p>You don&rsquo;t hold all the world&rsquo;s knowledge in a jewel case anymore.</p>
<p>You hold it in your pocket.<br>
And it still takes a second to load.</p>
]]></content:encoded></item><item><title>90s Kid - Be Kind, Rewind</title><link>https://errorzap.com/posts/90s-kid-be-kind-rewind/</link><pubDate>Tue, 31 Mar 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/90s-kid-be-kind-rewind/</guid><description>You remember the little sticker that begged you to spin the tape backward before you brought it home.</description><content:encoded><![CDATA[<figure style="margin:0 0 28px">
<svg viewBox="0 0 800 240" xmlns="http://www.w3.org/2000/svg" style="width:100%;border-radius:14px">
 <defs>
  <linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#1e1e2e"/><stop offset="1" stop-color="#11111b"/></linearGradient>
  <linearGradient id="sun" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f9e2af"/><stop offset="0.5" stop-color="#fab387"/><stop offset="1" stop-color="#f38ba8"/></linearGradient>
 </defs>
 <rect width="800" height="240" fill="url(#sky)"/>
 <circle cx="640" cy="150" r="74" fill="url(#sun)"/><rect x="566" y="118" width="148" height="4" fill="#11111b"/><rect x="566" y="127" width="148" height="4" fill="#11111b"/><rect x="566" y="136" width="148" height="4" fill="#11111b"/><rect x="566" y="145" width="148" height="4" fill="#11111b"/><rect x="566" y="154" width="148" height="4" fill="#11111b"/><rect x="566" y="163" width="148" height="4" fill="#11111b"/><rect x="566" y="172" width="148" height="4" fill="#11111b"/>
 <line x1="-200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="280" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="360" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="440" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="520" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="600" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="680" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="760" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="840" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="920" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="1000" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="0" y1="178" x2="800" y2="178" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="184" x2="800" y2="184" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="193" x2="800" y2="193" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="206" x2="800" y2="206" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="227" x2="800" y2="227" stroke="#94e2d5" stroke-width="1" opacity="0.22"/>
 <text x="48" y="150" font-size="78">📼</text>
 <text x="150" y="96" font-family="ui-monospace,monospace" font-size="14" fill="#cba6f7" letter-spacing="5">90s KID //</text>
 <text x="150" y="140" font-family="-apple-system,Segoe UI,sans-serif" font-size="30" font-weight="800" fill="#cdd6f4">Be Kind, Rewind</text>
 <rect x="0" y="236" width="800" height="4" fill="#f38ba8"/>
</svg></figure>
<h2 id="the-sacred-sticker-that-ran-your-whole-weekend">The Sacred Sticker That Ran Your Whole Weekend</h2>
<p>You remember the tape.</p>
<p>That fat black brick with a label half peeled off.<br>
The Blockbuster case that clicked shut like it meant business.<br>
And the sticker — always the sticker —</p>
<p><strong>BE KIND, REWIND.</strong></p>
<p>You didn&rsquo;t know who you were supposed to be kind <em>to</em>.<br>
A stranger? The future? The tape itself?</p>
<p>You just knew it felt like a rule from on high.</p>
<hr>
<p>The whole ritual was holy and a little terrifying.</p>
<p>The vibes:</p>
<ul>
<li>The <strong>late-fee dread</strong> that lived in your stomach all weekend</li>
<li>Sliding the tape in and hearing that hungry mechanical <em>gulp</em></li>
<li>The screen going blue, then snowy, then&hellip; <strong>somebody else&rsquo;s movie still playing</strong></li>
<li>That deep machine <em>whirrrrr</em> of rewinding, the counter ticking backward</li>
<li>The guilt if you returned it un-rewound, like you&rsquo;d left a mess at a friend&rsquo;s house</li>
</ul>
<p>You thought the movie <em>lived inside the box</em>.<br>
Like a tiny theater you were renting for one night only.<br>
You were not allowed to be late. You were not allowed to be unkind.</p>
<hr>
<p>Here&rsquo;s what was actually going on.</p>
<p>That tape was a long ribbon of <strong>magnetic film</strong>, wound between two reels inside the shell.</p>
<p>The picture and sound weren&rsquo;t a file — they were <strong>stripes of magnetism</strong> painted onto that ribbon, read by a spinning drum of tiny heads as the tape dragged past.</p>
<p>And rewinding mattered for a real reason.<br>
The tape <strong>played from one reel to the other</strong>, so when the movie ended, all the film was bunched on the <em>wrong</em> side.</p>
<p>The next person had to wait for it to spin all the way back before they could even press play.</p>
<p>So &ldquo;be kind, rewind&rdquo; wasn&rsquo;t sentiment.<br>
It was <strong>etiquette for shared, sequential hardware</strong>.<br>
You weren&rsquo;t streaming a movie. You were borrowing a physical line and you owed the next kid a fresh start.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>        BE KIND, REWIND
</span></span><span style="display:flex;"><span>   ___________________________
</span></span><span style="display:flex;"><span>  |  [ o ]            [ o ]   |
</span></span><span style="display:flex;"><span>  |   ===              ===    |
</span></span><span style="display:flex;"><span>  |  /   \   VHS      /   \   |
</span></span><span style="display:flex;"><span>  | (     )--========(     )  |
</span></span><span style="display:flex;"><span>  |  \___/  |||||||||  \___/  |
</span></span><span style="display:flex;"><span>  |_____________________  ____|
</span></span><span style="display:flex;"><span>  |#####################|    |
</span></span><span style="display:flex;"><span>  |#####################|____|
</span></span><span style="display:flex;"><span>  |_________________________ |
</span></span></code></pre></div><hr>
<p>The tapes are gone.<br>
The little machine that rewound them for you — gone.</p>
<p>But the etiquette never died.<br>
It just <strong>went invisible</strong>.</p>
<p>Now the platform rewinds for you. Silently. Instantly.<br>
You scrub the timeline and the whole movie is already <em>there</em>, every second equally close, no wrong side to bunch up on.</p>
<p>No counter. No whir. No guilt.</p>
<p>And honestly? Something&rsquo;s a little lost.</p>
<p>Because you&rsquo;ll never again hand someone a thing and quietly hope —</p>
<p><strong>you left it better than you found it.</strong></p>
]]></content:encoded></item><item><title>90s Kid - The Walkman and the Pencil</title><link>https://errorzap.com/posts/90s-kid-the-walkman/</link><pubDate>Thu, 26 Mar 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/90s-kid-the-walkman/</guid><description>You remember the sacred panic of a tape unspooling, and the pencil that always knew what to do.</description><content:encoded><![CDATA[<figure style="margin:0 0 28px">
<svg viewBox="0 0 800 240" xmlns="http://www.w3.org/2000/svg" style="width:100%;border-radius:14px">
 <defs>
  <linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#1e1e2e"/><stop offset="1" stop-color="#11111b"/></linearGradient>
  <linearGradient id="sun" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f9e2af"/><stop offset="0.5" stop-color="#fab387"/><stop offset="1" stop-color="#f38ba8"/></linearGradient>
 </defs>
 <rect width="800" height="240" fill="url(#sky)"/>
 <circle cx="640" cy="150" r="74" fill="url(#sun)"/><rect x="566" y="118" width="148" height="4" fill="#11111b"/><rect x="566" y="127" width="148" height="4" fill="#11111b"/><rect x="566" y="136" width="148" height="4" fill="#11111b"/><rect x="566" y="145" width="148" height="4" fill="#11111b"/><rect x="566" y="154" width="148" height="4" fill="#11111b"/><rect x="566" y="163" width="148" height="4" fill="#11111b"/><rect x="566" y="172" width="148" height="4" fill="#11111b"/>
 <line x1="-200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="280" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="360" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="440" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="520" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="600" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="680" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="760" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="840" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="920" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="1000" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="0" y1="178" x2="800" y2="178" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="184" x2="800" y2="184" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="193" x2="800" y2="193" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="206" x2="800" y2="206" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="227" x2="800" y2="227" stroke="#94e2d5" stroke-width="1" opacity="0.22"/>
 <text x="48" y="150" font-size="78">🎧</text>
 <text x="150" y="96" font-family="ui-monospace,monospace" font-size="14" fill="#cba6f7" letter-spacing="5">90s KID //</text>
 <text x="150" y="140" font-family="-apple-system,Segoe UI,sans-serif" font-size="30" font-weight="800" fill="#cdd6f4">The Walkman and the Pencil</text>
 <rect x="0" y="236" width="800" height="4" fill="#f38ba8"/>
</svg></figure>
<h2 id="the-yellow-brick-that-played-your-whole-heart">The Yellow Brick That Played Your Whole Heart</h2>
<p>You remember the Walkman.</p>
<p>That chunky little slab of plastic clipped to your waistband like a deputy&rsquo;s badge.<br>
Orange foam headphones that pinched your ears just enough to feel official.<br>
And the <em>clunk</em> — that satisfying mechanical <strong>clunk</strong> when the cassette door snapped shut.</p>
<p>You hit play.<br>
And for the length of one side, the world belonged to you.</p>
<p>It felt important. A little forbidden. The first thing that was <em>yours</em> and not the family&rsquo;s.</p>
<p>The vibes were immaculate:</p>
<ul>
<li><strong>Hitting record</strong> off the radio and screaming at the DJ for talking over the intro</li>
<li>The terror of <strong>low batteries</strong> — when the song slowed down and dropped an octave, like the music was <em>dying</em></li>
<li>Rewinding to your favorite part and <strong>counting in your head</strong> because there was no rewind-to-track, just raw guesswork</li>
<li>That ONE tape so loved it sounded like it was recorded underwater</li>
<li>And the <strong>pencil</strong>. Always the pencil.</li>
</ul>
<p>Because here&rsquo;s the thing every 90s kid knew by instinct.<br>
When the tape went slack and spat out a loop of shiny brown ribbon — <strong>you grabbed a pencil</strong>, jammed it in the little spoked hole, and <em>wound it back in.</em></p>
<p>You didn&rsquo;t know why it worked.<br>
You just knew it was magic, and you were the wizard.</p>
<p><strong>Here&rsquo;s what was actually happening.</strong></p>
<p>That brown ribbon was <strong>mylar tape coated in iron oxide</strong> — basically rust, lovingly arranged. The play head was a tiny electromagnet &ldquo;reading&rdquo; the magnetic pattern stamped into that rust as it dragged past at exactly <strong>1⅞ inches per second.</strong></p>
<p>Two reels. One feeding, one taking up. When the take-up reel stuck, the player kept pulling tape out — and you got The Spaghetti Incident.</p>
<p>The pencil? Its six-sided hole was a <strong>dead-perfect match</strong> for the cassette&rsquo;s reel hub. You weren&rsquo;t doing magic. You were doing <strong>manual tape transport</strong> — the exact job the motor did, just powered by a bored 9-year-old&rsquo;s wrist.</p>
<p>And the dying-batteries voice drop? Real physics. Weak power meant the motor spun <strong>slower</strong>, so the tape crawled past the head and every pitch sagged. You were literally hearing your AA batteries run out of breath.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>   ___________________________
</span></span><span style="display:flex;"><span>  |  .---.             .---.  |
</span></span><span style="display:flex;"><span>  | (  o  )           (  o  ) |
</span></span><span style="display:flex;"><span>  |  &#39;---&#39;   [ A ]     &#39;---&#39;  |
</span></span><span style="display:flex;"><span>  |    \_______________/      |
</span></span><span style="display:flex;"><span>  |   |::|::|::|::|::|::|      |
</span></span><span style="display:flex;"><span>  |___|_TYPE_I_·_60_min_|_____|
</span></span><span style="display:flex;"><span>       \___ pencil goes ___/
</span></span><span style="display:flex;"><span>            here →  O
</span></span></code></pre></div><p>You can&rsquo;t kill a format that taught you patience.</p>
<p>The Walkman never really died — it just lost the moving parts. The iPod was a Walkman that finally stopped eating your tapes. Your phone is a Walkman with the whole record store crammed inside it.</p>
<p>But somewhere a teenager just paid forty bucks for a <strong>brand-new cassette</strong> because the algorithm told them it was vintage.</p>
<p>And somewhere, an old pencil is <strong>so proud.</strong></p>
]]></content:encoded></item><item><title>90s Kid - The VCR That Blinked 12:00 Forever</title><link>https://errorzap.com/posts/90s-kid-the-blinking-vcr/</link><pubDate>Sat, 21 Mar 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/90s-kid-the-blinking-vcr/</guid><description>You remember the glowing green 12:00 that nobody in the house ever fixed.</description><content:encoded><![CDATA[<figure style="margin:0 0 28px">
<svg viewBox="0 0 800 240" xmlns="http://www.w3.org/2000/svg" style="width:100%;border-radius:14px">
 <defs>
  <linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#1e1e2e"/><stop offset="1" stop-color="#11111b"/></linearGradient>
  <linearGradient id="sun" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f9e2af"/><stop offset="0.5" stop-color="#fab387"/><stop offset="1" stop-color="#f38ba8"/></linearGradient>
 </defs>
 <rect width="800" height="240" fill="url(#sky)"/>
 <circle cx="640" cy="150" r="74" fill="url(#sun)"/><rect x="566" y="118" width="148" height="4" fill="#11111b"/><rect x="566" y="127" width="148" height="4" fill="#11111b"/><rect x="566" y="136" width="148" height="4" fill="#11111b"/><rect x="566" y="145" width="148" height="4" fill="#11111b"/><rect x="566" y="154" width="148" height="4" fill="#11111b"/><rect x="566" y="163" width="148" height="4" fill="#11111b"/><rect x="566" y="172" width="148" height="4" fill="#11111b"/>
 <line x1="-200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="280" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="360" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="440" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="520" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="600" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="680" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="760" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="840" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="920" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="1000" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="0" y1="178" x2="800" y2="178" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="184" x2="800" y2="184" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="193" x2="800" y2="193" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="206" x2="800" y2="206" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="227" x2="800" y2="227" stroke="#94e2d5" stroke-width="1" opacity="0.22"/>
 <text x="48" y="150" font-size="78">📼</text>
 <text x="150" y="96" font-family="ui-monospace,monospace" font-size="14" fill="#cba6f7" letter-spacing="5">90s KID //</text>
 <text x="150" y="140" font-family="-apple-system,Segoe UI,sans-serif" font-size="30" font-weight="800" fill="#cdd6f4">The VCR That Blinked 12:00 Forever</text>
 <rect x="0" y="236" width="800" height="4" fill="#f38ba8"/>
</svg></figure>
<h2 id="a-clock-that-never-learned-what-time-it-was">A Clock That Never Learned What Time It Was</h2>
<p>You remember the VCR.</p>
<p>That big black slab under the TV.<br>
Heavier than it had any right to be.<br>
A mouth that ate tapes and a single, stubborn green <strong>12:00</strong> blinking out into the dark living room.</p>
<p>12:00.<br>
12:00.<br>
12:00.</p>
<p>Forever.</p>
<p>Nobody set it. Nobody could. It just blinked, all night, like a tiny lighthouse warning ships away from the entertainment center.</p>
<p>And you, small and certain, knew exactly what it was about:</p>
<ul>
<li><strong>vibes:</strong></li>
<li>it was the <strong>brain</strong> of the TV, obviously</li>
<li>the blinking meant it was <em>thinking</em></li>
<li>pushing EJECT felt like defusing a bomb</li>
<li>the rented tape <strong>had</strong> to be rewound or something bad happened (you weren&rsquo;t told what)</li>
<li>the little tracking lines were a <strong>storm</strong> you could fix by waving your hand at it</li>
<li>that flap was a mouth and you were NOT putting your fingers near it</li>
</ul>
<p>It was important. It was a little forbidden. It was a little magic.</p>
<p>Here&rsquo;s the reveal.</p>
<p>That blinking 12:00 wasn&rsquo;t broken. It was just <strong>honest</strong>. The VCR had a tiny digital clock inside, and it had no idea what time it was because nobody ever told it. Every power flicker — every summer storm, every blown fuse, every time Dad vacuumed and hit the wrong outlet — wiped its memory clean and it woke up at the only number it knew: midnight. Square one. Again.</p>
<p>Inside that heavy box was genuinely wild stuff. Magnetic tape, a half-inch wide, dragged past a <strong>spinning head drum</strong> tilted at a slight angle so it could cram diagonal stripes of video onto the tape — <em>helical scan</em>, they called it. That whir you heard when you hit play was the drum spinning up to speed, threading the tape around itself like a tiny robot doing origami in the dark.</p>
<p>And the rewinding? Real. Tape is a <strong>line</strong>, not a grid. To see the start, you physically wound it back to the start. There was no &ldquo;skip to beginning.&rdquo; There was only patience and that <em>zzzzzzWP</em> of the tape snapping home.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>        ____________________________
</span></span><span style="display:flex;"><span>       |  [] SONY      VHS HQ        |
</span></span><span style="display:flex;"><span>       |                            |
</span></span><span style="display:flex;"><span>       |   .------------------.     |
</span></span><span style="display:flex;"><span>       |   |  __          __  |     |
</span></span><span style="display:flex;"><span>       |   | (__)  ::::   (__)|     |  &lt;- the tape
</span></span><span style="display:flex;"><span>       |   |______________ ___|     |
</span></span><span style="display:flex;"><span>       |   &#39;------------------&#39;     |
</span></span><span style="display:flex;"><span>       |                            |
</span></span><span style="display:flex;"><span>       |    [&lt;&lt;] [|&gt;] [&gt;&gt;] [#]       |
</span></span><span style="display:flex;"><span>       |        __                  |
</span></span><span style="display:flex;"><span>       |       |12:00|  *blink*     |
</span></span><span style="display:flex;"><span>       |       |_:_:_|              |
</span></span><span style="display:flex;"><span>       |____________________________|
</span></span></code></pre></div><p>It never really went away.</p>
<p>That blinking clock just moved. Now it&rsquo;s the microwave you never set after daylight saving. The oven that flashes after a brownout. The router light that means <em>something</em>, you&rsquo;re pretty sure. The little devices in your life still wake up confused, still ask you what time it is, still get ignored.</p>
<p>And rewinding? You do it every time you drag the scrubber bar back thirty seconds because you missed a line of dialogue.</p>
<p>Be kind. Rewind.</p>
<p>You still do. You just don&rsquo;t have to get up for it anymore.</p>
]]></content:encoded></item><item><title>90s Kid - The Answering Machine</title><link>https://errorzap.com/posts/90s-kid-the-answering-machine/</link><pubDate>Mon, 16 Mar 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/90s-kid-the-answering-machine/</guid><description>You remember the little blinking box that held strangers&amp;rsquo; voices hostage on a cassette the size of a postage stamp.</description><content:encoded><![CDATA[<figure style="margin:0 0 28px">
<svg viewBox="0 0 800 240" xmlns="http://www.w3.org/2000/svg" style="width:100%;border-radius:14px">
 <defs>
  <linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#1e1e2e"/><stop offset="1" stop-color="#11111b"/></linearGradient>
  <linearGradient id="sun" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f9e2af"/><stop offset="0.5" stop-color="#fab387"/><stop offset="1" stop-color="#f38ba8"/></linearGradient>
 </defs>
 <rect width="800" height="240" fill="url(#sky)"/>
 <circle cx="640" cy="150" r="74" fill="url(#sun)"/><rect x="566" y="118" width="148" height="4" fill="#11111b"/><rect x="566" y="127" width="148" height="4" fill="#11111b"/><rect x="566" y="136" width="148" height="4" fill="#11111b"/><rect x="566" y="145" width="148" height="4" fill="#11111b"/><rect x="566" y="154" width="148" height="4" fill="#11111b"/><rect x="566" y="163" width="148" height="4" fill="#11111b"/><rect x="566" y="172" width="148" height="4" fill="#11111b"/>
 <line x1="-200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="280" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="360" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="440" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="520" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="600" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="680" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="760" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="840" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="920" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="1000" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="0" y1="178" x2="800" y2="178" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="184" x2="800" y2="184" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="193" x2="800" y2="193" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="206" x2="800" y2="206" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="227" x2="800" y2="227" stroke="#94e2d5" stroke-width="1" opacity="0.22"/>
 <text x="48" y="150" font-size="78">📼</text>
 <text x="150" y="96" font-family="ui-monospace,monospace" font-size="14" fill="#cba6f7" letter-spacing="5">90s KID //</text>
 <text x="150" y="140" font-family="-apple-system,Segoe UI,sans-serif" font-size="30" font-weight="800" fill="#cdd6f4">The Answering Machine</text>
 <rect x="0" y="236" width="800" height="4" fill="#f38ba8"/>
</svg></figure>
<h2 id="the-box-that-kept-our-voices">The Box That Kept Our Voices</h2>
<p>You remember the answering machine.</p>
<p>It lived on the little phone table in the hallway.<br>
Beige. Always beige.<br>
A red light that <strong>blinked</strong> when something was waiting for you.</p>
<p>One blink. Two blinks. <em>Three blinks?!</em><br>
Three blinks meant something happened today.</p>
<p>And you were NOT allowed to touch it.</p>
<hr>
<p>You&rsquo;d come home from school and that red light would be going,<br>
and the whole house would change.</p>
<p>Mom hits PLAY.<br>
A beep.<br>
Then a voice — disembodied, a little too loud, a little too slow —<br>
floating out of a box into the hallway like a tiny ghost.</p>
<p>The vibes:</p>
<ul>
<li><strong>the outgoing message</strong> rehearsed nineteen times before anyone got it right</li>
<li>the cousin who didn&rsquo;t know how it worked and just breathed for forty seconds</li>
<li>the telemarketer&rsquo;s awkward pause, then <em>click</em></li>
<li>pressing your ear to it because you weren&rsquo;t allowed to turn it up</li>
<li>the cold terror of accidentally <strong>erasing all messages</strong></li>
</ul>
<p>It felt like a vault.<br>
A little machine guarding voices that belonged to grown-ups.</p>
<hr>
<p>Here&rsquo;s the reveal.</p>
<p>Inside that beige box?</p>
<p>Tape.</p>
<p>Sometimes <strong>two</strong> tiny cassettes — one looping your greeting on a couple feet of magnetic film, one recording whatever came in. Later models cheated and used a microchip with a few minutes of grainy memory, but the classic ones were pure analog.</p>
<p>The &ldquo;magic&rdquo; was just <strong>physics and a phone line</strong>.</p>
<p>The machine counted rings. After four, it picked up — closed the circuit so the caller stopped hearing <em>ring</em>, played your greeting off the loop tape, then dropped a record head onto the second tape and let it roll. The <em>beep</em> wasn&rsquo;t decoration. It was the cue: the tape is moving NOW, say your thing.</p>
<p>The blinking light? A counter ticking up each time the record head fired.</p>
<p>No cloud. No server.<br>
Voices physically scratched into a ribbon of rust, sitting on a hallway table.</p>
<hr>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>        _________________________
</span></span><span style="display:flex;"><span>       |   ___________________   |
</span></span><span style="display:flex;"><span>       |  |  ANSWERING MACHINE | |
</span></span><span style="display:flex;"><span>       |  |   ___    ___   ___ | |
</span></span><span style="display:flex;"><span>       |  |  (o o)  | 3 |  msg | |   &lt;- blinking
</span></span><span style="display:flex;"><span>       |  |___________________| |
</span></span><span style="display:flex;"><span>       |   [&lt;&lt;] [&gt;] [&gt;&gt;]  [REC]  |
</span></span><span style="display:flex;"><span>       |    ___________________  |
</span></span><span style="display:flex;"><span>       |   ||| | |||| | | || |   |   &lt;- tape window
</span></span><span style="display:flex;"><span>       |   |___________________| |
</span></span><span style="display:flex;"><span>       |_________________________|
</span></span></code></pre></div><hr>
<p>The funny part?</p>
<p>It never left.</p>
<p>We just stopped owning the beige box.<br>
Now the tape lives on a server farm somewhere, the greeting is robot-voiced, and the blinking red light became a <strong>little number bubble</strong> on your phone screen you swipe away without listening.</p>
<p>Same loop. Same dread of a full mailbox.</p>
<p>Except now nobody calls — they text <em>&ldquo;call me?&rdquo;</em> — and the only voicemails left are the dentist and a scam about your car&rsquo;s extended warranty.</p>
<p>That box held the voices of everyone you loved.</p>
<p>Four rings, then a beep.</p>
<p>You still flinch a little when something blinks red.</p>
]]></content:encoded></item><item><title>The Firewall API Call That Factory-Resets Your Router</title><link>https://errorzap.com/posts/the-firewall-api-call-that-wipes-your-router/</link><pubDate>Thu, 12 Mar 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/the-firewall-api-call-that-wipes-your-router/</guid><description>I ran a &amp;lsquo;harmless&amp;rsquo; config script against a pfSense box and watched the entire firewall configuration evaporate in a single function call.</description><content:encoded><![CDATA[<figure style="text-align:center;margin:0 0 30px"><img src="hero.png" alt="The Firewall API Call That Factory-Resets Your Router" style="max-width:520px;width:100%;border-radius:14px"/></figure>
<p>It was supposed to be a five-line automation. Read a value, tweak it, write it back. The kind of thing you knock out between coffees.</p>
<p>I shelled into a client&rsquo;s pfSense box — a clean, current 24.11 install — and ran my little script through <code>pfSsh.php</code>. It exited without a peep. No errors. No warnings. Just a fresh prompt blinking back at me.</p>
<p>Then the SSH session died.</p>
<p>Then the VPN dropped.</p>
<p>Then my phone lit up.</p>
<h2 id="the-investigation">The investigation</h2>
<p>I got back in through the console. The dashboard loaded, and my stomach went cold. No WAN config. No LAN rules. No NAT, no VLANs, no DHCP scopes. The box was sitting there with the bright-eyed innocence of a router that had never met a network.</p>
<p>The config wasn&rsquo;t corrupted. It was <em>gone</em>. Replaced with defaults, like someone had hit the reset button — except nobody had touched the hardware.</p>
<p>I pulled up my script. Five lines. The offending logic looked like ancient, trustworthy pfSense scripting lore:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#ff79c6">global</span> <span style="color:#8be9fd;font-style:italic">$config</span>;
</span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">$config</span>[<span style="color:#f1fa8c">&#39;system&#39;</span>][<span style="color:#f1fa8c">&#39;something&#39;</span>] <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#39;newvalue&#39;</span>;
</span></span><span style="display:flex;"><span>write_config(<span style="color:#f1fa8c">&#34;scripted tweak&#34;</span>);
</span></span></code></pre></div><p>Every old forum thread, every crusty wiki page from a decade ago, does exactly this. Grab the <code>$config</code> global, poke a key, call <code>write_config()</code>. It&rsquo;s the canonical move.</p>
<p>So why did it nuke the box?</p>
<p>I added one debug line before the write:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#ff79c6">global</span> <span style="color:#8be9fd;font-style:italic">$config</span>;
</span></span><span style="display:flex;"><span>var_dump(<span style="color:#8be9fd;font-style:italic">$config</span>);
</span></span></code></pre></div><p>The output told the whole story in one ugly word: <strong>empty.</strong></p>
<h2 id="the-aha">The &ldquo;aha&rdquo;</h2>
<p>On modern pfSense, that legacy <code>$config</code> global is no longer the living, fully-populated config tree the old guides assume. In the <code>pfSsh.php</code> scripting context it came back essentially empty — a hollow shell.</p>
<p>And <code>write_config()</code> doesn&rsquo;t merge. It doesn&rsquo;t patch. It takes whatever is in memory <em>right now</em> and serializes that as the new, authoritative config. The whole thing.</p>
<p>So my script read an empty global, set one key on top of nothing, and then confidently wrote that &ldquo;nothing plus one key&rdquo; back as the complete configuration. <code>write_config()</code> did exactly what it was told. It overwrote a fully working firewall with a near-empty husk.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>┌──────────────────────────────────────────────────────────┐
</span></span><span style="display:flex;"><span>│  WHAT I THOUGHT HAPPENED          WHAT ACTUALLY HAPPENED  │
</span></span><span style="display:flex;"><span>├──────────────────────────────────────────────────────────┤
</span></span><span style="display:flex;"><span>│  $config = { full tree }     ►    $config = { } (EMPTY)   │
</span></span><span style="display:flex;"><span>│        │                                  │               │
</span></span><span style="display:flex;"><span>│        ▼ set one key                      ▼ set one key   │
</span></span><span style="display:flex;"><span>│  { full tree + key }              { almost nothing }      │
</span></span><span style="display:flex;"><span>│        │                                  │               │
</span></span><span style="display:flex;"><span>│        ▼ write_config()                   ▼ write_config()│
</span></span><span style="display:flex;"><span>│   ✓ saved correctly               ✗ overwrote everything  │
</span></span><span style="display:flex;"><span>└──────────────────────────────────────────────────────────┘
</span></span></code></pre></div><p>The modern config backend wants you to read and write <em>specific paths</em> — <code>config_get_path()</code> and <code>config_set_path()</code> — never the whole global at once.</p>
<figure style="text-align:center;margin:34px 0">
<svg viewBox="0 0 560 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Path API versus legacy global write">
<rect x="0" y="0" width="560" height="220" fill="#11111b" rx="10"/>
<text x="280" y="30" fill="#cdd6f4" font-family="monospace" font-size="15" text-anchor="middle">read & write ONE key — not the whole tree</text>
<rect x="30" y="55" width="230" height="130" fill="#1e1e2e" stroke="#a6e3a1" stroke-width="2" rx="8"/>
<text x="145" y="80" fill="#a6e3a1" font-family="monospace" font-size="13" text-anchor="middle">SAFE — path API</text>
<text x="145" y="110" fill="#94e2d5" font-family="monospace" font-size="12" text-anchor="middle">config_get_path(k)</text>
<text x="145" y="132" fill="#94e2d5" font-family="monospace" font-size="12" text-anchor="middle">config_set_path(k,v)</text>
<text x="145" y="162" fill="#cdd6f4" font-family="monospace" font-size="12" text-anchor="middle">► touches only k ✓</text>
<rect x="300" y="55" width="230" height="130" fill="#1e1e2e" stroke="#f38ba8" stroke-width="2" rx="8"/>
<text x="415" y="80" fill="#f38ba8" font-family="monospace" font-size="13" text-anchor="middle">FOOTGUN — global</text>
<text x="415" y="110" fill="#fab387" font-family="monospace" font-size="12" text-anchor="middle">global $config; // empty</text>
<text x="415" y="132" fill="#fab387" font-family="monospace" font-size="12" text-anchor="middle">write_config()</text>
<text x="415" y="162" fill="#f38ba8" font-family="monospace" font-size="12" text-anchor="middle">✗ overwrites ALL of it</text>
<text x="280" y="208" fill="#6c7086" font-family="monospace" font-size="12" text-anchor="middle">one call, two very different blast radii</text>
</svg>
<figcaption style="color:#6c7086;font-size:14px;margin-top:8px">Scoped path writes touch one key. A global write_config() rewrites the entire config from whatever's in memory.</figcaption>
</figure>
<h2 id="the-fix">The fix</h2>
<p>First, recover. Restore the last known-good config from a backup and reload:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span><span style="color:#6272a4"># Restore from a saved backup and reload</span>
</span></span><span style="display:flex;"><span>config_restore /cf/conf/backup/config-&lt;latest-good&gt;.xml
</span></span><span style="display:flex;"><span>/etc/rc.reload_all
</span></span></code></pre></div><p>pfSense keeps automatic backups under <code>/cf/conf/backup/</code> — list them with <code>ls -lt /cf/conf/backup/</code> and pick the newest one from <em>before</em> your script ran. That brought the firewall back to life.</p>
<p>Then, fix the script. Use the path API and stop touching the global entirely:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#6272a4">// Read a specific key (with a default), modify, write that key back.
</span></span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">$value</span> <span style="color:#ff79c6">=</span> config_get_path(<span style="color:#f1fa8c">&#39;system/something&#39;</span>, <span style="color:#f1fa8c">&#39;default&#39;</span>);
</span></span><span style="display:flex;"><span>config_set_path(<span style="color:#f1fa8c">&#39;system/something&#39;</span>, <span style="color:#f1fa8c">&#39;newvalue&#39;</span>);
</span></span><span style="display:flex;"><span>write_config(<span style="color:#f1fa8c">&#34;scripted tweak via path API&#34;</span>);
</span></span></code></pre></div><p>And — non-negotiable — snapshot the config <em>before</em> any scripted write, so recovery is one command instead of a panic:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>cp /cf/config.xml <span style="color:#f1fa8c">&#34;/cf/conf/backup/config-</span><span style="color:#ff79c6">$(</span>date +%Y%m%d-%H%M%S<span style="color:#ff79c6">)</span><span style="color:#f1fa8c">-prewrite.xml&#34;</span>
</span></span></code></pre></div><h2 id="why-it-happened">Why it happened</h2>
<p>Platforms migrate their internals quietly. pfSense moved to a path-based config backend, and the old global <code>$config</code> got demoted from &ldquo;the config&rdquo; to &ldquo;a vestigial variable that may or may not be populated, depending on context.&rdquo;</p>
<p>The docs got updated. The thousand forum posts from 2014 did not.</p>
<p>So the trap is perfect: the legacy pattern still <em>compiles</em>, still <em>runs</em>, still <em>exits clean</em>. It just operates on a ghost of the real config — and <code>write_config()</code> is happy to make that ghost permanent.</p>
<p>A function that overwrites everything from in-memory state is a loaded gun. When the thing loading that memory changes underneath you, the gun is now pointed at your whole config and you don&rsquo;t even know it.</p>
<h2 id="takeaways">Takeaways</h2>
<ul>
<li><strong>A global <code>write_config()</code> rewrites the entire config from in-memory state.</strong> If that state is stale, partial, or empty, you&rsquo;ve just factory-reset your box. Treat it as a full-config replace, never a patch.</li>
<li><strong>When a platform migrates its config backend, the old global is a footgun.</strong> It still runs clean — that&rsquo;s what makes it dangerous. Verify what it actually contains before trusting it.</li>
<li><strong>Use the scoped API.</strong> <code>config_get_path()</code> / <code>config_set_path()</code> touch exactly one key. That&rsquo;s the blast radius you want.</li>
<li><strong>Snapshot before any scripted write.</strong> <code>cp /cf/config.xml /cf/conf/backup/...-prewrite.xml</code> turns a disaster into a 30-second restore.</li>
<li><strong><code>var_dump()</code> your assumptions.</strong> One debug line showing an empty global would&rsquo;ve saved the whole incident. When in doubt, print what you&rsquo;ve actually got — not what the old guides promise you have.</li>
</ul>
]]></content:encoded></item><item><title>90s Kid - The Floppy Disk</title><link>https://errorzap.com/posts/90s-kid-the-floppy-disk/</link><pubDate>Wed, 11 Mar 2026 00:00:00 -0600</pubDate><guid>https://errorzap.com/posts/90s-kid-the-floppy-disk/</guid><description>A square of beige plastic that held your whole world in 1.44 megabytes and felt like it could hold the universe.</description><content:encoded><![CDATA[<figure style="margin:0 0 28px">
<svg viewBox="0 0 800 240" xmlns="http://www.w3.org/2000/svg" style="width:100%;border-radius:14px">
 <defs>
  <linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#1e1e2e"/><stop offset="1" stop-color="#11111b"/></linearGradient>
  <linearGradient id="sun" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f9e2af"/><stop offset="0.5" stop-color="#fab387"/><stop offset="1" stop-color="#f38ba8"/></linearGradient>
 </defs>
 <rect width="800" height="240" fill="url(#sky)"/>
 <circle cx="640" cy="150" r="74" fill="url(#sun)"/><rect x="566" y="118" width="148" height="4" fill="#11111b"/><rect x="566" y="127" width="148" height="4" fill="#11111b"/><rect x="566" y="136" width="148" height="4" fill="#11111b"/><rect x="566" y="145" width="148" height="4" fill="#11111b"/><rect x="566" y="154" width="148" height="4" fill="#11111b"/><rect x="566" y="163" width="148" height="4" fill="#11111b"/><rect x="566" y="172" width="148" height="4" fill="#11111b"/>
 <line x1="-200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="280" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="360" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="440" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="520" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="600" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="680" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="760" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="840" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="920" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="1000" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="0" y1="178" x2="800" y2="178" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="184" x2="800" y2="184" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="193" x2="800" y2="193" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="206" x2="800" y2="206" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="227" x2="800" y2="227" stroke="#94e2d5" stroke-width="1" opacity="0.22"/>
 <text x="48" y="150" font-size="78">💾</text>
 <text x="150" y="96" font-family="ui-monospace,monospace" font-size="14" fill="#cba6f7" letter-spacing="5">90s KID //</text>
 <text x="150" y="140" font-family="-apple-system,Segoe UI,sans-serif" font-size="30" font-weight="800" fill="#cdd6f4">The Floppy Disk</text>
 <rect x="0" y="236" width="800" height="4" fill="#f38ba8"/>
</svg></figure>
<h2 id="the-save-icon-you-could-hold-in-your-hand">The Save Icon You Could Hold In Your Hand</h2>
<p>You remember the floppy disk.</p>
<p>Not the bendy black ones your older cousin had.<br>
The hard ones. The <strong>beige squares</strong> with the little silver shutter on top.</p>
<p>You remember the <em>click</em> it made going into the slot.<br>
That confident, mechanical <em>chunk</em> that told you something important was happening.</p>
<p>You&rsquo;d slide it in.<br>
The drive light would blink.<br>
And the whole computer would make that grinding, churning noise like it was thinking <em>really hard</em>.</p>
<hr>
<p>To a kid, a floppy was basically magic.</p>
<ul>
<li><strong>The metal shutter.</strong> You were NOT supposed to slide it open. So obviously you slid it open. There was shiny brown film inside. You touched it. You doomed the disk.</li>
<li><strong>The write-protect tab.</strong> A tiny plastic switch that felt like arming a nuclear device.</li>
<li><strong>The label.</strong> You wrote &ldquo;GAMES&rdquo; on it in marker like a tiny librarian.</li>
<li><strong>The hoard.</strong> A whole rainbow box of them, and you had no idea what was on 80% of them.</li>
</ul>
<p>It felt <strong>forbidden</strong> and <strong>fragile</strong> and <strong>yours</strong>.</p>
<p>You knew, somehow, that if you put a magnet near it, you&rsquo;d kill it. Nobody told you the rules. You just <em>knew</em>. Floppy disk law was passed down kid to kid like folklore.</p>
<hr>
<p>Here&rsquo;s the reveal.</p>
<p>That beige square held <strong>1.44 megabytes</strong>.</p>
<p>That&rsquo;s it. That&rsquo;s the whole thing.<br>
Less than a single phone photo today. A blurry one.</p>
<p>Inside that shutter was an actual spinning disk of <em>magnetic-coated mylar</em> — that shiny brown film you weren&rsquo;t supposed to touch. The drive head physically pressed against it and read magnetic charges, like a tiny record player for ones and zeros.</p>
<p>The shutter? That slid open <strong>automatically</strong> when you inserted it, so the head could reach the disk. You opening it by hand was just you, a small barbarian, exposing the delicate part to dust and fingerprints.</p>
<p>The write-protect tab actually worked by <strong>letting light through a hole</strong>. Open the hole, a sensor saw it, and the drive physically refused to write. It wasn&rsquo;t software. It was a <em>window</em>. Genuinely un-hackable by a kid, which is the only security that ever truly held.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>        ________________________
</span></span><span style="display:flex;"><span>       |  __                    |
</span></span><span style="display:flex;"><span>       | |  |  &lt;- write-protect |
</span></span><span style="display:flex;"><span>       | |__|     tab (a hole!) |
</span></span><span style="display:flex;"><span>       |  ____________________  |
</span></span><span style="display:flex;"><span>       | |####################| |  &lt;- metal shutter
</span></span><span style="display:flex;"><span>       | |####################| |     (do NOT slide it)
</span></span><span style="display:flex;"><span>       |_|____________________|_|
</span></span><span style="display:flex;"><span>       |                        |
</span></span><span style="display:flex;"><span>       |   [ GAMES ]            |  &lt;- your handwriting
</span></span><span style="display:flex;"><span>       |   ( in marker )        |
</span></span><span style="display:flex;"><span>       |________________________|
</span></span></code></pre></div><hr>
<p>The funny part?</p>
<p>The floppy never really left.</p>
<p>Look at the <strong>Save button</strong> in basically any app you&rsquo;ve used in the last thirty years. That little square with the shutter? That&rsquo;s a floppy disk. A piece of 1980s hardware, frozen forever as an icon, saving documents to drives that floppy disks couldn&rsquo;t <em>dream</em> of holding.</p>
<p>There are kids today hitting Save who have never seen the object the button is <em>shaped like</em>. To them it&rsquo;s just &ldquo;the save shape.&rdquo; A glyph. A rune with no origin story.</p>
<p>But you know.</p>
<p>You know it went <em>chunk</em>.<br>
You know you weren&rsquo;t supposed to touch the brown part.<br>
And you know that somewhere in a closet, there&rsquo;s still a rainbow box of them, holding 1.44 megabytes of something you&rsquo;ll never read again.</p>
<p>And honestly? That&rsquo;s plenty.</p>
]]></content:encoded></item><item><title>The Payment That Showed $0 (But Wasn't)</title><link>https://errorzap.com/posts/the-payment-that-showed-zero-but-wasnt/</link><pubDate>Sun, 08 Mar 2026 00:00:00 -0700</pubDate><guid>https://errorzap.com/posts/the-payment-that-showed-zero-but-wasnt/</guid><description>A recorded payment rendered as &amp;lsquo;$0.00 applied&amp;rsquo; and everyone assumed the money vanished — but the ledger had been right the whole time.</description><content:encoded><![CDATA[<figure style="text-align:center;margin:0 0 30px"><img src="hero.png" alt="The Payment That Showed $0 (But Wasn't)" style="max-width:520px;width:100%;border-radius:14px"/></figure>
<p>The message came in the way these always do. No timestamp, no context, just adrenaline.</p>
<p>&ldquo;The payment shows zero applied. The customer paid. Where did the money GO?&rdquo;</p>
<p>Self-hosted invoicing app. A real payment, recorded against a real invoice. The UI said <strong>$0.00 applied</strong>. To everyone looking at that screen, that meant one thing: a customer handed over money and the system ate it.</p>
<p>That&rsquo;s a five-alarm fire in billing. Money you can&rsquo;t account for is money you have to refund, re-bill, or explain to an auditor. So I did the thing you do when the building is allegedly burning. I stopped trusting the screen.</p>
<h2 id="the-investigation">The investigation</h2>
<p>The UI is a story the app tells you. The database is what actually happened. When the two disagree, the database wins — every time.</p>
<p>So I went to the source of truth. Payments table first.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> id, invoice_id, amount, applied, status, created_at
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">FROM</span> payments
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> invoice_id <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">4815</span>;
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span> id  | invoice_id | amount  | applied | status    | created_at
</span></span><span style="display:flex;"><span>-----+------------+---------+---------+-----------+---------------------
</span></span><span style="display:flex;"><span> 162 |       4815 | 1200.00 |  NULL   | completed | 2026-03-07 16:42:11
</span></span></code></pre></div><p>There it is. <code>amount</code> = 1200.00. Status <code>completed</code>. The money was recorded. It did not vanish. It was sitting in the table exactly where it should be.</p>
<p><code>applied</code> was <code>NULL</code> — interesting, but not &ldquo;missing money&rdquo; interesting. Stored zero would&rsquo;ve been one thing. <code>NULL</code> is the smell of a column nobody populates.</p>
<p>Now the invoice. Did the balance actually move?</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> id, amount, balance, status
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">FROM</span> invoices
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> id <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">4815</span>;
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span> id  | amount  | balance | status
</span></span><span style="display:flex;"><span>-----+---------+---------+--------
</span></span><span style="display:flex;"><span>4815 | 1200.00 |    0.00 | paid
</span></span></code></pre></div><p>Balance dropped to zero. Status flipped to <code>paid</code>. The ledger was <strong>correct</strong>. The accounting was <strong>correct</strong>. The money was where it belonged.</p>
<p>So why was the UI screaming?</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>        WHAT THE SCREEN SAID            WHAT THE DB SAID
</span></span><span style="display:flex;"><span>       ┌──────────────────────┐       ┌──────────────────────┐
</span></span><span style="display:flex;"><span>       │  Payment #162        │       │  payments.amount      │
</span></span><span style="display:flex;"><span>       │  Applied:  $0.00  ⚠  │  vs   │    = 1200.00  ✓       │
</span></span><span style="display:flex;"><span>       │  &#34;money gone?!&#34;      │       │  invoices.balance     │
</span></span><span style="display:flex;"><span>       └──────────────────────┘       │    = 0.00 (paid) ✓    │
</span></span><span style="display:flex;"><span>                  │                    └──────────────────────┘
</span></span><span style="display:flex;"><span>                  │                              │
</span></span><span style="display:flex;"><span>                  └──────────► SAME ◄────────────┘
</span></span><span style="display:flex;"><span>                         UNDERLYING TRUTH
</span></span></code></pre></div><h2 id="the-aha">The &ldquo;aha&rdquo;</h2>
<p>The &ldquo;Applied&rdquo; figure on that payment screen wasn&rsquo;t reading <code>payments.amount</code>. It was reading a <strong>computed display field</strong> — a value the UI derived at render time from a relationship that, for this payment, came back empty.</p>
<p>The <code>applied</code> column existed in the schema but was never written by this payment path. It lived only in the database and was never even exposed through the API. The renderer reached for it, found <code>NULL</code>, coerced it to <code>0.00</code>, and printed a number that looked like a catastrophe.</p>
<p>Nothing was lost. A blank field got rendered as a zero, and a zero in a money column reads like a heist.</p>
<figure style="text-align:center;margin:34px 0">
<svg viewBox="0 0 560 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Stored truth versus rendered display">
  <rect x="0" y="0" width="560" height="220" fill="#11111b"/>
  <rect x="36" y="48" width="200" height="120" rx="10" fill="#1e1e2e" stroke="#45475a"/>
  <text x="136" y="40" fill="#a6e3a1" font-family="monospace" font-size="14" text-anchor="middle">DATABASE (truth)</text>
  <text x="56" y="86" fill="#cdd6f4" font-family="monospace" font-size="13">amount  = 1200.00</text>
  <text x="56" y="112" fill="#cdd6f4" font-family="monospace" font-size="13">balance =    0.00</text>
  <text x="56" y="138" fill="#6c7086" font-family="monospace" font-size="13">applied =    NULL</text>
  <rect x="324" y="48" width="200" height="120" rx="10" fill="#1e1e2e" stroke="#45475a"/>
  <text x="424" y="40" fill="#f38ba8" font-family="monospace" font-size="14" text-anchor="middle">UI (rendering)</text>
  <text x="344" y="100" fill="#fab387" font-family="monospace" font-size="13">Applied:</text>
  <text x="344" y="128" fill="#f38ba8" font-family="monospace" font-size="22">$0.00  ⚠</text>
  <path d="M236 108 L324 108" stroke="#cba6f7" stroke-width="2" fill="none" marker-end="url(#a)"/>
  <text x="280" y="98" fill="#89b4fa" font-family="monospace" font-size="11" text-anchor="middle">render</text>
  <text x="280" y="206" fill="#6c7086" font-family="monospace" font-size="12" text-anchor="middle">NULL & empty relation < truth > 0.00 panic</text>
  <defs>
    <marker id="a" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto">
      <path d="M0,0 L8,3 L0,6 Z" fill="#cba6f7"/>
    </marker>
  </defs>
</svg>
<figcaption style="color:#6c7086;font-size:14px;margin-top:8px">The data was fine. The renderer turned an empty field into a $1,200 scare.</figcaption>
</figure>
<h2 id="the-fix">The fix</h2>
<p>There was no data to fix. The fix was confirming the ground truth and proving the money was accounted for, then knowing which field to never trust again.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#6272a4">-- Prove the payment is reconciled against the invoice
</span></span></span><span style="display:flex;"><span><span style="color:#ff79c6">SELECT</span> i.id,
</span></span><span style="display:flex;"><span>       i.amount        <span style="color:#ff79c6">AS</span> invoice_total,
</span></span><span style="display:flex;"><span>       i.balance,
</span></span><span style="display:flex;"><span>       COALESCE(<span style="color:#ff79c6">SUM</span>(p.amount), <span style="color:#bd93f9">0</span>) <span style="color:#ff79c6">AS</span> paid_total,
</span></span><span style="display:flex;"><span>       i.amount <span style="color:#ff79c6">-</span> COALESCE(<span style="color:#ff79c6">SUM</span>(p.amount), <span style="color:#bd93f9">0</span>) <span style="color:#ff79c6">AS</span> expected_balance
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">FROM</span> invoices i
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">LEFT</span> <span style="color:#ff79c6">JOIN</span> payments p <span style="color:#ff79c6">ON</span> p.invoice_id <span style="color:#ff79c6">=</span> i.id <span style="color:#ff79c6">AND</span> p.status <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">&#39;completed&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">WHERE</span> i.id <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">4815</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">GROUP</span> <span style="color:#ff79c6">BY</span> i.id;
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span> id  | invoice_total | balance | paid_total | expected_balance
</span></span><span style="display:flex;"><span>-----+---------------+---------+------------+------------------
</span></span><span style="display:flex;"><span>4815 |       1200.00 |    0.00 |    1200.00 |             0.00
</span></span></code></pre></div><p><code>balance</code> matches <code>expected_balance</code>. Books are square. Crisis cancelled — because there was never a crisis, only a bad pixel.</p>
<h2 id="why-it-happened">Why it happened</h2>
<p>The app had two notions of &ldquo;applied&rdquo;: the real stored amount on the payment, and a derived field the UI computed from a relation that this particular payment flow never filled in. When the relation was empty, the computed value fell through to zero instead of throwing or hiding itself.</p>
<p>A zero in a dollar column is the most alarming value a billing system can show. The renderer printed it with full confidence and zero context, and a correct ledger looked like a robbery.</p>
<h2 id="takeaways">Takeaways</h2>
<ul>
<li><strong>The UI is a rendering of the truth, not the truth.</strong> When a number looks wrong, query the database before you assume data loss.</li>
<li><strong>Most &ldquo;missing money&rdquo; is a display bug.</strong> Check the ledger and the invoice balance first — the books are usually fine.</li>
<li><strong>Know which fields are stored vs computed.</strong> A value derived at render time can lie even when every stored byte is correct.</li>
<li><strong><code>NULL</code> coerced to <code>0</code> is a landmine in money columns.</strong> A blank field and an actual zero mean wildly different things; don&rsquo;t let the UI conflate them.</li>
<li><strong>Schema fields can exist without ever being populated or exposed.</strong> Confirm the data path actually writes the column before you trust what reads it.</li>
</ul>
]]></content:encoded></item><item><title>90s Kid - The Cable Box and Its Wired Remote</title><link>https://errorzap.com/posts/90s-kid-the-cable-box/</link><pubDate>Fri, 06 Mar 2026 00:00:00 -0700</pubDate><guid>https://errorzap.com/posts/90s-kid-the-cable-box/</guid><description>You remember the heavy little box on top of the TV, and the remote that was leashed to it like a dog that wasn&amp;rsquo;t allowed off the porch.</description><content:encoded><![CDATA[<figure style="margin:0 0 28px">
<svg viewBox="0 0 800 240" xmlns="http://www.w3.org/2000/svg" style="width:100%;border-radius:14px">
 <defs>
  <linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#1e1e2e"/><stop offset="1" stop-color="#11111b"/></linearGradient>
  <linearGradient id="sun" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f9e2af"/><stop offset="0.5" stop-color="#fab387"/><stop offset="1" stop-color="#f38ba8"/></linearGradient>
 </defs>
 <rect width="800" height="240" fill="url(#sky)"/>
 <circle cx="640" cy="150" r="74" fill="url(#sun)"/><rect x="566" y="118" width="148" height="4" fill="#11111b"/><rect x="566" y="127" width="148" height="4" fill="#11111b"/><rect x="566" y="136" width="148" height="4" fill="#11111b"/><rect x="566" y="145" width="148" height="4" fill="#11111b"/><rect x="566" y="154" width="148" height="4" fill="#11111b"/><rect x="566" y="163" width="148" height="4" fill="#11111b"/><rect x="566" y="172" width="148" height="4" fill="#11111b"/>
 <line x1="-200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="280" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="360" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="440" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="520" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="600" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="680" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="760" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="840" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="920" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="1000" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="0" y1="178" x2="800" y2="178" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="184" x2="800" y2="184" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="193" x2="800" y2="193" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="206" x2="800" y2="206" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="227" x2="800" y2="227" stroke="#94e2d5" stroke-width="1" opacity="0.22"/>
 <text x="48" y="150" font-size="78">📺</text>
 <text x="150" y="96" font-family="ui-monospace,monospace" font-size="14" fill="#cba6f7" letter-spacing="5">90s KID //</text>
 <text x="150" y="140" font-family="-apple-system,Segoe UI,sans-serif" font-size="30" font-weight="800" fill="#cdd6f4">The Cable Box and Its Wired Remote</text>
 <rect x="0" y="236" width="800" height="4" fill="#f38ba8"/>
</svg></figure>
<h2 id="the-box-on-top-of-the-box">The Box On Top Of The Box</h2>
<p>You remember the cable box.</p>
<p>Not the TV. The other thing.<br>
The squat plastic brick that lived <strong>on top</strong> of the TV, where it had no business being.</p>
<p>It had a red LED clock that glowed like a tiny alarmed eye.<br>
It got warm. Always warm.<br>
And it had a <strong>wired remote</strong> — a remote on a leash — that snaked across the carpet like it was afraid to leave home.</p>
<p>You knew, in your kid bones, that this box was Important.</p>
<ul>
<li>It hummed faintly, like it was <em>thinking</em></li>
<li>The channel number flipped on it with a soft <em>clunk</em> you could almost feel</li>
<li>Dad told you not to unplug it or &ldquo;we&rsquo;ll lose the channels&rdquo;</li>
<li>The remote cord was always one stomped-on foot away from being yanked out</li>
<li>There was a little sliding door on the front, and behind it, <strong>forbidden buttons</strong></li>
</ul>
<p>You were pretty sure the box was <em>negotiating</em> with the TV on your behalf.<br>
And you weren&rsquo;t entirely wrong.</p>
<h3 id="what-it-actually-was">What it actually was</h3>
<p>Here&rsquo;s the nerdy reveal.</p>
<p>Your TV in the 90s could only really speak one fluent language: <strong>channels 2 through 13</strong>, the old VHF band.<br>
But the cable company was pumping <strong>dozens</strong> of channels down that one coax wire, all stacked on different frequencies way up high where your TV couldn&rsquo;t tune.</p>
<p>The cable box was a <strong>set-top converter</strong>. Its whole job:</p>
<blockquote>
<p>Grab the channel you wanted from way up the frequency dial<br>
and <strong>re-broadcast it down to channel 3</strong> (or 4),<br>
the one frequency your TV trusted.</p>
</blockquote>
<p>That&rsquo;s why you set the TV to 3 and <em>left it there forever</em>.<br>
The TV was just a dumb window.<br>
<strong>The box did the choosing.</strong></p>
<p>The wired remote? That was infrared and pricey, so the cheap units just ran a cable straight to the box — no batteries, no line-of-sight, no losing it in the couch. A leash, yes. But an <em>honest</em> one.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>        _________________________
</span></span><span style="display:flex;"><span>       |   [ 0 3 ]        CABLE   |
</span></span><span style="display:flex;"><span>       |   .----.                 |
</span></span><span style="display:flex;"><span>       |   | 03 |   o o o o o     |
</span></span><span style="display:flex;"><span>       |   &#39;----&#39;   POWER  ~~~    |
</span></span><span style="display:flex;"><span>       |_________________________|
</span></span><span style="display:flex;"><span>              |||  coax in
</span></span><span style="display:flex;"><span>              |||
</span></span><span style="display:flex;"><span>        ======O======&gt;  TV ch.3
</span></span><span style="display:flex;"><span>       (  &#34;leashed&#34;  )
</span></span><span style="display:flex;"><span>        \  remote   /
</span></span><span style="display:flex;"><span>         &#39;~~~~~~~~~&#39;
</span></span></code></pre></div><h3 id="it-never-really-left">It never really left</h3>
<p>The leash got cut. The box got smaller, then it got a guide, then a DVR, then a slot for movies you paid eight bucks to &ldquo;rent&rdquo; with a button.</p>
<p>But the <em>idea</em> never died.</p>
<p>Every streaming stick jammed into the back of your TV today is the <strong>exact same trick</strong>:<br>
a little warm box that does the real work, while the TV just sits there being a dumb, beautiful window.</p>
<p>You still set the input and leave it.<br>
You still don&rsquo;t unplug it, or &ldquo;we&rsquo;ll lose everything.&rdquo;</p>
<p>The box on top of the box won.<br>
It just learned to hide.</p>
]]></content:encoded></item><item><title>How My Own Backup Deleted My Files</title><link>https://errorzap.com/posts/how-my-own-backup-deleted-my-files/</link><pubDate>Wed, 04 Mar 2026 00:00:00 -0700</pubDate><guid>https://errorzap.com/posts/how-my-own-backup-deleted-my-files/</guid><description>I deleted nine files on purpose — and my backup node helpfully deleted them everywhere else too.</description><content:encoded><![CDATA[<figure style="text-align:center;margin:0 0 30px"><img src="hero.png" alt="How My Own Backup Deleted My Files" style="max-width:520px;width:100%;border-radius:14px"/></figure>
<p>The ticket was boring. Cleanup. A client wanted some stale files gone from their primary share. I deleted them. Coffee. Done.</p>
<p>Twenty minutes later the same nine files vanished from the backup node too.</p>
<p>That&rsquo;s the part that made my stomach drop. The backup node existed <em>precisely</em> so that &ldquo;oops, deleted it on the primary&rdquo; was a non-event. It was supposed to be the safety net. Instead it watched me cut the trapeze and then helpfully cut its own rope to match.</p>
<h2 id="the-investigation">The investigation</h2>
<p>First instinct: ransomware, a rogue cron, a fat-fingered teammate. All wrong. The truth was dumber and worse.</p>
<p>I&rsquo;d inherited this setup and never read the sync config closely. I assumed &ldquo;backup node&rdquo; meant one-way. It didn&rsquo;t.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>grep -i <span style="color:#f1fa8c">&#39;mode\|direction\|propagat&#39;</span> /etc/syncthing/config.xml
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>&lt;folder id=&#34;primary-share&#34; type=&#34;sendreceive&#34; ...&gt;
</span></span></code></pre></div><p><code>sendreceive</code>. Bidirectional. The backup node wasn&rsquo;t a backup at all — it was a <em>mirror</em>. And a mirror does exactly one job: make the other side look like this side. Including the absences.</p>
<p>I deleted nine files on the primary. The sync engine saw nine deletions, decided they were authoritative changes, and faithfully replicated them downstream. No malice. No bug. It did exactly what I configured it to do.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>┌──────────────┐   delete 9 files    ┌──────────────┐
</span></span><span style="display:flex;"><span>│   PRIMARY    │ ──────────────────► │  &#34;BACKUP&#34;    │
</span></span><span style="display:flex;"><span>│  (source)    │   sendreceive       │  node        │
</span></span><span style="display:flex;"><span>└──────────────┘   propagates ✗      └──────────────┘
</span></span><span style="display:flex;"><span>       │                                    │
</span></span><span style="display:flex;"><span>       ▼                                    ▼
</span></span><span style="display:flex;"><span>   files gone                          files gone too
</span></span><span style="display:flex;"><span>                                       ▓ safety net: GONE
</span></span></code></pre></div><p>The &ldquo;aha&rdquo; wasn&rsquo;t a clever diagnostic. It was reading one attribute and feeling like an idiot. <strong>Sync is not backup.</strong> A sync engine has no concept of &ldquo;retain this.&rdquo; Its entire worldview is <em>converge</em>. Deletion is just another change to converge on.</p>
<figure style="text-align:center;margin:34px 0">
<svg viewBox="0 0 560 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Bidirectional sync versus pull-only backup">
<defs>
<marker id="arrow" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
<path d="M0,0 L6,3 L0,6 Z" fill="#cba6f7"/>
</marker>
</defs>
<rect x="0" y="0" width="560" height="220" fill="#11111b"/>
<text x="280" y="26" fill="#cdd6f4" font-family="monospace" font-size="14" text-anchor="middle">sync mirrors deletions · backup keeps them</text>
<rect x="40" y="60" width="130" height="56" rx="8" fill="#1e1e2e" stroke="#45475a"/>
<text x="105" y="93" fill="#89b4fa" font-family="monospace" font-size="13" text-anchor="middle">PRIMARY</text>
<rect x="390" y="50" width="130" height="34" rx="8" fill="#1e1e2e" stroke="#45475a"/>
<text x="455" y="72" fill="#f38ba8" font-family="monospace" font-size="12" text-anchor="middle">sendreceive ✗</text>
<rect x="390" y="130" width="130" height="34" rx="8" fill="#1e1e2e" stroke="#45475a"/>
<text x="455" y="152" fill="#a6e3a1" font-family="monospace" font-size="12" text-anchor="middle">pull-only ✓</text>
<line x1="170" y1="78" x2="385" y2="67" stroke="#f38ba8" stroke-width="2" marker-end="url(#arrow)"/>
<line x1="385" y1="67" x2="172" y2="88" stroke="#f38ba8" stroke-width="2" stroke-dasharray="4 4" marker-end="url(#arrow)"/>
<text x="278" y="64" fill="#f38ba8" font-family="monospace" font-size="11" text-anchor="middle">delete →</text>
<text x="278" y="100" fill="#f38ba8" font-family="monospace" font-size="11" text-anchor="middle">← delete propagates</text>
<line x1="170" y1="120" x2="385" y2="147" stroke="#a6e3a1" stroke-width="2" marker-end="url(#arrow)"/>
<text x="278" y="142" fill="#a6e3a1" font-family="monospace" font-size="11" text-anchor="middle">receive only, never push</text>
</svg>
<figcaption style="color:#6c7086;font-size:14px;margin-top:8px">A bidirectional mirror converges on deletions. A pull-only backup receives changes but never propagates them back.</figcaption>
</figure>
<h2 id="the-fix">The fix</h2>
<p>Recover first, redesign second. The deleted files came back from a daily snapshot that — by luck, not design — lived <em>off</em> the sync path. Then I changed the node from a mirror into an actual backup.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># 1) flip the backup node to PULL-ONLY: it receives, never pushes</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">#    Syncthing: set this folder to &#34;Receive Only&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">#    config.xml -&gt; type=&#34;receiveonly&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 2) route deletions to a versioned trash instead of hard-deleting</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">#    &lt;versioning type=&#34;trashcan&#34;&gt; with a retention window</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">#    (deletes land in .stversions/, not the void)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 3) shorten the sync interval to bound the blast radius</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">#    rescanIntervalS: 3600 -&gt; 300   # 5 min, not 1 hour</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 4) confirm the daily snapshot job runs OUTSIDE the synced tree</span>
</span></span><span style="display:flex;"><span>systemctl list-timers | grep snapshot
</span></span><span style="display:flex;"><span>ls -la /var/snapshots/   <span style="color:#6272a4"># snapshots here, NOT under the synced folder</span>
</span></span></code></pre></div><p>After the flip, the verification was the satisfying part. Delete a test file on the primary, watch the backup node <em>not</em> care:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># on primary</span>
</span></span><span style="display:flex;"><span>touch /srv/share/CANARY.txt
</span></span><span style="display:flex;"><span>rm  /srv/share/CANARY.txt
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># on backup node, a few minutes later</span>
</span></span><span style="display:flex;"><span>ls /srv/backup/share/CANARY.txt
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># CANARY.txt is still there ✓  (delete did not propagate)</span>
</span></span></code></pre></div><p>Receive-only means upstream deletions are treated as <em>conflicts to ignore</em>, not commands to obey. The node now keeps things the primary throws away. That&rsquo;s the whole point of a backup.</p>
<h2 id="why-it-happened">Why it happened</h2>
<p>Because &ldquo;sync&rdquo; and &ldquo;backup&rdquo; use the same verbs — copy, mirror, replicate — people treat them as the same noun. They aren&rsquo;t.</p>
<p>Sync optimizes for <em>agreement</em>: every node looks identical, fast. Backup optimizes for <em>survival</em>: keep a copy even when the source loses it. Those goals conflict the instant you delete something. A mirror resolves the conflict by deleting too. A backup resolves it by holding on.</p>
<p>I&rsquo;d labeled a mirror &ldquo;backup&rdquo; and trusted the label. The config said <code>sendreceive</code> the whole time. I just never looked.</p>
<h2 id="takeaways">Takeaways</h2>
<ul>
<li><strong>Sync is not backup.</strong> Bidirectional sync replicates deletions perfectly — that&rsquo;s a feature, and it will eat your data.</li>
<li><strong>A backup node must be one-way.</strong> Receive-only / pull-only: it accepts changes but never pushes or propagates deletes upstream-to-down.</li>
<li><strong>Route deletions to trash, not the void.</strong> A versioned trashcan with retention turns &ldquo;gone forever&rdquo; into &ldquo;gone for now.&rdquo;</li>
<li><strong>Keep snapshots off the sync path.</strong> If your point-in-time recovery lives inside the synced tree, the sync can delete your recovery too.</li>
<li><strong>Read the config before you trust the label.</strong> <code>grep</code> the one attribute that defines behavior — <code>sendreceive</code> vs <code>receiveonly</code> is the whole story.</li>
</ul>
]]></content:encoded></item><item><title>90s Kid - The Phone Cord That Reached the Whole House</title><link>https://errorzap.com/posts/90s-kid-the-phone-cord/</link><pubDate>Sun, 01 Mar 2026 00:00:00 -0700</pubDate><guid>https://errorzap.com/posts/90s-kid-the-phone-cord/</guid><description>The coiled cord was a leash, a lifeline, and the closest thing your house had to a portal — and you stretched it to its absolute limit.</description><content:encoded><![CDATA[<figure style="margin:0 0 28px">
<svg viewBox="0 0 800 240" xmlns="http://www.w3.org/2000/svg" style="width:100%;border-radius:14px">
 <defs>
  <linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#1e1e2e"/><stop offset="1" stop-color="#11111b"/></linearGradient>
  <linearGradient id="sun" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f9e2af"/><stop offset="0.5" stop-color="#fab387"/><stop offset="1" stop-color="#f38ba8"/></linearGradient>
 </defs>
 <rect width="800" height="240" fill="url(#sky)"/>
 <circle cx="640" cy="150" r="74" fill="url(#sun)"/><rect x="566" y="118" width="148" height="4" fill="#11111b"/><rect x="566" y="127" width="148" height="4" fill="#11111b"/><rect x="566" y="136" width="148" height="4" fill="#11111b"/><rect x="566" y="145" width="148" height="4" fill="#11111b"/><rect x="566" y="154" width="148" height="4" fill="#11111b"/><rect x="566" y="163" width="148" height="4" fill="#11111b"/><rect x="566" y="172" width="148" height="4" fill="#11111b"/>
 <line x1="-200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="280" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="360" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="440" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="520" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="600" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="680" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="760" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="840" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="920" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="1000" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="0" y1="178" x2="800" y2="178" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="184" x2="800" y2="184" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="193" x2="800" y2="193" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="206" x2="800" y2="206" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="227" x2="800" y2="227" stroke="#94e2d5" stroke-width="1" opacity="0.22"/>
 <text x="48" y="150" font-size="78">☎</text>
 <text x="150" y="96" font-family="ui-monospace,monospace" font-size="14" fill="#cba6f7" letter-spacing="5">90s KID //</text>
 <text x="150" y="140" font-family="-apple-system,Segoe UI,sans-serif" font-size="30" font-weight="800" fill="#cdd6f4">The Phone Cord That Reached the Wh</text>
 <rect x="0" y="236" width="800" height="4" fill="#f38ba8"/>
</svg></figure>
<h2 id="the-coil-that-stretched-from-the-kitchen-to-the-edge-of-forever">The Coil That Stretched From the Kitchen to the Edge of Forever</h2>
<p>You remember the cord.</p>
<p>That curly, springy, <strong>impossibly tangled</strong> umbilical between the wall and the phone you were absolutely not supposed to be using right now.</p>
<p>You remember stretching it.</p>
<p>Kitchen, around the corner, down the hall — pulled taut, vibrating like a guitar string, the handset pressed to your ear while you sat on the floor of the only room with a door that closed.</p>
<p>You remember the <em>stretch limit.</em><br>
The exact spot where one more inch would yank the whole phone off the wall.</p>
<hr>
<p>It felt like more than a wire.</p>
<p>The vibes:</p>
<ul>
<li><strong>A leash.</strong> Mom could find you by following it like a treasure map.</li>
<li><strong>A weapon.</strong> Whip it and it cracked like a tiny lightning bolt.</li>
<li><strong>A mystery.</strong> Why was it ALWAYS knotted? You hung up straight. It tangled out of spite.</li>
<li><strong>Forbidden tech.</strong> Long-distance calls cost <em>real money</em>, and you could feel the adults watching the clock.</li>
<li><strong>A portal.</strong> Somehow your friend&rsquo;s voice was <em>in your hand</em> and they were across town.</li>
</ul>
<p>You&rsquo;d stretch it as far as it would go just to whisper. That extra ten feet of cord was the closest thing your house had to privacy.</p>
<hr>
<p>Here&rsquo;s what it actually was.</p>
<p>That coiled cord carried two tiny copper wires — a <strong>circuit</strong> — and your voice rode it as a wobbling electrical current, a copy of the sound pressure hitting the mouthpiece.</p>
<p>The curl wasn&rsquo;t decoration. It was <strong>strain relief</strong>: coil it and you get slack without a long trip-wire dangling everywhere. The price of that clever spring? It stored twist, and every time you flipped the handset it wound up a little more. The tangle was <em>physics</em>, not spite.</p>
<p>And the wall jack? Behind it, a pair of wires ran out of your house, down the street, to a <strong>central office</strong> full of switches — once human operators with patch cords, later electromechanical relays clacking in the dark — quietly connecting your line to anyone&rsquo;s, anywhere.</p>
<p>You were holding the end of a network that wrapped the entire planet. You just used it to ask if anyone was home.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>        _.-._
</span></span><span style="display:flex;"><span>       / ___ \
</span></span><span style="display:flex;"><span>      | /   \ |     &#34;...around the corner...
</span></span><span style="display:flex;"><span>      | \___/ |          one more inch...&#34;
</span></span><span style="display:flex;"><span>       \_____/
</span></span><span style="display:flex;"><span>          |
</span></span><span style="display:flex;"><span>        __|__
</span></span><span style="display:flex;"><span>       (_____)
</span></span><span style="display:flex;"><span>       /  |  \____
</span></span><span style="display:flex;"><span>      |   |       )___
</span></span><span style="display:flex;"><span>       \  |   ___/    )___
</span></span><span style="display:flex;"><span>        \ |__/    ___/    )___
</span></span><span style="display:flex;"><span>         \|      /    ___/    )
</span></span><span style="display:flex;"><span>          \_____/    /    ___/
</span></span><span style="display:flex;"><span>                 \__/____/
</span></span><span style="display:flex;"><span>              wall ---&gt; the whole world
</span></span></code></pre></div><hr>
<p>The cord is gone now. You carry the phone <em>and</em> the central office in your pocket, and it never tangles.</p>
<p>But the stretch never left you.</p>
<p>You still wander the house looking for the quiet room. You still pace to the edge of the WiFi. You still find the one spot — by the window, on the stairs — where the signal holds and nobody can hear you.</p>
<p>The leash got invisible.</p>
<p>You&rsquo;re <strong>still</strong> pulling it as far as it&rsquo;ll go.</p>
]]></content:encoded></item><item><title>When Your Backup Client Lies About Its Name</title><link>https://errorzap.com/posts/when-your-backup-client-lies-about-its-name/</link><pubDate>Fri, 27 Feb 2026 00:00:00 -0700</pubDate><guid>https://errorzap.com/posts/when-your-backup-client-lies-about-its-name/</guid><description>A green-lit backup agent that hadn&amp;rsquo;t saved a single byte in weeks, because the machine and the server disagreed about what it was called.</description><content:encoded><![CDATA[<figure style="text-align:center;margin:0 0 30px"><img src="hero.png" alt="When Your Backup Client Lies About Its Name" style="max-width:520px;width:100%;border-radius:14px"/></figure>
<p>The dashboard was green. That&rsquo;s the part that still bugs me.</p>
<p>A customer calls because a workstation died and they need yesterday&rsquo;s file back. Easy day. I pull up the backup server, find the machine, and go to restore. Except there&rsquo;s nothing to restore. Last successful backup: never. Not &ldquo;stale.&rdquo; Not &ldquo;failed.&rdquo; <strong>Never.</strong></p>
<p>But the agent was online. Heartbeat OK. Service running OK. Sitting there reporting in like a good little soldier. It just hadn&rsquo;t backed up a damn thing the entire time it had been installed.</p>
<h2 id="the-investigation">The investigation</h2>
<p>First instinct: blame the agent. So I remoted in and read the client logs. The agent was happily completing runs. No errors. It thought it was doing its job.</p>
<p>So now I&rsquo;ve got an agent that says &ldquo;I backed up&rdquo; and a server that says &ldquo;this machine has never backed up.&rdquo; Both swear they&rsquo;re telling the truth. When two systems disagree about reality, the disagreement is almost always about <strong>identity</strong>.</p>
<p>I checked what name the agent was registered under on the server. Then I checked the machine&rsquo;s actual OS hostname.</p>
<p>They didn&rsquo;t match.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>┌──────────────────────────┐        ┌──────────────────────────┐
</span></span><span style="display:flex;"><span>│      THE WORKSTATION      │        │      BACKUP SERVER        │
</span></span><span style="display:flex;"><span>│                           │        │                           │
</span></span><span style="display:flex;"><span>│  hostname:  WS-FRONTDESK  │        │  client:  frontdesk-pc    │
</span></span><span style="display:flex;"><span>│  agent runs OK            │        │  keyed by name ───┐       │
</span></span><span style="display:flex;"><span>│  &#34;backup complete&#34; OK     │        │                   ▼       │
</span></span><span style="display:flex;"><span>└───────────┬──────────────┘        │   ┌───────────────────┐   │
</span></span><span style="display:flex;"><span>            │  reports as            │   │ frontdesk-pc      │   │
</span></span><span style="display:flex;"><span>            │  WS-FRONTDESK ─────────┼──►│ (PHANTOM CLIENT)  │   │
</span></span><span style="display:flex;"><span>            │                        │   │ last backup: none │   │
</span></span><span style="display:flex;"><span>            ▼                        │   └───────────────────┘   │
</span></span><span style="display:flex;"><span>   bytes go ►►► nowhere              │   WS-FRONTDESK: not found │
</span></span><span style="display:flex;"><span>└──────────────────────────┘        └──────────────────────────┘
</span></span></code></pre></div><p>The agent identified itself by the machine&rsquo;s real hostname. The server had the client filed under a different, hand-typed name from install day. So the agent checked in as <code>WS-FRONTDESK</code>, the server looked for a client called <code>WS-FRONTDESK</code>, found nobody, and the actual data went into the void. Meanwhile the <code>frontdesk-pc</code> entry someone created manually just sat there forever, eternally &ldquo;waiting for first backup,&rdquo; looking close enough to green that nobody clocked it.</p>
<h2 id="the-aha">The &ldquo;aha&rdquo;</h2>
<p>Backup identity <strong>is</strong> the hostname. Not a label you pick. Not a friendly name. The hostname.</p>
<p>The server keys everything — auth, storage path, history — off the client name. If that name doesn&rsquo;t equal what the OS reports as <code>hostname</code>, the agent and server are two ships passing in the night, both convinced they&rsquo;re docked.</p>
<figure style="text-align:center;margin:34px 0">
<svg viewBox="0 0 560 220" xmlns="http://www.w3.org/2000/svg" font-family="ui-monospace,monospace">
  <rect x="0" y="0" width="560" height="220" fill="#11111b"/>
  <rect x="40" y="50" width="180" height="120" rx="10" fill="#1e1e2e" stroke="#45475a"/>
  <rect x="340" y="50" width="180" height="120" rx="10" fill="#1e1e2e" stroke="#45475a"/>
  <text x="130" y="78" fill="#cdd6f4" font-size="13" text-anchor="middle">Agent says</text>
  <text x="130" y="104" fill="#a6e3a1" font-size="16" text-anchor="middle">WS-FRONTDESK</text>
  <text x="430" y="78" fill="#cdd6f4" font-size="13" text-anchor="middle">Server expects</text>
  <text x="430" y="104" fill="#f38ba8" font-size="16" text-anchor="middle">frontdesk-pc</text>
  <line x1="220" y1="120" x2="340" y2="120" stroke="#fab387" stroke-width="2" stroke-dasharray="6 5"/>
  <text x="280" y="112" fill="#fab387" font-size="22" text-anchor="middle">≠</text>
  <text x="280" y="150" fill="#6c7086" font-size="12" text-anchor="middle">no match</text>
  <text x="130" y="140" fill="#6c7086" font-size="11" text-anchor="middle">= hostname</text>
  <text x="430" y="140" fill="#6c7086" font-size="11" text-anchor="middle">= typed at install</text>
  <text x="280" y="200" fill="#cba6f7" font-size="13" text-anchor="middle">identity mismatch -> silent backup failure</text>
</svg>
<figcaption style="color:#6c7086;font-size:14px;margin-top:8px">Two systems, two names, zero backups — and a green light the whole time.</figcaption>
</figure>
<h2 id="the-fix">The fix</h2>
<p>Stop guessing. Ask the machine its own name, on the box itself:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-powershell" data-lang="powershell"><span style="display:flex;"><span><span style="color:#6272a4"># On the workstation — what does the OS actually call itself?</span>
</span></span><span style="display:flex;"><span>hostname
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># Verify what the agent will report</span>
</span></span><span style="display:flex;"><span>[System.Net.Dns]::GetHostName()
</span></span></code></pre></div><p>That gave me <code>WS-FRONTDESK</code>. The server had <code>frontdesk-pc</code>. So I deleted the phantom client, re-registered the machine under its <strong>real</strong> hostname, and pushed the server&rsquo;s preconfigured installer — the one that bakes the correct identity in instead of trusting a human to type it right:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#6272a4"># Remove the dead phantom entry, register the machine by its true hostname,</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># then build the preconfigured client installer the server signs.</span>
</span></span><span style="display:flex;"><span>backup-admin client remove   --name <span style="color:#f1fa8c">&#34;frontdesk-pc&#34;</span>
</span></span><span style="display:flex;"><span>backup-admin client add      --name <span style="color:#f1fa8c">&#34;WS-FRONTDESK&#34;</span>
</span></span><span style="display:flex;"><span>backup-admin installer build --client <span style="color:#f1fa8c">&#34;WS-FRONTDESK&#34;</span> --out ./install-WS-FRONTDESK.exe
</span></span></code></pre></div><p>Ran the signed installer on the box, kicked off a manual run, and watched the server. First full backup completed. Restore point exists. The phantom is gone.</p>
<p>(The other valid fix is to rename the <em>machine</em> to match the registered name — but renaming a production workstation is a bigger blast radius than fixing a backup entry. Match the agent to the host, not the host to a typo.)</p>
<h2 id="why-it-happened">Why it happened</h2>
<p>Someone registered the client by hand and typed a &ldquo;nicer&rdquo; name than the ugly auto-generated hostname. The installer was generic, so identity was never enforced — it trusted the typed name. Two sources of truth, no reconciliation, and a monitoring view that treated &ldquo;client exists and pings&rdquo; as &ldquo;client is protected.&rdquo; Existence isn&rsquo;t protection. A backup you can&rsquo;t restore is just a process eating CPU.</p>
<h2 id="takeaways">Takeaways</h2>
<ul>
<li><strong>Backup identity is the hostname.</strong> The registered client name must equal <code>hostname</code> on the machine — verify it, don&rsquo;t assume it.</li>
<li><strong>A green agent proves nothing.</strong> Online does not mean backing up. The only real green is a recent, restorable recovery point.</li>
<li><strong>Use the server&rsquo;s preconfigured installer.</strong> Hand-typed client names are how phantom clients are born. Bake identity in.</li>
<li><strong>Alert on silence.</strong> Add a &ldquo;no successful backup in N hours&rdquo; check so an empty backup history screams instead of sitting there looking calm.</li>
<li><strong>When two systems disagree about reality, suspect identity first.</strong> Auth, naming, and keys cause more &ldquo;impossible&rdquo; failures than disk or network ever will.</li>
</ul>
]]></content:encoded></item><item><title>The git rm That Deleted 30,000 Files</title><link>https://errorzap.com/posts/the-git-rm-that-deleted-30000-files/</link><pubDate>Wed, 25 Feb 2026 00:00:00 -0700</pubDate><guid>https://errorzap.com/posts/the-git-rm-that-deleted-30000-files/</guid><description>One innocent &lt;code&gt;git rm -r&lt;/code&gt; to untrack a folder, and 30,209 notes vanished off the disk in a single keystroke.</description><content:encoded><![CDATA[<figure style="text-align:center;margin:0 0 30px"><img src="hero.png" alt="The git rm That Deleted 30,000 Files" style="max-width:520px;width:100%;border-radius:14px"/></figure>
<p>A client kept their notes vault in git. Years of markdown. Meeting notes, runbooks, half-finished ideas — the kind of stuff you don&rsquo;t think about until it&rsquo;s gone.</p>
<p>They wanted to stop tracking one big folder. Reasonable. It didn&rsquo;t belong in version control.</p>
<p>So they ran the obvious command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git rm -r attachments/
</span></span></code></pre></div><p>Git chewed on it for a second and printed a wall of <code>rm 'attachments/...'</code> lines. Looked fine. Then <code>git status</code> showed something that made my stomach drop.</p>
<p>It wasn&rsquo;t just the <code>attachments/</code> folder.</p>
<pre tabindex="0"><code>deleted:    notes/2024/standup.md
deleted:    notes/2024/oncall.md
deleted:    runbooks/dns.md
... 30,206 more ...
</code></pre><p>The working tree was empty. <code>ls</code> came back nearly bare. <strong>30,209 markdown files were gone from disk.</strong> Not staged-for-deletion-someday. Gone. Off the filesystem.</p>
<h2 id="the-investigation">The investigation</h2>
<p>The panic version of this story ends with a restore from last night&rsquo;s backup and a lost afternoon. The calm version starts with one question: <em>where does git actually delete from?</em></p>
<p>People think <code>git rm</code> means &ldquo;stop tracking this file.&rdquo; It doesn&rsquo;t. <code>git rm</code> removes a file from <strong>two</strong> places at once: the index (staging area) and the <strong>working tree</strong> — your actual disk.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>┌──────────────┐   ┌──────────────┐   ┌──────────────┐
</span></span><span style="display:flex;"><span>│ Working Tree │   │    Index     │   │   HEAD/Repo  │
</span></span><span style="display:flex;"><span>│  (your disk) │   │  (staging)   │   │ (last commit)│
</span></span><span style="display:flex;"><span>└──────┬───────┘   └──────┬───────┘   └──────┬───────┘
</span></span><span style="display:flex;"><span>       │                  │                  │
</span></span><span style="display:flex;"><span>  git rm ──► ✗ DELETES ──►│ ✗ stages delete  │ ✓ STILL HERE
</span></span><span style="display:flex;"><span>       │                  │                  │
</span></span><span style="display:flex;"><span>  git rm --cached ─────►  │ ✗ untracks only  │ ✓ STILL HERE
</span></span><span style="display:flex;"><span>       ▼                  ▼                  ▼
</span></span><span style="display:flex;"><span>   file gone          marked rm          recoverable
</span></span></code></pre></div><p>That last column is the whole ballgame. <code>git rm</code> touches the working tree and the index, but it does <strong>not</strong> touch your last commit until you actually commit the deletion.</p>
<p>Nobody had committed yet.</p>
<p>The files were sitting safe in <code>HEAD</code> the entire time, even as the disk looked like a crime scene.</p>
<figure style="text-align:center;margin:34px 0">
<svg viewBox="0 0 560 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="git rm deletes the working tree and index, but HEAD still holds every file">
  <rect x="0" y="0" width="560" height="220" fill="#11111b"/>
  <rect x="24" y="40" width="140" height="120" rx="10" fill="#1e1e2e" stroke="#45475a"/>
  <rect x="210" y="40" width="140" height="120" rx="10" fill="#1e1e2e" stroke="#45475a"/>
  <rect x="396" y="40" width="140" height="120" rx="10" fill="#1e1e2e" stroke="#a6e3a1"/>
  <text x="94" y="30" text-anchor="middle" fill="#cdd6f4" font-family="monospace" font-size="13">Working Tree</text>
  <text x="280" y="30" text-anchor="middle" fill="#cdd6f4" font-family="monospace" font-size="13">Index</text>
  <text x="466" y="30" text-anchor="middle" fill="#cdd6f4" font-family="monospace" font-size="13">HEAD (commit)</text>
  <text x="94" y="108" text-anchor="middle" fill="#f38ba8" font-family="monospace" font-size="34">✗</text>
  <text x="280" y="108" text-anchor="middle" fill="#f38ba8" font-family="monospace" font-size="34">✗</text>
  <text x="466" y="108" text-anchor="middle" fill="#a6e3a1" font-family="monospace" font-size="34">✓</text>
  <text x="94" y="138" text-anchor="middle" fill="#6c7086" font-family="monospace" font-size="11">wiped</text>
  <text x="280" y="138" text-anchor="middle" fill="#6c7086" font-family="monospace" font-size="11">staged rm</text>
  <text x="466" y="138" text-anchor="middle" fill="#94e2d5" font-family="monospace" font-size="11">30,209 safe</text>
  <text x="187" y="105" text-anchor="middle" fill="#cba6f7" font-family="monospace" font-size="20">►</text>
  <text x="373" y="105" text-anchor="middle" fill="#fab387" font-family="monospace" font-size="20">►</text>
  <text x="280" y="195" text-anchor="middle" fill="#89b4fa" font-family="monospace" font-size="12">git rm -r <folder>  →  recovery lives in the column on the right</text>
</svg>
<figcaption style="color:#6c7086;font-size:14px;margin-top:8px">The deletion never reached the last commit — that's why this was survivable.</figcaption>
</figure>
<h2 id="the-aha">The aha</h2>
<p>If <code>HEAD</code> still has every file, recovery is just &ldquo;copy them back out of the last commit.&rdquo; No backup tape, no remote clone, no heroics.</p>
<h2 id="the-fix">The fix</h2>
<p>First, confirm the damage scope. Trust nothing, count everything:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git status --porcelain | grep -c <span style="color:#f1fa8c">&#39;^D&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 30209</span>
</span></span></code></pre></div><p>30,209 staged deletions. Matches the missing files exactly. Good — nothing weird, just one bad command.</p>
<p>Now restore the working tree and reset the index straight from <code>HEAD</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git restore --staged --worktree .
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># or, on older git:</span>
</span></span><span style="display:flex;"><span>git checkout HEAD -- .
</span></span></code></pre></div><p>Verify the files actually came back on disk before believing it:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>find . -name <span style="color:#f1fa8c">&#39;*.md&#39;</span> | wc -l
</span></span><span style="display:flex;"><span><span style="color:#6272a4"># 30209</span>
</span></span></code></pre></div><p>All present. <code>git status</code> clean. If <code>HEAD</code> had been corrupt, the same files were still in the remote — <code>git fetch &amp;&amp; git restore --source=origin/main --worktree .</code> would have done it from there.</p>
<p>Then we did the thing they <em>meant</em> to do — untrack without deleting:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git rm -r --cached attachments/
</span></span><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">echo</span> <span style="color:#f1fa8c">&#39;attachments/&#39;</span> &gt;&gt; .gitignore
</span></span><span style="display:flex;"><span>git commit -m <span style="color:#f1fa8c">&#34;Stop tracking attachments/ (keep on disk)&#34;</span>
</span></span></code></pre></div><p><code>--cached</code> removes the folder from the index only. The disk is never touched. That one flag is the entire difference between a config tidy-up and a disaster.</p>
<h2 id="why-it-happened">Why it happened</h2>
<p><code>git rm</code> carries a default that doesn&rsquo;t match most people&rsquo;s mental model. The intent was &ldquo;git, please forget this exists.&rdquo; Git heard &ldquo;delete this from the index <em>and</em> the disk,&rdquo; because that&rsquo;s what it does. The <code>-r</code> made it recursive, and the glob pulled in far more than expected.</p>
<p>There was no malice and no bad command syntax. It did exactly what it&rsquo;s documented to do. The gap was understanding that the working tree is a target, not a bystander.</p>
<h2 id="takeaways">Takeaways</h2>
<ul>
<li><strong><code>git rm</code> deletes from your disk.</strong> Use <code>git rm --cached</code> to untrack a file while keeping it on the filesystem — that&rsquo;s almost always what you actually want.</li>
<li><strong>Count before, count after.</strong> <code>find . -name '*.md' | wc -l</code> on both sides of any bulk git op. A number that drops by 30,209 is not a surprise you want at commit time.</li>
<li><strong>Test on ONE file first.</strong> Run the destructive command against a single path, check <code>git status</code> and <code>ls</code>, <em>then</em> scale up. One file lost is a shrug; thirty thousand is a bad day.</li>
<li><strong>Don&rsquo;t run destructive git in a live data directory without a backup.</strong> A notes vault is data, not just a repo. Treat it like one.</li>
<li><strong>Know your three trees.</strong> Working tree, index, HEAD. Recovery is trivial when you know which one still has your files — and panic when you don&rsquo;t.</li>
</ul>
]]></content:encoded></item><item><title>90s Kid - The Antenna on the Roof</title><link>https://errorzap.com/posts/90s-kid-the-antenna-on-the-roof/</link><pubDate>Tue, 24 Feb 2026 00:00:00 -0700</pubDate><guid>https://errorzap.com/posts/90s-kid-the-antenna-on-the-roof/</guid><description>You remember the metal skeleton on the roof that ruled the channels and feared the wind.</description><content:encoded><![CDATA[<figure style="margin:0 0 28px">
<svg viewBox="0 0 800 240" xmlns="http://www.w3.org/2000/svg" style="width:100%;border-radius:14px">
 <defs>
  <linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#1e1e2e"/><stop offset="1" stop-color="#11111b"/></linearGradient>
  <linearGradient id="sun" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f9e2af"/><stop offset="0.5" stop-color="#fab387"/><stop offset="1" stop-color="#f38ba8"/></linearGradient>
 </defs>
 <rect width="800" height="240" fill="url(#sky)"/>
 <circle cx="640" cy="150" r="74" fill="url(#sun)"/><rect x="566" y="118" width="148" height="4" fill="#11111b"/><rect x="566" y="127" width="148" height="4" fill="#11111b"/><rect x="566" y="136" width="148" height="4" fill="#11111b"/><rect x="566" y="145" width="148" height="4" fill="#11111b"/><rect x="566" y="154" width="148" height="4" fill="#11111b"/><rect x="566" y="163" width="148" height="4" fill="#11111b"/><rect x="566" y="172" width="148" height="4" fill="#11111b"/>
 <line x1="-200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="280" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="360" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="440" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="520" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="600" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="680" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="760" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="840" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="920" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="1000" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="0" y1="178" x2="800" y2="178" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="184" x2="800" y2="184" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="193" x2="800" y2="193" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="206" x2="800" y2="206" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="227" x2="800" y2="227" stroke="#94e2d5" stroke-width="1" opacity="0.22"/>
 <text x="48" y="150" font-size="78">📺</text>
 <text x="150" y="96" font-family="ui-monospace,monospace" font-size="14" fill="#cba6f7" letter-spacing="5">90s KID //</text>
 <text x="150" y="140" font-family="-apple-system,Segoe UI,sans-serif" font-size="30" font-weight="800" fill="#cdd6f4">The Antenna on the Roof</text>
 <rect x="0" y="236" width="800" height="4" fill="#f38ba8"/>
</svg></figure>
<h2 id="the-sky-was-the-cable-box">The Sky Was the Cable Box</h2>
<p>You remember the antenna on the roof.</p>
<p>That big aluminum bone-cage bolted up by the chimney.<br>
Pointing somewhere. Pointing at <strong>something</strong>.<br>
You never knew what.</p>
<p>It just lived up there.<br>
Catching things out of the air.</p>
<hr>
<p>And you knew the rules without anybody teaching you.</p>
<p>When the picture went to static, you didn&rsquo;t call a repairman.<br>
You didn&rsquo;t reboot anything.</p>
<p>You sent a person to <strong>stand in a doorway</strong> and not move.</p>
<p>The vibes:</p>
<ul>
<li>the screen fuzzed and Dad yelled <strong>&ldquo;HOLD IT RIGHT THERE&rdquo;</strong></li>
<li>somebody&rsquo;s arm became part of the TV</li>
<li>a storm was coming and you could feel the channels getting nervous</li>
<li>the wind made the picture <em>breathe</em></li>
<li>adjusting it on the roof was a <strong>forbidden, slightly heroic</strong> act</li>
</ul>
<p>It felt like the antenna was alive.<br>
Like it had moods.<br>
Like it was listening to the sky and sometimes the sky talked back in snow.</p>
<hr>
<p><strong>Here&rsquo;s what it actually was.</strong></p>
<p>That metal skeleton wasn&rsquo;t decoration. It was a <em>receiving array</em> — a careful arrangement of metal rods cut to specific lengths, each one tuned to grab a slice of the radio spectrum where TV stations lived (VHF down low, UHF up high).</p>
<p>The stations broadcast their signal as <strong>electromagnetic waves</strong> from a tower miles away. Those waves washed over your whole neighborhood, invisible. The antenna&rsquo;s job was to let those waves nudge a tiny voltage into the metal — and your TV amplified that whisper into Saturday morning cartoons.</p>
<p>The reason your cousin had to freeze in the doorway? <strong>Multipath.</strong> The signal bounced off hills, buildings, even <em>people</em>, and the bounced copy arrived a hair late, smearing the picture into ghosts and snow. Your cousin&rsquo;s body was a wall. Moving them changed the math. You were, no exaggeration, <strong>hand-tuning a radio-frequency interference pattern</strong> with a human being.</p>
<p>The big rotator boxes that spun the antenna? Those folks were literally re-aiming at a different tower in a different town.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>        |
</span></span><span style="display:flex;"><span>       =|=        &lt;- the little rods caught
</span></span><span style="display:flex;"><span>      ==|==          UHF (the high channels)
</span></span><span style="display:flex;"><span>     ===|===
</span></span><span style="display:flex;"><span>        |
</span></span><span style="display:flex;"><span>   =====|=====   &lt;- the long rods caught
</span></span><span style="display:flex;"><span>        |            VHF (2 through 13)
</span></span><span style="display:flex;"><span>   =====|=====
</span></span><span style="display:flex;"><span>        |
</span></span><span style="display:flex;"><span>        |  ~ ~ ~ ((( signal from the tower )))
</span></span><span style="display:flex;"><span>   _____|_____
</span></span><span style="display:flex;"><span>  /  rooftop  \
</span></span></code></pre></div><hr>
<p>It never really left, you know.</p>
<p>We buried it in coax and called it cable. We beamed it down from dishes. We streamed it over fiber.</p>
<p>But cut the cord today and put up a cheap flat antenna, and the local channels still come down out of the sky — <strong>free, crisp, digital</strong> — same towers, same physics, no human in the doorway.</p>
<p>The sky&rsquo;s still broadcasting.</p>
<p>We just stopped looking up.</p>
]]></content:encoded></item><item><title>90s Kid - The Family Computer in the Living Room</title><link>https://errorzap.com/posts/90s-kid-the-family-computer/</link><pubDate>Thu, 19 Feb 2026 00:00:00 -0700</pubDate><guid>https://errorzap.com/posts/90s-kid-the-family-computer/</guid><description>There was exactly one computer, it lived in the living room, and the whole family had to take turns being kings of it.</description><content:encoded><![CDATA[<figure style="margin:0 0 28px">
<svg viewBox="0 0 800 240" xmlns="http://www.w3.org/2000/svg" style="width:100%;border-radius:14px">
 <defs>
  <linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#1e1e2e"/><stop offset="1" stop-color="#11111b"/></linearGradient>
  <linearGradient id="sun" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f9e2af"/><stop offset="0.5" stop-color="#fab387"/><stop offset="1" stop-color="#f38ba8"/></linearGradient>
 </defs>
 <rect width="800" height="240" fill="url(#sky)"/>
 <circle cx="640" cy="150" r="74" fill="url(#sun)"/><rect x="566" y="118" width="148" height="4" fill="#11111b"/><rect x="566" y="127" width="148" height="4" fill="#11111b"/><rect x="566" y="136" width="148" height="4" fill="#11111b"/><rect x="566" y="145" width="148" height="4" fill="#11111b"/><rect x="566" y="154" width="148" height="4" fill="#11111b"/><rect x="566" y="163" width="148" height="4" fill="#11111b"/><rect x="566" y="172" width="148" height="4" fill="#11111b"/>
 <line x1="-200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="280" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="360" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="440" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="520" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="600" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="680" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="760" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="840" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="920" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="1000" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="0" y1="178" x2="800" y2="178" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="184" x2="800" y2="184" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="193" x2="800" y2="193" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="206" x2="800" y2="206" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="227" x2="800" y2="227" stroke="#94e2d5" stroke-width="1" opacity="0.22"/>
 <text x="48" y="150" font-size="78">🖥</text>
 <text x="150" y="96" font-family="ui-monospace,monospace" font-size="14" fill="#cba6f7" letter-spacing="5">90s KID //</text>
 <text x="150" y="140" font-family="-apple-system,Segoe UI,sans-serif" font-size="30" font-weight="800" fill="#cdd6f4">The Family Computer in the Living </text>
 <rect x="0" y="236" width="800" height="4" fill="#f38ba8"/>
</svg></figure>
<h2 id="one-beige-box-one-family-one-turn-at-a-time">One Beige Box, One Family, One Turn at a Time</h2>
<p>You remember <strong>the family computer.</strong></p>
<p>Not <em>your</em> computer. <em>The</em> computer.</p>
<p>It lived on a special desk in the living room or the den, glowing beige, humming softly, surrounded by a little civilization of mouse pads and floppy disks and a printer that screamed.</p>
<p>There was exactly one.<br>
And you had to <strong>share it.</strong></p>
<hr>
<p>To a kid, that machine was the most important object in the house.</p>
<p>The vibes:</p>
<ul>
<li>A throne. Whoever sat there ruled.</li>
<li>A <strong>forbidden zone</strong> — &ldquo;don&rsquo;t touch that, you&rsquo;ll break it.&rdquo;</li>
<li>A library, an arcade, and a portal, all stacked into one buzzing box.</li>
<li>A timer-based hostage situation: <em>&ldquo;five more minutes, then it&rsquo;s your sister&rsquo;s turn.&rdquo;</em></li>
<li>A thing Dad understood and you did not, which made it slightly magic.</li>
</ul>
<p>You learned to fear the <strong>Recycle Bin</strong> like it was a black hole.<br>
You learned that the screen saver — those flying toasters, that bouncing pipe maze — meant the throne was empty and the kingdom was yours.</p>
<p>And you <em>never</em> turned it off wrong. Turning it off wrong felt like a crime.</p>
<hr>
<p>Here&rsquo;s what it actually was.</p>
<p>That beige box was a personal computer — usually running <strong>Windows 95 or 98</strong> — and the reason your whole family crowded around one was simple: <strong>they were expensive,</strong> and the idea of one-per-person hadn&rsquo;t arrived yet.</p>
<p>Inside: a single-core processor measured in <em>megahertz</em>, maybe <strong>16 to 64 megabytes</strong> of RAM, and a spinning hard drive you could <em>hear</em> thinking. The &ldquo;do not touch&rdquo; energy was real — there was no cloud, no backup, no undo. If you deleted the family photos, they were <strong>gone.</strong> That fear was justified.</p>
<p>And those separate logins? That was Windows quietly inventing the thing your phone does now: <strong>user profiles</strong> — so Mom&rsquo;s desktop, Dad&rsquo;s solitaire stats, and your folder of saved game levels could coexist on one machine without a war.</p>
<p>The CRT monitor was basically a small TV bolted to the front of a vacuum tube. Hence the deep <em>thunk</em> and that whiff of warm static when you powered it on.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>        _________________________
</span></span><span style="display:flex;"><span>       |  _____________________  |
</span></span><span style="display:flex;"><span>       | |                     | |
</span></span><span style="display:flex;"><span>       | |   C:\&gt;              | |
</span></span><span style="display:flex;"><span>       | |   Welcome.          | |
</span></span><span style="display:flex;"><span>       | |   ▓ flying toaster  | |
</span></span><span style="display:flex;"><span>       | |          ✈          | |
</span></span><span style="display:flex;"><span>       | |_____________________| |
</span></span><span style="display:flex;"><span>       |_________________________|
</span></span><span style="display:flex;"><span>       |___[ ]___[====]___[ ]____|
</span></span><span style="display:flex;"><span>          \_______________________\
</span></span><span style="display:flex;"><span>           &#39;-.___________________.-&#39;
</span></span><span style="display:flex;"><span>            [::::::::::::::::::::::]   &lt;- the throne
</span></span></code></pre></div><hr>
<p>The family computer never really died.</p>
<p>It just <strong>multiplied.</strong></p>
<p>The one beige throne split into a laptop each, a phone each, a tablet on the couch. The shared machine became the personal device, and &ldquo;wait your turn&rdquo; became &ldquo;go to your room and use your own.&rdquo;</p>
<p>But every time a family streams one movie on one big screen, gathered shoulder-to-shoulder, fighting over the remote?</p>
<p>That&rsquo;s the <strong>living-room computer</strong>, reincarnated.</p>
<p>Same throne.<br>
Same five-more-minutes.<br>
Same warm hum of the one good screen.</p>
]]></content:encoded></item><item><title>The Settings API That Corrupted Every Setting at Once</title><link>https://errorzap.com/posts/the-settings-api-that-corrupted-every-setting/</link><pubDate>Thu, 19 Feb 2026 00:00:00 -0700</pubDate><guid>https://errorzap.com/posts/the-settings-api-that-corrupted-every-setting/</guid><description>I changed one backup-retention field, ran it twice, and every client&amp;rsquo;s settings ate themselves like a snake swallowing its own tail.</description><content:encoded><![CDATA[<figure style="text-align:center;margin:0 0 30px"><img src="hero.png" alt="The Settings API That Corrupted Every Setting at Once" style="max-width:520px;width:100%;border-radius:14px"/></figure>
<p>It was supposed to be a five-minute job.</p>
<p>A client&rsquo;s backup server needed one field bumped on a handful of backup clients. Retention. One value. I had a Python wrapper sitting right there with a method named exactly what I wanted: <code>change_client_setting()</code>. Tab-complete practically begged me to use it.</p>
<p>So I looped over the clients and called it. Twice, because I tweaked a second field on the second pass.</p>
<p>Then the dashboard started showing clients with no backup schedule, no retention, no paths — like they&rsquo;d never been configured. Not one client. <strong>All</strong> of them.</p>
<h2 id="the-investigation">The investigation</h2>
<p>First instinct: did I fat-finger the loop? No. The script was boring. Read a name, set a field, move on.</p>
<p>Second instinct: did the service choke? Logs were clean. The server was happily <em>applying</em> the garbage I&rsquo;d handed it.</p>
<p>So I pulled the raw settings blob the API was actually POSTing. And there it was — the settings object, wrapped inside another settings object, wrapped inside <em>another</em> one. A JSON turducken.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>┌─────────────────────────────────────────────┐
</span></span><span style="display:flex;"><span>│ POST #1   { retention: 30, schedule: ... }   │  ✓ looks fine
</span></span><span style="display:flex;"><span>├─────────────────────────────────────────────┤
</span></span><span style="display:flex;"><span>│ POST #2   { settings: {                      │
</span></span><span style="display:flex;"><span>│              settings: {                      │
</span></span><span style="display:flex;"><span>│                retention: 30, schedule: ...   │  ✗ nested once
</span></span><span style="display:flex;"><span>│              } } }                            │
</span></span><span style="display:flex;"><span>├─────────────────────────────────────────────┤
</span></span><span style="display:flex;"><span>│ POST #3   { settings: { settings: {          │
</span></span><span style="display:flex;"><span>│              settings: { ...buried... } } } } │  ✗ nested twice → unreadable
</span></span><span style="display:flex;"><span>└─────────────────────────────────────────────┘
</span></span><span style="display:flex;"><span>                       ▼
</span></span><span style="display:flex;"><span>        server reads top level → finds nothing it recognizes
</span></span><span style="display:flex;"><span>        every real field is now three layers down → effectively gone
</span></span></code></pre></div><h2 id="the-aha">The &ldquo;aha&rdquo;</h2>
<p>Here&rsquo;s the part that made my stomach drop.</p>
<p><code>change_client_setting()</code> does <strong>not</strong> set one field. It round-trips the <em>entire</em> settings object: GET the whole blob, mutate the one key in memory, then re-serialize and POST the whole thing back.</p>
<p>That&rsquo;s already more blast radius than the name implies. But the killer was the encoder. Its re-serialization had a recursive-nesting bug — each round-trip tacked the existing object <em>inside</em> a fresh <code>settings</code> wrapper instead of replacing it.</p>
<p>One call: subtly wrong but survivable. Two calls: the real fields are buried deep enough that the server can&rsquo;t find them anymore. The defaults win. Every client looks wiped.</p>
<p>I didn&rsquo;t corrupt one setting. I corrupted the <em>container</em> every setting lives in — for every client — by changing a single number.</p>
<h2 id="the-fix">The fix</h2>
<p>Stop trusting the convenience method. Write a minimal-POST helper that submits <strong>only</strong> the field that changed, never the whole object — so there&rsquo;s no full blob to re-encode and no encoder to trip over.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#ff79c6">def</span> <span style="color:#50fa7b">safe_set_client_setting</span>(server, client_id, key, value, dry_run<span style="color:#ff79c6">=</span><span style="color:#ff79c6">False</span>):
</span></span><span style="display:flex;"><span>    <span style="color:#6272a4"># Snapshot the current settings BEFORE touching anything.</span>
</span></span><span style="display:flex;"><span>    current <span style="color:#ff79c6">=</span> server<span style="color:#ff79c6">.</span>get_client_settings(client_id)
</span></span><span style="display:flex;"><span>    snapshot_path <span style="color:#ff79c6">=</span> <span style="color:#f1fa8c">f</span><span style="color:#f1fa8c">&#34;snapshots/</span><span style="color:#f1fa8c">{</span>client_id<span style="color:#f1fa8c">}</span><span style="color:#f1fa8c">-</span><span style="color:#f1fa8c">{</span><span style="color:#8be9fd;font-style:italic">int</span>(time<span style="color:#ff79c6">.</span>time())<span style="color:#f1fa8c">}</span><span style="color:#f1fa8c">.json&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">with</span> <span style="color:#8be9fd;font-style:italic">open</span>(snapshot_path, <span style="color:#f1fa8c">&#34;w&#34;</span>) <span style="color:#ff79c6">as</span> f:
</span></span><span style="display:flex;"><span>        json<span style="color:#ff79c6">.</span>dump(current, f, indent<span style="color:#ff79c6">=</span><span style="color:#bd93f9">2</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">if</span> dry_run:
</span></span><span style="display:flex;"><span>        <span style="color:#8be9fd;font-style:italic">print</span>(<span style="color:#f1fa8c">f</span><span style="color:#f1fa8c">&#34;[dry-run] </span><span style="color:#f1fa8c">{</span>client_id<span style="color:#f1fa8c">}</span><span style="color:#f1fa8c">: </span><span style="color:#f1fa8c">{</span>key<span style="color:#f1fa8c">}</span><span style="color:#f1fa8c"> </span><span style="color:#f1fa8c">{</span>current<span style="color:#ff79c6">.</span>get(key)<span style="color:#f1fa8c">!r}</span><span style="color:#f1fa8c"> -&gt; </span><span style="color:#f1fa8c">{</span>value<span style="color:#f1fa8c">!r}</span><span style="color:#f1fa8c">&#34;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#ff79c6">return</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#6272a4"># Minimal POST: ONLY the one field. No round-trip of the whole blob.</span>
</span></span><span style="display:flex;"><span>    server<span style="color:#ff79c6">.</span>post_setting(client_id, {key: value})
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#6272a4"># Read-back validation. Trust nothing.</span>
</span></span><span style="display:flex;"><span>    after <span style="color:#ff79c6">=</span> server<span style="color:#ff79c6">.</span>get_client_settings(client_id)
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">if</span> <span style="color:#8be9fd;font-style:italic">str</span>(after<span style="color:#ff79c6">.</span>get(key)) <span style="color:#ff79c6">!=</span> <span style="color:#8be9fd;font-style:italic">str</span>(value):
</span></span><span style="display:flex;"><span>        <span style="color:#ff79c6">raise</span> RuntimeError(
</span></span><span style="display:flex;"><span>            <span style="color:#f1fa8c">f</span><span style="color:#f1fa8c">&#34;VALIDATION FAILED </span><span style="color:#f1fa8c">{</span>client_id<span style="color:#f1fa8c">}</span><span style="color:#f1fa8c">: </span><span style="color:#f1fa8c">{</span>key<span style="color:#f1fa8c">}</span><span style="color:#f1fa8c"> is </span><span style="color:#f1fa8c">{</span>after<span style="color:#ff79c6">.</span>get(key)<span style="color:#f1fa8c">!r}</span><span style="color:#f1fa8c">, &#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f1fa8c">f</span><span style="color:#f1fa8c">&#34;expected </span><span style="color:#f1fa8c">{</span>value<span style="color:#f1fa8c">!r}</span><span style="color:#f1fa8c"> — restore from </span><span style="color:#f1fa8c">{</span>snapshot_path<span style="color:#f1fa8c">}</span><span style="color:#f1fa8c">&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#8be9fd;font-style:italic">print</span>(<span style="color:#f1fa8c">f</span><span style="color:#f1fa8c">&#34;✓ </span><span style="color:#f1fa8c">{</span>client_id<span style="color:#f1fa8c">}</span><span style="color:#f1fa8c">: </span><span style="color:#f1fa8c">{</span>key<span style="color:#f1fa8c">}</span><span style="color:#f1fa8c"> = </span><span style="color:#f1fa8c">{</span>value<span style="color:#f1fa8c">}</span><span style="color:#f1fa8c">  (snapshot </span><span style="color:#f1fa8c">{</span>snapshot_path<span style="color:#f1fa8c">}</span><span style="color:#f1fa8c">)&#34;</span>)
</span></span></code></pre></div><p>Recovery itself was unglamorous — restore each client from the JSON snapshot I now wish I&rsquo;d had <em>before</em> the first run:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#ff79c6">for</span> f in snapshots/*-pre-incident.json; <span style="color:#ff79c6">do</span>
</span></span><span style="display:flex;"><span>  <span style="color:#8be9fd;font-style:italic">id</span><span style="color:#ff79c6">=</span><span style="color:#ff79c6">$(</span>basename <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$f</span><span style="color:#f1fa8c">&#34;</span> | cut -d- -f1<span style="color:#ff79c6">)</span>
</span></span><span style="display:flex;"><span>  python restore_settings.py --client <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$id</span><span style="color:#f1fa8c">&#34;</span> --from <span style="color:#f1fa8c">&#34;</span><span style="color:#8be9fd;font-style:italic">$f</span><span style="color:#f1fa8c">&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">done</span>
</span></span></code></pre></div><p>Four guardrails came out of this, and they&rsquo;re now non-negotiable on that box:</p>
<figure style="text-align:center;margin:34px 0">
<svg viewBox="0 0 560 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Safe write pipeline">
  <rect x="0" y="0" width="560" height="220" fill="#11111b" rx="10"/>
  <text x="280" y="30" fill="#cdd6f4" font-family="monospace" font-size="15" text-anchor="middle">safe_set_client_setting()</text>
  <rect x="20" y="55" width="120" height="56" rx="8" fill="#1e1e2e" stroke="#45475a"/>
  <text x="80" y="80" fill="#fab387" font-family="monospace" font-size="12" text-anchor="middle">snapshot</text>
  <text x="80" y="98" fill="#6c7086" font-family="monospace" font-size="10" text-anchor="middle">save .json</text>
  <rect x="160" y="55" width="120" height="56" rx="8" fill="#1e1e2e" stroke="#45475a"/>
  <text x="220" y="80" fill="#cba6f7" font-family="monospace" font-size="12" text-anchor="middle">dry-run?</text>
  <text x="220" y="98" fill="#6c7086" font-family="monospace" font-size="10" text-anchor="middle">preview diff</text>
  <rect x="300" y="55" width="120" height="56" rx="8" fill="#1e1e2e" stroke="#89b4fa"/>
  <text x="360" y="80" fill="#89b4fa" font-family="monospace" font-size="12" text-anchor="middle">minimal POST</text>
  <text x="360" y="98" fill="#6c7086" font-family="monospace" font-size="10" text-anchor="middle">one field only</text>
  <rect x="440" y="55" width="100" height="56" rx="8" fill="#1e1e2e" stroke="#a6e3a1"/>
  <text x="490" y="80" fill="#a6e3a1" font-family="monospace" font-size="12" text-anchor="middle">read-back</text>
  <text x="490" y="98" fill="#6c7086" font-family="monospace" font-size="10" text-anchor="middle">verify ✓</text>
  <line x1="140" y1="83" x2="158" y2="83" stroke="#6c7086" stroke-width="2"/>
  <line x1="280" y1="83" x2="298" y2="83" stroke="#6c7086" stroke-width="2"/>
  <line x1="420" y1="83" x2="438" y2="83" stroke="#6c7086" stroke-width="2"/>
<p><text x="280" y="160" fill="#f38ba8" font-family="monospace" font-size="13" text-anchor="middle">old path: GET whole blob → re-encode → POST whole blob</text>
<text x="280" y="185" fill="#94e2d5" font-family="monospace" font-size="13" text-anchor="middle">new path: never hand the encoder the full object</text>
</svg></p>
<figcaption style="color:#6c7086;font-size:14px;margin-top:8px">Snapshot → dry-run → minimal write → read-back. Every mutation, every time.</figcaption>
</figure>
<h2 id="why-it-happened">Why it happened</h2>
<p>The method name lied — or rather, I read a name and assumed a behavior. &ldquo;Change one client setting&rdquo; sounded surgical. Under the hood it was a full read-modify-write of a complex object, handed to a serializer nobody had stress-tested across repeated calls.</p>
<p>A buggy encoder on a single-field write is annoying. A buggy encoder on a <em>whole-object round-trip</em> is a data-integrity bomb, because every write re-touches every field. The bug didn&rsquo;t need to corrupt the value I changed. It corrupted the structure wrapping everything.</p>
<p>And I had no snapshot, no read-back, and no dry-run — so the first time I learned any of this was when the dashboard went blank.</p>
<h2 id="takeaways">Takeaways</h2>
<ul>
<li><strong>Read what the API actually sends, not what the method is named.</strong> &ldquo;Set one field&rdquo; frequently means &ldquo;GET, mutate, re-serialize, POST everything.&rdquo; That round-trip is your real blast radius.</li>
<li><strong>Prefer minimal, targeted writes.</strong> If you can POST just the changed key, do it — you never hand a fragile encoder the whole object to mangle.</li>
<li><strong>Validate by reading back.</strong> A write that &ldquo;succeeded&rdquo; tells you the server accepted bytes, not that the right value landed. Read it again and compare.</li>
<li><strong>Snapshot before you mutate.</strong> A pre-write JSON dump turns a catastrophe into a one-line restore. It costs milliseconds and buys you the whole night back.</li>
<li><strong>Ship a <code>--dry-run</code> for anything that touches production config.</strong> Seeing the diff before it&rsquo;s real would have caught the nesting on call number one.</li>
</ul>
]]></content:encoded></item><item><title>90s Kid - The Sound of Dial-Up</title><link>https://errorzap.com/posts/90s-kid-the-sound-of-dial-up/</link><pubDate>Sat, 14 Feb 2026 00:00:00 -0700</pubDate><guid>https://errorzap.com/posts/90s-kid-the-sound-of-dial-up/</guid><description>You remember the song the internet sang before it would let you in.</description><content:encoded><![CDATA[<figure style="margin:0 0 28px">
<svg viewBox="0 0 800 240" xmlns="http://www.w3.org/2000/svg" style="width:100%;border-radius:14px">
 <defs>
  <linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#1e1e2e"/><stop offset="1" stop-color="#11111b"/></linearGradient>
  <linearGradient id="sun" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f9e2af"/><stop offset="0.5" stop-color="#fab387"/><stop offset="1" stop-color="#f38ba8"/></linearGradient>
 </defs>
 <rect width="800" height="240" fill="url(#sky)"/>
 <circle cx="640" cy="150" r="74" fill="url(#sun)"/><rect x="566" y="118" width="148" height="4" fill="#11111b"/><rect x="566" y="127" width="148" height="4" fill="#11111b"/><rect x="566" y="136" width="148" height="4" fill="#11111b"/><rect x="566" y="145" width="148" height="4" fill="#11111b"/><rect x="566" y="154" width="148" height="4" fill="#11111b"/><rect x="566" y="163" width="148" height="4" fill="#11111b"/><rect x="566" y="172" width="148" height="4" fill="#11111b"/>
 <line x1="-200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="-40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="40" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="120" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="200" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="280" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="360" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="440" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="520" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="600" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="680" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="760" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="840" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="920" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="1000" y1="240" x2="400" y2="176" stroke="#cba6f7" stroke-width="1" opacity="0.25"/><line x1="0" y1="178" x2="800" y2="178" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="184" x2="800" y2="184" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="193" x2="800" y2="193" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="206" x2="800" y2="206" stroke="#94e2d5" stroke-width="1" opacity="0.22"/><line x1="0" y1="227" x2="800" y2="227" stroke="#94e2d5" stroke-width="1" opacity="0.22"/>
 <text x="48" y="150" font-size="78">📞</text>
 <text x="150" y="96" font-family="ui-monospace,monospace" font-size="14" fill="#cba6f7" letter-spacing="5">90s KID //</text>
 <text x="150" y="140" font-family="-apple-system,Segoe UI,sans-serif" font-size="30" font-weight="800" fill="#cdd6f4">The Sound of Dial-Up</text>
 <rect x="0" y="236" width="800" height="4" fill="#f38ba8"/>
</svg></figure>
<h2 id="the-song-the-internet-sang-before-it-let-you-in">The Song the Internet Sang Before It Let You In</h2>
<p>You remember the sound.</p>
<p>That screech.<br>
That hiss.<br>
That little robot choir having a full-blown meltdown inside the beige box next to the family computer.</p>
<p>You&rsquo;d double-click the little globe icon.<br>
And then you&rsquo;d <strong>wait</strong>.</p>
<p>First the dial tone. Then the <em>boop-beep-boop</em> of a phone number being typed by a ghost. Then&hellip; the scream.</p>
<p>We had no idea what it was.<br>
We just knew it meant <strong>the internet was coming</strong>.</p>
<hr>
<p>The kid&rsquo;s-eye view of dial-up was pure superstition. The vibes:</p>
<ul>
<li><strong>You could not touch the phone.</strong> Pick up the kitchen handset and you&rsquo;d <em>kill the internet</em>. This was law. Mom yelling &ldquo;ARE YOU ONLINE?!&rdquo; up the stairs.</li>
<li>The noise felt like a <strong>password the computer whispered to the sky</strong>.</li>
<li>If it connected on the first try, that was a <strong>good omen</strong>. The day would go well.</li>
<li>&ldquo;Busy signal&rdquo; meant the internet was simply&hellip; full. Try again later.</li>
<li>It was forbidden, expensive, and a little bit magic. You were <em>visiting</em> the internet, not living in it.</li>
</ul>
<p>You&rsquo;d sit there hoping it wouldn&rsquo;t say <em>that other sound</em> — the sad descending <em>bweeeoooo</em> of a failed handshake. The dial-up walk of shame.</p>
<hr>
<p>So what WAS that noise, really?</p>
<p>Here&rsquo;s the nerdy truth: that screech was two modems <strong>negotiating</strong> out loud.</p>
<p>Your modem and the one at the ISP were on a <em>phone line</em> — a thing built for human voices, not data. So they had to figure out, in real time, how to fake it. The handshake went like this:</p>
<ul>
<li>A tone goes out: <strong>&ldquo;Hi, I&rsquo;m a modem, anyone home?&rdquo;</strong></li>
<li>The other side answers with its own tone.</li>
<li>Then that wall of static? That&rsquo;s both modems <strong>probing the line</strong> — testing how noisy it was, what frequencies survived the trip, how fast they could safely talk.</li>
<li>They agreed on a speed (the holy <strong>56k</strong>), did an echo-cancellation handshake, and went quiet.</li>
</ul>
<p>The screaming was the <em>only</em> part you were meant to hear. Once they shook hands, the modem muted itself. Silence meant <strong>you were in</strong>.</p>
<p>You were literally eavesdropping on two machines learning to understand each other.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>   ___________________________
</span></span><span style="display:flex;"><span>  |  .---------------------.  |
</span></span><span style="display:flex;"><span>  |  |  CONNECTING...      |  |
</span></span><span style="display:flex;"><span>  |  |                     |  |
</span></span><span style="display:flex;"><span>  |  |  bzzt  kshhhh  PING |  |
</span></span><span style="display:flex;"><span>  |  |  /\  /\/\    /\  /\  |  |
</span></span><span style="display:flex;"><span>  |  | /  \/    \/\/  \/  \ |  |
</span></span><span style="display:flex;"><span>  |  &#39;---------------------&#39;  |
</span></span><span style="display:flex;"><span>  |   [::::]  56k  [ MODEM ]  |
</span></span><span style="display:flex;"><span>  |___________________________|
</span></span><span style="display:flex;"><span>        |  |          |  |
</span></span><span style="display:flex;"><span>     ===(  )==phone==(  )===
</span></span></code></pre></div><p>It never really left, you know.</p>
<p>The handshake just got <em>quiet</em>. Your phone still negotiates with a cell tower every time it wakes up — speed, frequency, signal quality — the exact same dance. Your Wi-Fi does it. Your laptop does it a thousand times a day. Every device you own is <strong>still screaming that song</strong>.</p>
<p>It just learned to do it under its breath.</p>
<p>We were the last kids who got to <em>hear</em> the internet think.<br>
And honestly?</p>
<p>It sounded like the future.</p>
]]></content:encoded></item><item><title>90s Kid - The Box on the Side of the House</title><link>https://errorzap.com/posts/90s-kid-box-on-side-of-house/</link><pubDate>Wed, 11 Feb 2026 00:57:00 -0700</pubDate><guid>https://errorzap.com/posts/90s-kid-box-on-side-of-house/</guid><description>Every 90s house had a boring gray box bolted to the siding marked DO NOT OPEN. Turns out it still runs your whole digital life.</description><content:encoded><![CDATA[<h2 id="the-box-on-the-side-of-the-house">The Box on the Side of the House</h2>
<p><img loading="lazy" src="/posts/90s-kid-box-on-side-of-house/img1.png"></p>
<p>If you grew up in the 90s, you probably remember <strong>the box</strong>.</p>
<p>Every house had one.<br>
Gray. Boring. Bolted to the siding like it was issued by the government.<br>
Usually with a sticker that said <strong>DO NOT OPEN</strong>, which of course meant <em>this is extremely important</em>.</p>
<p>As kids, we didn’t know what it did.<br>
We just knew:</p>
<ul>
<li>It wasn’t ours</li>
<li>Adults avoided explaining it</li>
<li>And touching it felt like a crime</li>
</ul>
<p>So we stared at it.<br>
Respectfully. Suspiciously. Like it might bite.</p>
<p>That box is called a <em>demarcation box</em>. But you didn’t need to know that then. What mattered was the vibe: <strong>this thing runs something big</strong>.</p>
<p>Years later, you finally see a technician open it. Inside?<br>
Wires. Labels. Splitters. Grounding blocks.<br>
Not magic. Just infrastructure.</p>
<p>And here’s the punchline:</p>
<p>That same boring gray box still runs your life.</p>
<p>Your internet comes through it.<br>
Your phone signal passes through it.<br>
Your work, your streaming, your security cameras, your “why is the Wi-Fi down” moments — all of it depends on that ugly little rectangle.</p>
<p>The cartoon isn’t about nostalgia for old tech.<br>
It’s about remembering a time when <strong>infrastructure was visible</strong>, mysterious, and slightly scary.</p>
<p>We didn’t understand it.<br>
But we knew it mattered.</p>
<p>And the funniest part?<br>
The box never went away.<br>
We just finally learned its name.</p>
]]></content:encoded></item><item><title>I’m Not Old — I Watched the Internet Get Installed</title><link>https://errorzap.com/posts/watching-birth-of-internet-before-we/</link><pubDate>Tue, 10 Feb 2026 15:05:07 -0700</pubDate><guid>https://errorzap.com/posts/watching-birth-of-internet-before-we/</guid><description>A 90s-kid origin story: backhoes, giant coax, and the future services that turned into broadband.</description><content:encoded><![CDATA[<p>90s kid memories • cable internet origin story</p>
<h1 id="im-not-old--i-watched-the-internet-get-installed">I’m Not Old — I Watched the Internet Get Installed</h1>
<p>Backhoes, giant coax, and “future services” that turned into broadband.</p>
<p><img loading="lazy" src="/posts/watching-birth-of-internet-before-we/img1.png"></p>
<p><img loading="lazy" src="/posts/watching-birth-of-internet-before-we/img2.jpg"></p>
<p><img loading="lazy" src="/posts/watching-birth-of-internet-before-we/img3.png"></p>
<p>One of my most specific “90s kid” memories isn’t a TV show or a toy — it’s standing outside watching the cable company tear up the neighborhood.
I remember the <strong>trencher</strong>, the fresh dirt, the orange cones, and those giant spools of coax that looked way too serious for “just TV.”
It was the kind of infrastructure that made a kid stop and stare because it felt like something important was happening.</p>
<p>I was told back then it was for internet — for “the future.” The crew answers were always vague, like they were trained to say the same line:</p>
<blockquote>
<p>“Future services.”</p>
</blockquote>
<p>At the time, that phrase sounded like marketing fog. But a few years later, cable modems showed up and the fog cleared:
that trenching was the physical foundation of broadband. The internet didn’t magically appear — it got <em>installed</em>.</p>
<hr>
<h2 id="what-was-actually-happening-the-non-marketing-version">What Was Actually Happening (The Non-Marketing Version)</h2>
<p>Cable TV networks were originally designed as a one-way broadcast system: signals go out, everyone receives them.
Internet requires two-way traffic — your computer has to talk back. So in the 90s, cable companies started upgrading their “plant”
so it could handle both directions and higher quality signals.</p>
<p>🧩 Quick mental model: Hybrid Fiber-Coax (HFC)</p>
<p>Headend</p>
<p>Cable company core</p>
<p>➜</p>
<p>Fiber trunk</p>
<p>Light to the neighborhood</p>
<p>➜</p>
<p>Optical node</p>
<p>Light → RF</p>
<p>➜</p>
<p>Coax plant</p>
<p>Copper to houses</p>
<p>Translation: most neighborhoods got <strong>fiber to a local node</strong>, then <strong>coax the rest of the way</strong>.
The big cable you saw trenched was likely part of that coax distribution rebuild — the “last mile” TV infrastructure becoming a data network.</p>
<h2 id="how-early-cable-modems-worked-in-human-language">How Early Cable Modems Worked (In Human Language)</h2>
<p><img loading="lazy" src="/posts/watching-birth-of-internet-before-we/img4.jpg"></p>
<p>Early cable modems weren’t just “Ethernet devices.” They were more like a TV tuner and a radio modem combined.
The network used <strong>RF channels</strong> (the same kind of spectrum used for cable TV), and everyone in the neighborhood shared it.</p>
<p>📥 Downstream (download)</p>
<ul>
<li>High frequency RF channel(s)</li>
<li>Everyone listens</li>
<li>Your modem filters what’s “for you”</li>
<li>Fast compared to upstream</li>
</ul>
<p>📤 Upstream (upload)</p>
<ul>
<li>Lower frequencies (noisier)</li>
<li>Shared “talk-back” channel</li>
<li>Modems take turns transmitting</li>
<li>Historically the weak link</li>
</ul>
<p>In the late 90s, the industry standardized around <strong>DOCSIS</strong> (the CableLabs spec that made cable modems interoperable).
That’s when cable internet stopped being a chaotic science project and became a scalable product: authentication, provisioning, and managed speeds.</p>
<hr>
<h2 id="the-90s-kid-part-we-saw-the-seam">The 90s Kid Part: We Saw the Seam</h2>
<p>What makes this feel so “90s kid” isn’t just the tech — it’s that we watched the transition happen in public.
Infrastructure wasn’t hidden. Progress had a soundtrack: trucks, compressors, trenchers, and the constant beep-beep of heavy equipment reversing.</p>
<p>📼 90s kid sensory checklist (if you know, you know)</p>
<ul>
<li>Standing outside watching crews work because it was actually interesting</li>
<li>Everyone asking “what are they putting in?” and getting vague answers</li>
<li>Dial-up sounds inside the house, construction sounds outside</li>
<li>Then one day: always-on internet, like a cheat code</li>
</ul>
<p>And the part that’s equal parts funny and existential: a lot of that buried coax is still there.
It might be reused, re-terminated, amplified differently, or partially replaced — but the “future services” foundation is still under the street.</p>
<h2 id="so-am-i-old">So… Am I Old?</h2>
<p>Maybe a little. But mostly I’m just <strong>time-privileged</strong>.
I didn’t read about broadband arriving — I watched it get installed.
The internet wasn’t a cloud. It was dirt, cable, and a crew saying “future services” like a prophecy.</p>
<p>Written by someone who remembers when the internet smelled like hot asphalt and fresh trench dirt.</p>
]]></content:encoded></item><item><title>OpenClaw Smart AI Model routing to reduce cost</title><link>https://errorzap.com/posts/openclaw-smart-ai-model-routing-to/</link><pubDate>Wed, 04 Feb 2026 21:29:00 -0700</pubDate><guid>https://errorzap.com/posts/openclaw-smart-ai-model-routing-to/</guid><description>Stop Wasting API Calls:A Practical Guide to Multi-Tier AI Model Systems How to run 24/7 AI automation without burning through your quota or your budget ⚡ W</description><content:encoded><![CDATA[<h1 id="stop-wasting-api-calls-a-practical-guide-to-multi-tier-ai-model-systems">Stop Wasting API Calls: A Practical Guide to Multi-Tier AI Model Systems</h1>
<p>How to run 24/7 AI automation without burning through your quota or your budget</p>
<p><strong>⚡ Want the detailed technical implementation?</strong><br>
This post covers the concepts and benefits. For the full technical guide with code examples, configuration files, and monitoring setup, scroll down to the <a href="#complete-implementation-guide-building-your-tiered-ai-system">Complete Implementation Guide</a> section below.</p>
<h2 id="the-problem-using-premium-models-for-everything">The Problem: Using Premium Models for Everything</h2>
<p>If you&rsquo;re running AI automation—whether it&rsquo;s a personal assistant, business monitoring, or development tools—you&rsquo;ve probably noticed your API usage climbing fast.</p>
<p>Here&rsquo;s what most people do wrong: <strong>they pick their favorite model and use it for everything.</strong></p>
<ul>
<li>&ldquo;I like Claude Sonnet, so that&rsquo;s what I use&rdquo;</li>
<li>&ldquo;GPT-4 is the best, I&rsquo;ll just use that&rdquo;</li>
<li>&ldquo;Gemini Pro is good enough for most things&rdquo;</li>
</ul>
<p><strong>This approach has two big problems:</strong></p>
<ol>
<li>You burn through API quotas unnecessarily</li>
<li>You pay more than you need to</li>
</ol>
<p>The reality is that most AI tasks don&rsquo;t require premium models. You&rsquo;re using a sledgehammer to crack walnuts.</p>
<h2 id="the-solution-match-model-cost-to-task-complexity">The Solution: Match Model Cost to Task Complexity</h2>
<p>Think of AI models like tools in a workshop. You wouldn&rsquo;t use a precision laser cutter to cut plywood. You use:</p>
<ul>
<li><strong>Cheap tools</strong> for simple, repetitive tasks</li>
<li><strong>Balanced tools</strong> for everyday work</li>
<li><strong>Premium tools</strong> only when you actually need the capability</li>
</ul>
<p>The same logic applies to AI models.</p>
<h2 id="the-3-tier-system">The 3-Tier System</h2>
<p><img loading="lazy" src="/posts/openclaw-smart-ai-model-routing-to/img1.png"></p>
<h3 id="-tier-1-background-workers-cheap--fast">💵 Tier 1: Background Workers (Cheap &amp; Fast)</h3>
<p><strong>Models:</strong> Gemini Flash, Claude Haiku, DeepSeek V3<br>
<strong>Cost:</strong> $0.10 - $0.50 per million tokens<br>
<strong>Speed:</strong> Very fast responses</p>
<p><strong>Use for:</strong></p>
<ul>
<li>Scheduled tasks and cron jobs</li>
<li>File operations (move, copy, rename, organize)</li>
<li>Simple monitoring (is X up? did Y complete?)</li>
<li>Data extraction from logs or files</li>
<li>Basic yes/no questions</li>
<li>Heartbeat checks</li>
</ul>
<p><strong>Example:</strong><br>
<strong>Task:</strong> Check if server 192.168.1.100 responds to ping<br>
<strong>Tier 1 Response:</strong> &ldquo;Server is up. Response: 12ms&rdquo;<br>
<strong>Cost:</strong> ~$0.0001</p>
<p><strong>Why it works:</strong> These tasks don&rsquo;t require reasoning, creativity, or complex understanding. They&rsquo;re simple data retrieval or yes/no answers. Cheap models handle them perfectly.</p>
<h3 id="-tier-2-daily-driver-balanced">🧠 Tier 2: Daily Driver (Balanced)</h3>
<p><strong>Models:</strong> Claude Sonnet, Gemini Pro, GPT-4o<br>
<strong>Cost:</strong> $3 - $15 per million tokens<br>
<strong>Speed:</strong> Good balance</p>
<p><strong>Use for:</strong></p>
<ul>
<li>All normal chat conversations (your default)</li>
<li>Code writing and review</li>
<li>Research and analysis</li>
<li>Documentation</li>
<li>Email composition</li>
<li>Most technical troubleshooting</li>
<li>Content creation</li>
</ul>
<p><strong>Example:</strong><br>
<strong>Task:</strong> Summarize 50 emails and prioritize urgent ones<br>
<strong>Tier 2 Response:</strong> Detailed summary with context and priorities<br>
<strong>Cost:</strong> ~$0.05-0.10</p>
<p><strong>Why it works:</strong> Tier 2 models are smart enough for 90% of what you&rsquo;ll throw at them. They understand context, can reason through problems, and write quality content. This should be your default for anything interactive.</p>
<h3 id="-tier-3-the-heavy-hitters-premium">🚀 Tier 3: The Heavy Hitters (Premium)</h3>
<p><strong>Models:</strong> Claude Opus, GPT-4 (full)<br>
<strong>Cost:</strong> $15 - $75 per million tokens<br>
<strong>Speed:</strong> Slower, but most capable</p>
<p><strong>Use for:</strong></p>
<ul>
<li>Complex architecture decisions</li>
<li>Multi-step reasoning with many variables</li>
<li>Novel problem-solving (no clear solution path)</li>
<li>When Tier 2 has tried and failed multiple times</li>
<li>High-stakes content (legal, financial, critical business decisions)</li>
</ul>
<p><strong>Example:</strong><br>
<strong>Task:</strong> Debug a subtle async race condition in distributed system<br>
<strong>After:</strong> Tier 2 tried 3 approaches and failed<br>
<strong>Tier 3 Response:</strong> Identified timing issue with detailed trace<br>
<strong>Cost:</strong> ~$2-5<br>
<strong>Worth it:</strong> Saved 4-6 hours of manual debugging</p>
<p><strong>Why it works:</strong> Tier 3 models have the best reasoning capabilities. But you only need that extra power occasionally. Use it strategically, not by default.</p>
<p><img loading="lazy" src="/posts/openclaw-smart-ai-model-routing-to/img2.png"></p>
<h2 id="real-example-daily-automation-workflow">Real Example: Daily Automation Workflow</h2>
<p>Here&rsquo;s how a typical day breaks down:</p>
<h4 id="600-am---morning-checks-tier-1">6:00 AM - Morning Checks (Tier 1)</h4>
<ul>
<li>Check server status: 5 servers</li>
<li>Count unread emails</li>
<li>Review calendar</li>
<li>Check backup completion</li>
</ul>
<p><strong>Cost:</strong> ~$0.002 total</p>
<h4 id="throughout-day---interactive-work-tier-2">Throughout Day - Interactive Work (Tier 2)</h4>
<ul>
<li>10 chat conversations</li>
<li>3 code reviews</li>
<li>2 email summaries</li>
<li>1 documentation update</li>
</ul>
<p><strong>Cost:</strong> ~$0.40 total</p>
<h4 id="occasionally---complex-problem-tier-3">Occasionally - Complex Problem (Tier 3)</h4>
<ul>
<li>Maybe once a week</li>
<li>Usually after Tier 2 can&rsquo;t solve it</li>
</ul>
<p><strong>Cost:</strong> ~$2-5 per use</p>
<p><strong>Monthly Total:</strong> $15-25 depending on heavy problem-solving needs</p>
<p><img loading="lazy" src="/posts/openclaw-smart-ai-model-routing-to/img3.png"></p>
<h2 id="common-mistakes-to-avoid">Common Mistakes to Avoid</h2>
<table>
	<thead>
			<tr>
					<th>Mistake</th>
					<th>Problem</th>
					<th>Solution</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>✗ Using Premium Models by Default</td>
					<td>Burns through quota and budget</td>
					<td>Set Tier 2 as your default, escalate only when needed</td>
			</tr>
			<tr>
					<td>✗ Using Cheap Models for Complex Tasks</td>
					<td>Wastes time with poor results</td>
					<td>If unsure, start with Tier 2. Downgrade later if it&rsquo;s overkill.</td>
			</tr>
			<tr>
					<td>✗ Not Tracking Usage</td>
					<td>Can&rsquo;t identify what&rsquo;s expensive</td>
					<td>Log every call. Review weekly.</td>
			</tr>
			<tr>
					<td>✗ Manual Model Switching</td>
					<td>You&rsquo;ll forget or choose wrong</td>
					<td>Automate tier selection based on task type</td>
			</tr>
	</tbody>
</table>
<h2 id="quick-reference-template">Quick Reference Template</h2>
<p>Copy this into your system prompt:</p>
<pre tabindex="0"><code>You are a cost-efficient AI assistant using a tiered model system:

TIER 1 (Background): Gemini Flash, Claude Haiku
- Cost: $0.10-0.50 per 1M tokens
- Use for: Scheduled tasks, monitoring, file ops, simple queries
- Auto-select for all background work

TIER 2 (Default): Claude Sonnet, Gemini Pro
- Cost: $3-15 per 1M tokens  
- Use for: Chat, code, research, analysis
- Your default for all interactive work

TIER 3 (Premium): Claude Opus, GPT-4
- Cost: $15-75 per 1M tokens
- Use for: Complex reasoning, after Tier 2 fails
- ALWAYS ask permission before using

RULES:
1. Background/automated = Tier 1 (automatic)
2. Interactive/chat = Tier 2 (default)
3. Complex/failed attempts = Tier 3 (ask first)
4. Log all usage
5. Alert at 80% monthly budget
</code></pre><h2 id="conclusion">Conclusion</h2>
<p>The goal isn&rsquo;t to use the cheapest model possible. It&rsquo;s to <strong>match model capability to task complexity.</strong></p>
<ul>
<li>Simple tasks → Simple models</li>
<li>Normal work → Balanced models</li>
<li>Hard problems → Premium models</li>
</ul>
<p>This approach:</p>
<ul>
<li><strong>Reduces API quota usage</strong> by 60-80%</li>
<li><strong>Lowers costs</strong> significantly</li>
<li><strong>Maintains quality</strong> where it matters</li>
<li><strong>Reserves premium models</strong> for when you actually need them</li>
</ul>
<p><strong>Start simple:</strong></p>
<ol>
<li>Move background tasks to Tier 1</li>
<li>Keep Tier 2 as your default</li>
<li>Use Tier 3 strategically</li>
</ol>
<p>Ready to implement this yourself? Keep reading for the complete technical guide.</p>
<h1 id="complete-implementation-guide-building-your-tiered-ai-system">Complete Implementation Guide: Building Your Tiered AI System</h1>
<p>The technical details, configuration examples, and monitoring setup that powers my always-on AI assistant</p>
<h2 id="the-four-tier-architecture">The Four-Tier Architecture</h2>
<p>My production system actually uses <strong>four tiers</strong>, not three. The fourth tier adds fallback redundancy that prevents failures when primary services have issues.</p>
<p>┌─────────────────────────────────────────────────────────┐
│ TIER 1: Gemini Flash (Primary) │
│ Cost: $0.10/M input, $0.40/M output │
│ Use: 95% of all requests │
│ Handles: Summaries, Q&amp;A, simple code, background tasks │
└─────────────────────────────────────────────────────────┘
│
▼ (rate limited or down)
┌─────────────────────────────────────────────────────────┐
│ TIER 2: OpenRouter Bridge │
│ Cost: $0.075-$0.30/M (varies by model) │
│ Use: Fallback when Gemini fails │
│ Models: Gemini Flash Lite, DeepSeek V3 │
└─────────────────────────────────────────────────────────┘
│
▼ (still failing)
┌─────────────────────────────────────────────────────────┐
│ TIER 3: Claude Haiku │
│ Cost: $0.80/M input, $4/M output │
│ Use: Emergency fallback only │
│ When: Both Gemini and OpenRouter unavailable │
└─────────────────────────────────────────────────────────┘
│
▼ (explicit user request)
┌─────────────────────────────────────────────────────────┐
│ TIER 4: Claude Sonnet/Opus │
│ Cost: $3-15/M input, $15-75/M output │
│ Use: Complex reasoning, architecture, code review │
│ When: User explicitly invokes or task requires it │
└─────────────────────────────────────────────────────────┘</p>
<h2 id="tier-1-gemini-flash-the-workhorse">Tier 1: Gemini Flash (The Workhorse)</h2>
<p><strong>Model:</strong> <code>gemini-2.0-flash</code><br>
<strong>Cost:</strong> $0.10/M input, $0.40/M output<br>
<strong>Context Window:</strong> 1,000,000 tokens<br>
<strong>Rate Limits (Paid Tier 1):</strong> 2,000 RPM, 4M tokens/minute</p>
<p><strong>Handles 95% of all requests:</strong></p>
<ul>
<li><strong>Cron jobs</strong> - Daily summaries, health checks, weather alerts</li>
<li><strong>Heartbeat tasks</strong> - Keeping the assistant &ldquo;warm&rdquo; every 2 hours</li>
<li><strong>Simple queries</strong> - &ldquo;What time is it in Tokyo?&rdquo; doesn&rsquo;t need Opus</li>
<li><strong>Text processing</strong> - Summarization, formatting, extraction</li>
<li><strong>Background workers</strong> - Tasks that run while you sleep</li>
</ul>
<h3 id="why-gemini-flash">Why Gemini Flash?</h3>
<ol>
<li><strong>Massive context window</strong> - 1M tokens means it can ingest entire codebases</li>
<li><strong>Speed</strong> - Flash is fast, responses in under a second</li>
<li><strong>Cost</strong> - At $0.10/M input, you can process 10 million tokens for a dollar</li>
<li><strong>Google&rsquo;s free tier</strong> - 15 requests/minute free, 1,500/day (but paid tier recommended for reliability)</li>
</ol>
<h3 id="configuration-example">Configuration Example</h3>
<pre tabindex="0"><code>{
  &#34;google&#34;: {
    &#34;baseUrl&#34;: &#34;https://generativelanguage.googleapis.com/v1beta&#34;,
    &#34;apiKey&#34;: &#34;YOUR_API_KEY&#34;,
    &#34;models&#34;: [
      {
        &#34;id&#34;: &#34;gemini-2.0-flash&#34;,
        &#34;name&#34;: &#34;Gemini 2.0 Flash&#34;,
        &#34;cost&#34;: { &#34;input&#34;: 0.1, &#34;output&#34;: 0.4 },
        &#34;contextWindow&#34;: 1000000,
        &#34;maxTokens&#34;: 8192
      }
    ]
  }
}
</code></pre><h2 id="tier-2-openrouter-the-safety-net">Tier 2: OpenRouter (The Safety Net)</h2>
<p><strong>What is OpenRouter?</strong> A unified API gateway that routes to 100+ models from different providers. One API key, access to everything.</p>
<p><strong>Why use it as a fallback?</strong><br>
When Gemini hits rate limits or goes down (it happens), OpenRouter provides instant failover to alternative models.</p>
<h3 id="the-fallback-chain">The Fallback Chain</h3>
<pre tabindex="0"><code>{
  &#34;model&#34;: {
    &#34;primary&#34;: &#34;google/gemini-2.0-flash&#34;,
    &#34;fallbacks&#34;: [
      &#34;openrouter/google/gemini-2.5-flash-lite&#34;,
      &#34;openrouter/deepseek/deepseek-chat-v3-0324&#34;,
      &#34;anthropic/claude-haiku-4&#34;
    ]
  }
}
</code></pre><p><strong>When a request fails:</strong></p>
<ol>
<li>Try Gemini Flash directly → 429 rate limit</li>
<li>Try Gemini Flash Lite via OpenRouter → works, costs $0.075/M</li>
<li>If that fails, try DeepSeek V3 → works, costs $0.14/M</li>
<li>Last resort: Claude Haiku → works, costs $0.80/M</li>
</ol>
<p>The system automatically retries down the chain. User never sees an error.</p>
<h2 id="tier-3-claude-haiku-emergency-fallback">Tier 3: Claude Haiku (Emergency Fallback)</h2>
<p><strong>Model:</strong> <code>claude-haiku-4</code><br>
<strong>Cost:</strong> $0.80/M input, $4/M output<br>
<strong>When used:</strong> Only when Tiers 1 and 2 both fail</p>
<p>Haiku is the &ldquo;never fail&rdquo; option. Anthropic&rsquo;s infrastructure is rock solid. If Gemini is down AND OpenRouter is having issues, Haiku catches everything.</p>
<p>At 10x the cost of Gemini, you don&rsquo;t want this firing constantly. That&rsquo;s where monitoring comes in.</p>
<h2 id="tier-4-claude-sonnetopus-the-heavy-artillery">Tier 4: Claude Sonnet/Opus (The Heavy Artillery)</h2>
<p><strong>Models:</strong> <code>claude-sonnet-4-5</code>, <code>claude-opus-4-5</code><br>
<strong>Cost:</strong> $3-15/M input, $15-75/M output<br>
<strong>When used:</strong> Complex reasoning, code architecture, explicit requests</p>
<p><strong>Reserved for tasks that actually need them:</strong></p>
<ul>
<li>Multi-file code refactoring</li>
<li>System architecture decisions</li>
<li>Complex debugging requiring deep reasoning</li>
<li>When the user explicitly asks for the &ldquo;big brain&rdquo;</li>
</ul>
<h3 id="model-alias-system">Model Alias System</h3>
<pre tabindex="0"><code>{
  &#34;models&#34;: {
    &#34;anthropic/claude-sonnet-4-5&#34;: { &#34;alias&#34;: &#34;sonnet&#34; },
    &#34;anthropic/claude-opus-4-5&#34;: { &#34;alias&#34;: &#34;opus&#34; },
    &#34;google/gemini-2.0-flash&#34;: { &#34;alias&#34;: &#34;gemini-flash&#34; }
  }
}
</code></pre><p>User can type <code>/use opus</code> to explicitly switch, but defaults stay cheap.</p>
<p><img loading="lazy" src="/posts/openclaw-smart-ai-model-routing-to/img4.png"></p>
<h2 id="monitoring-catching-runaway-costs">Monitoring: Catching Runaway Costs</h2>
<p>The tier system only works if you monitor it. I run a Windows Task Scheduler job every 30 minutes that:</p>
<ol>
<li><strong>Parses logs</strong> for model usage</li>
<li><strong>Counts by model</strong> - How many Haiku? Sonnet? Opus?</li>
<li><strong>Checks thresholds</strong> - Opus should be 0 for background tasks</li>
<li><strong>Sends Telegram alerts</strong> if something&rsquo;s wrong</li>
</ol>
<h3 id="alert-thresholds">Alert Thresholds</h3>
<pre tabindex="0"><code>$maxOpusPerHour = 1      # Opus should NEVER be used by cron jobs
$maxHaikuPerHour = 5     # Haiku means Gemini is failing
$maxSonnetPerHour = 20   # Runaway conversation detection
$max429PerHour = 10      # Rate limit problems
</code></pre><h3 id="what-triggers-alerts">What Triggers Alerts</h3>
<table>
	<thead>
			<tr>
					<th>Condition</th>
					<th>Alert</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Opus used at all</td>
					<td>🚨 OPUS USED - Check fallback config!</td>
			</tr>
			<tr>
					<td>Haiku &gt; 5/hour</td>
					<td>⚠️ Haiku fallback triggered - Gemini may be failing</td>
			</tr>
			<tr>
					<td>402 errors</td>
					<td>🚨 PAYMENT REQUIRED - Credits depleted!</td>
			</tr>
			<tr>
					<td>429 &gt; 10/hour</td>
					<td>⚠️ Rate limit errors - API quota issues</td>
			</tr>
	</tbody>
</table>
<p>If I wake up to a Telegram message, something&rsquo;s wrong. No message = system healthy.</p>
<h2 id="real-world-cost-comparison">Real-World Cost Comparison</h2>
<h3 id="before-everything-on-claude">Before (Everything on Claude)</h3>
<table>
	<thead>
			<tr>
					<th>Task</th>
					<th>Model</th>
					<th>Daily Calls</th>
					<th>Cost/Day</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Cron jobs</td>
					<td>Sonnet</td>
					<td>50</td>
					<td>$2.25</td>
			</tr>
			<tr>
					<td>Heartbeats</td>
					<td>Haiku</td>
					<td>12</td>
					<td>$0.10</td>
			</tr>
			<tr>
					<td>User chat</td>
					<td>Sonnet</td>
					<td>200</td>
					<td>$9.00</td>
			</tr>
			<tr>
					<td>Background</td>
					<td>Sonnet</td>
					<td>100</td>
					<td>$4.50</td>
			</tr>
			<tr>
					<td>TOTAL</td>
					<td></td>
					<td></td>
					<td>$15.85/day</td>
			</tr>
	</tbody>
</table>
<h3 id="after-tiered-system">After (Tiered System)</h3>
<table>
	<thead>
			<tr>
					<th>Task</th>
					<th>Model</th>
					<th>Daily Calls</th>
					<th>Cost/Day</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Cron jobs</td>
					<td>Gemini Flash</td>
					<td>50</td>
					<td>$0.02</td>
			</tr>
			<tr>
					<td>Heartbeats</td>
					<td>Gemini Flash</td>
					<td>12</td>
					<td>$0.005</td>
			</tr>
			<tr>
					<td>User chat</td>
					<td>Gemini Flash</td>
					<td>180</td>
					<td>$0.07</td>
			</tr>
			<tr>
					<td>User chat (complex)</td>
					<td>Sonnet</td>
					<td>20</td>
					<td>$0.90</td>
			</tr>
			<tr>
					<td>Background</td>
					<td>Gemini Flash</td>
					<td>100</td>
					<td>$0.04</td>
			</tr>
			<tr>
					<td>TOTAL</td>
					<td></td>
					<td></td>
					<td>$1.04/day</td>
			</tr>
	</tbody>
</table>
<p><strong>Savings: 93%</strong><br>
From $15.85/day to $1.04/day<br>
Monthly: $475 → $31</p>
<h2 id="implementation-tips">Implementation Tips</h2>
<h4 id="1-start-with-logging-before-switching">1. Start with logging before switching</h4>
<p>Track what models are being used and why before changing anything. You might find 80% of your expensive calls are for simple tasks.</p>
<h4 id="2-use-model-aliases">2. Use model aliases</h4>
<p>Make it easy to switch: <code>gemini-flash</code>, <code>sonnet</code>, <code>opus</code>. Users shouldn&rsquo;t memorize model IDs.</p>
<h4 id="3-set-up-alerting-immediately">3. Set up alerting immediately</h4>
<p>The moment you deploy a fallback system, monitor it. A misconfigured fallback chain can burn through credits overnight.</p>
<h4 id="4-test-your-fallbacks">4. Test your fallbacks</h4>
<p>Deliberately rate-limit yourself and verify the chain works:</p>
<pre tabindex="0"><code># Simulate Gemini failure
curl -X POST your-api -H &#34;X-Force-Fallback: true&#34;
</code></pre><h4 id="5-consider-task-specific-routing">5. Consider task-specific routing</h4>
<p>Some tasks should <em>always</em> use a specific tier:</p>
<ul>
<li>Summarization → Always Tier 1</li>
<li>Code review → Always Tier 4</li>
<li>Health checks → Always Tier 1</li>
</ul>
<h2 id="the-configuration-that-runs-my-system">The Configuration That Runs My System</h2>
<pre tabindex="0"><code>{
  &#34;agents&#34;: {
    &#34;defaults&#34;: {
      &#34;model&#34;: {
        &#34;primary&#34;: &#34;google/gemini-2.0-flash&#34;,
        &#34;fallbacks&#34;: [
          &#34;openrouter/google/gemini-2.5-flash-lite&#34;,
          &#34;openrouter/deepseek/deepseek-chat-v3-0324&#34;,
          &#34;anthropic/claude-haiku-4&#34;
        ]
      },
      &#34;heartbeat&#34;: {
        &#34;model&#34;: &#34;gemini-flash&#34;,
        &#34;every&#34;: &#34;2h&#34;
      }
    }
  }
}
</code></pre><h2 id="final-thoughts">Final Thoughts</h2>
<p>The &ldquo;just use GPT-4/Claude for everything&rdquo; approach is dead. Modern AI infrastructure requires the same thinking we apply to any distributed system:</p>
<ul>
<li><strong>Use the cheapest resource that works</strong></li>
<li><strong>Have fallbacks for reliability</strong></li>
<li><strong>Monitor everything</strong></li>
<li><strong>Reserve expensive resources for when they&rsquo;re needed</strong></li>
</ul>
<p>My bot now handles thousands of daily interactions for about a dollar. The expensive models are still there when I need them - but they&rsquo;re not wasting money on &ldquo;what&rsquo;s the weather?&rdquo; queries.</p>
<p><strong>Build your tiers. Set your fallbacks. Sleep peacefully while your AI runs on pennies.</strong></p>
<hr>
<p><strong>Questions? Running an always-on AI assistant?</strong><br>
Drop a comment. I&rsquo;d love to hear how you&rsquo;re handling costs.</p>
<p><em>Author: PuebloKC</em><br>
<em>Running OpenClaw AI automation system</em><br>
<em>February 2026</em></p>
]]></content:encoded></item><item><title>Google Drive and OneDrive ARE NOT backups</title><link>https://errorzap.com/posts/google-drive-and-onedrive-are-not/</link><pubDate>Wed, 22 Oct 2025 21:48:00 -0600</pubDate><guid>https://errorzap.com/posts/google-drive-and-onedrive-are-not/</guid><description>Google Drive and OneDrive are sync tools, not backups.If ransomware hits or files get deleted, they sync that too—straight into the void.☁️ Backup Rule 101</description><content:encoded><![CDATA[<p>Google Drive and OneDrive are sync tools, not backups.</p>
<p>If ransomware hits or files get deleted, they sync that too—straight into the void.</p>
<p>☁️ Backup Rule 101: Always keep at least one offline or immutable copy of your data.</p>
<p>💾 True backup = a separate system that keeps previous versions safe from sync errors or hacks.</p>
<p>#TechTips #BackupStrategy #CyberSecurity #DataProtection</p>
<p><img loading="lazy" src="/posts/google-drive-and-onedrive-are-not/img1.jpg"></p>
]]></content:encoded></item><item><title>My digital brain developed with AI</title><link>https://errorzap.com/posts/my-digital-brain-developed-with-ai/</link><pubDate>Mon, 15 Sep 2025 01:16:00 -0600</pubDate><guid>https://errorzap.com/posts/my-digital-brain-developed-with-ai/</guid><description>Trying to improve my awful systems and memory with automation and Ai developed solutions.Brainscan eats data from multiple apps and sources and then displa</description><content:encoded><![CDATA[<p>Trying to improve my awful systems and memory with automation and Ai developed solutions.</p>
<p>Brainscan eats data from multiple apps and sources and then displays it and allows it marked for bill etc.</p>
<p>BrainDump is a simple web app for quickly entering a note or idea about anything. This gets saved and categorized for later use.</p>
<p>Worklog is a simple way to enter work for clients, which feeds into Brainscan as well.</p>
<p>The final piece is the call the nerd. Com final front-end finishing then we will funnel that into the same system along with alert</p>
<p>Now I have a server that processes every random thought or idea I have for later use, auto creates tasks and events, auto creates invoices and billing info.</p>
<p>All hosted on a $35 virtual cloud server</p>
<p><img loading="lazy" src="/posts/my-digital-brain-developed-with-ai/img1.jpg"></p>
<p><img loading="lazy" src="/posts/my-digital-brain-developed-with-ai/img2.jpg"></p>
<p><img loading="lazy" src="/posts/my-digital-brain-developed-with-ai/img3.jpg"></p>
]]></content:encoded></item><item><title>online security gut check. secure yourself now.</title><link>https://errorzap.com/posts/online-security-gut-check-secure/</link><pubDate>Tue, 08 Jul 2025 02:50:00 -0600</pubDate><guid>https://errorzap.com/posts/online-security-gut-check-secure/</guid><description>🔐 Have You Enabled 2FA Everywhere? Changed Your Password This Decade?Let’s start with a gut check.Do you reuse the same password across multiple sites?Is y</description><content:encoded><![CDATA[<p><img loading="lazy" src="/posts/online-security-gut-check-secure/img1.jpg"></p>
<p>🔐 Have You Enabled 2FA Everywhere? Changed Your Password This Decade?</p>
<p>Let’s start with a gut check.</p>
<p>Do you reuse the same password across multiple sites?</p>
<p>Is your email password the same one you used in 2013?</p>
<p>Have you enabled two-factor authentication (2FA) on your most important accounts?</p>
<p>Or&hellip; are you still getting text codes (yikes) instead of using an authenticator app?</p>
<p>If any of the above made you uncomfortable — good. That’s the point.</p>
<hr>
<p>🧠 Why This Still Matters</p>
<p>Every week we clean up the mess after someone gets locked out of an account, or worse, loses access due to a breach. And 99% of the time? It could’ve been prevented with a simple change:</p>
<p>✅ Stronger, unique passwords</p>
<p>✅ Two-Factor Authentication (2FA)</p>
<p>Still think you’re safe because “nobody would hack me”? Newsflash: they don’t target you. They target everyone, and your password is probably already floating around out there. Just check <a href="https://haveibeenpwned.com">https://haveibeenpwned.com</a> if you don’t believe me.</p>
<hr>
<p>🛡️ Do This Now:</p>
<p>Enable 2FA (Everywhere)</p>
<p>Start with:</p>
<ol>
<li>
<p>Email</p>
</li>
<li>
<p>Bank</p>
</li>
<li>
<p>cloud storage</p>
</li>
<li>
<p>Social media</p>
</li>
<li>
<p>Any sites with your personal data or billing info</p>
</li>
</ol>
<p>Use an authenticator app (like Authy, Microsoft Authenticator, or Google Authenticator). Not SMS. Text-based 2FA can be hijacked.</p>
<ol start="2">
<li>Use a Password Manager</li>
</ol>
<p>Bitwarden, 1Password, or even the built-in tools from Apple or Google are far better than your sticky note or Excel sheet.</p>
<ol start="3">
<li>Change Old Passwords</li>
</ol>
<p>If your password is:</p>
<p>Over 5 years old</p>
<p>Shared across more than one site</p>
<p>Or contains your pet&rsquo;s name and birth year&hellip;</p>
<p>Change. It. Now.</p>
<p>Use 12+ characters, mix it up, and stop using &ldquo;!&rdquo; at the end to feel secure. Hackers are wise to that game.</p>
<hr>
<p>⚙️ Bonus Tip: Turn on Login Alerts</p>
<p>Most sites let you enable notifications when someone logs in from a new device. Do that. It’s like a smoke alarm for your accounts.</p>
<p>Final tip: listen to your IT people, ensure you have working backups, and someone monitoring the security of your devices, servers, and other online assets.</p>
<p>And update your end of life networking devices and computers!</p>
<hr>
<p>🔚 Final Thoughts</p>
<p>Cybersecurity isn’t just for IT nerds. It’s for everyone. The smallest effort (like enabling 2FA) goes a long way in preventing a total digital meltdown.</p>
<p>And if it’s too much? That’s what we’re here for.</p>
<p>!Need help securing your accounts or deploying 2FA for your business? Contact us <a href="http://mypueblopc.com">mypueblopc.com</a></p>
]]></content:encoded></item><item><title>Ransomware: It’s Like an Escape Room… But With Your Business.</title><link>https://errorzap.com/posts/ransomware-its-like-escape-room-but/</link><pubDate>Sat, 21 Jun 2025 00:02:00 -0600</pubDate><guid>https://errorzap.com/posts/ransomware-its-like-escape-room-but/</guid><description>There&amp;rsquo;s been a massive spike in real-world malware attacks lately on average businesses—not just attempts, but full-blown breaches where threat actors gain</description><content:encoded><![CDATA[<p><img loading="lazy" src="/posts/ransomware-its-like-escape-room-but/img1.jpg"></p>
<p>There&rsquo;s been a massive spike in real-world malware attacks lately on average businesses—not just attempts, but full-blown breaches where threat actors gain complete remote control of systems.</p>
<p>These attacks almost always end in ransomware, data theft, or total system compromise.</p>
<p>Where I used to see occasional phishing or intrusion attempts, I’m now seeing successful attacks almost daily. Just this week, one careless click on a fake file by a single employee could have given attackers full access to an entire company&rsquo;s network, backups, and data—completely wiping everything out.</p>
<p>If proper security tools and procedures hadn’t been in place, that business would have been destroyed.</p>
<p>This stuff is as serious—and terrifying—as it gets. And yet, far too many business owners and individuals still believe it “won’t happen to them.”</p>
]]></content:encoded></item><item><title>Explosive home remedies</title><link>https://errorzap.com/posts/explosive-home-remedies/</link><pubDate>Wed, 18 Jun 2025 04:28:00 -0600</pubDate><guid>https://errorzap.com/posts/explosive-home-remedies/</guid><description>When nasal spray just won’t cut it, light the fuse of freedom.</description><content:encoded><![CDATA[<p><img loading="lazy" src="/posts/explosive-home-remedies/img1.jpg"></p>
<p>When nasal spray just won’t cut it, light the fuse of freedom.</p>
<p><img loading="lazy" src="/posts/explosive-home-remedies/img2.jpg"></p>
]]></content:encoded></item><item><title>EyeBong</title><link>https://errorzap.com/posts/eyebong/</link><pubDate>Wed, 18 Jun 2025 03:22:00 -0600</pubDate><guid>https://errorzap.com/posts/eyebong/</guid><description>Eyebong™: The first ophthalmic cannabinoid serum with mind-expanding vision technology. See the truth. Or go blind trying.</description><content:encoded><![CDATA[<p>Eyebong™: The first ophthalmic cannabinoid serum with mind-expanding vision technology. See the truth. Or go blind trying.</p>
<p><img loading="lazy" src="/posts/eyebong/img1.jpg"></p>
]]></content:encoded></item><item><title>No Fires Today (Because I Put Them Out at 4AM)</title><link>https://errorzap.com/posts/no-fires-today-because-i-put-them-out/</link><pubDate>Mon, 19 May 2025 13:15:00 -0600</pubDate><guid>https://errorzap.com/posts/no-fires-today-because-i-put-them-out/</guid><description>Today’s biggest crisis? Logging into Google.(Yep. That was it. The login screen. The pinnacle of chaos.)So I’m officially calling it a success.Never mind t</description><content:encoded><![CDATA[<p><img loading="lazy" src="/posts/no-fires-today-because-i-put-them-out/img1.jpg"></p>
<p>Today’s biggest crisis? Logging into Google.</p>
<p>(Yep. That was it. The login screen. The pinnacle of chaos.)</p>
<p>So I’m officially calling it a success.</p>
<p>Never mind the fact that I spent the weekend elbows-deep in outdated machines, broken updates, stubborn printers, and a firewall that decided it had free will.</p>
<p>Everything looked smooth because I spent hours making sure it was.</p>
<p>No one notices when things don’t go wrong—</p>
<p>because I already fought all the gremlins before sunrise.</p>
<p>You&rsquo;re welcome.</p>
]]></content:encoded></item><item><title>Click this. Click that. Install this. Run that. Update this. Reboot that.</title><link>https://errorzap.com/posts/click-this-click-that-install-this-run/</link><pubDate>Sun, 18 May 2025 02:21:00 -0600</pubDate><guid>https://errorzap.com/posts/click-this-click-that-install-this-run/</guid><description>Ah yes, the sacred ritual of modern IT.Every day I wake up, stretch, and immediately begin my daily chant:“Have you tried turning it off and on again?”My h</description><content:encoded><![CDATA[<p><img loading="lazy" src="/posts/click-this-click-that-install-this-run/img1.jpg"></p>
<p>Ah yes, the sacred ritual of modern IT.</p>
<p>Every day I wake up, stretch, and immediately begin my daily chant:</p>
<p>“Have you tried turning it off and on again?”</p>
<p>My hands? Permanently in “Ctrl + Alt + Delete” formation.</p>
<p>My coffee? 98% bitterness, 2% thermal paste.</p>
<p>My brain? Running on 3 Chrome tabs, one of which is frozen.</p>
<p>Users be like:</p>
<p>“It just stopped working.”</p>
<p>Oh did it? Did it just stop, Barbara? Or did you feed it malware, deny the updates, ignore 13 error messages, and then scream when it exploded?</p>
<p>I am the keeper of passwords no one remembers, the slayer of printers with free will, and the high priest of “Why the hell is this plugged into HDMI 3?”</p>
<p>Sometimes I wonder…</p>
<p>What if I clicked myself into safe mode?</p>
<p>Would I finally get peace?</p>
<p>But no.</p>
<p>Here I go again.</p>
<p>Click this. Click that.</p>
<p>Pray to the Wi-Fi gods.</p>
<p>Sacrifice a USB stick.</p>
<p>And hope—just hope—the server doesn’t reboot during lunch.</p>
<p>#TechLife #DigitalShaman #CtrlAltPray</p>
]]></content:encoded></item><item><title>prednisolone</title><link>https://errorzap.com/posts/prednisolone/</link><pubDate>Thu, 24 Apr 2025 00:33:00 -0600</pubDate><guid>https://errorzap.com/posts/prednisolone/</guid><description>Oompa Loompa, doopatty Zone,Let me explain what Prednisolone&amp;rsquo;s known:It fights inflammation, that’s its big skill—But oh, the side effects? They&amp;rsquo;re kind of</description><content:encoded><![CDATA[<p>Oompa Loompa, doopatty Zone,</p>
<p>Let me explain what Prednisolone&rsquo;s known:</p>
<p>It fights inflammation, that’s its big skill—</p>
<p>But oh, the side effects? They&rsquo;re kind of a pill.</p>
<p>Oompa Loompa, roid-powered ride,</p>
<p>Here’s what can happen on the steroid tide:</p>
<p>Your bones get brittle, your mood might swing,</p>
<p>You’ll crave weird snacks and curse everything.</p>
<p>It zaps your immune like a ninja in black,</p>
<p>But robs your potassium, sneaks out the back.</p>
<p>It fluffs your face, may give you the shakes,</p>
<p>And keeps you up counting sheep-fueled earthquakes.</p>
<p>Oompa Loompa, pred-sanity plea,</p>
<p>Taper it slow and eat potassium, see?</p>
<p>Magnesium helps, and some D on the side—</p>
<p>Or else it’s moonface and a cortisol slide</p>
<p><img loading="lazy" src="/posts/prednisolone/img1.jpg"></p>
]]></content:encoded></item><item><title>Ways to Spot a Scam (Without Even Updating Your Firewall)</title><link>https://errorzap.com/posts/ways-to-spot-scam-without-even-updating/</link><pubDate>Sun, 13 Apr 2025 08:25:00 -0600</pubDate><guid>https://errorzap.com/posts/ways-to-spot-scam-without-even-updating/</guid><description>&lt;ol&gt;
&lt;li&gt;It’s on Social MediaIf it’s on Facebook, TikTok, or Instagram—it’s a scam.Doesn’t matter if it’s a giveaway, a job offer, or your grandma tagging you in&lt;/li&gt;
&lt;/ol&gt;</description><content:encoded><![CDATA[<p><img loading="lazy" src="/posts/ways-to-spot-scam-without-even-updating/img1.jpg"></p>
<ol>
<li>It’s on Social Media</li>
</ol>
<p>If it’s on Facebook, TikTok, or Instagram—it’s a scam.</p>
<p>Doesn’t matter if it’s a giveaway, a job offer, or your grandma tagging you in a CBD oil pyramid scheme.</p>
<p>Rule of thumb: If you learned about it between cat videos and a guy eating drywall—it’s fake.</p>
<hr>
<ol start="2">
<li>Someone Calls YOU</li>
</ol>
<p>No one calls you to give you money.</p>
<p>Not the IRS, not Microsoft, not “Visa Fraud Department.”</p>
<p>Hell, even your friends barely call anymore. If someone dials you first, assume they want your soul, your wallet, or both.</p>
<hr>
<ol start="3">
<li>They Say “Kindly”</li>
</ol>
<p>“Kindly click the link below…”</p>
<p>“Kindly confirm your identity…”</p>
<p>“Kindly ignore the fact this was written by a Nigerian AI on a sugar high.”</p>
<p>Reality check: No legit person says “kindly” unless they’re about to scam you or they’re 110 years old.</p>
<hr>
<ol start="4">
<li>They Say They’re the Government</li>
</ol>
<p>Ah yes, the government reaching out to help you with money, jail, or tech support.</p>
<p>Spoiler:</p>
<p>The IRS doesn’t email.</p>
<p>The FBI doesn’t text.</p>
<p>Social Security doesn’t DM you at 3AM.</p>
<p>And if they’re calling from a “restricted number,” it’s either a scammer… or your ex.</p>
]]></content:encoded></item><item><title>My life</title><link>https://errorzap.com/posts/my-life/</link><pubDate>Tue, 18 Mar 2025 21:40:00 -0600</pubDate><guid>https://errorzap.com/posts/my-life/</guid><description>So all of my life, I knew something was perhaps different about me. I would see others socializing, having fun, playing. However, I could not do those same</description><content:encoded><![CDATA[<figure style="margin:0 0 26px">
<svg viewBox="0 0 800 200" xmlns="http://www.w3.org/2000/svg" style="width:100%;border-radius:14px">
  <defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
    <stop offset="0" stop-color="#1e1e2e"/><stop offset="1" stop-color="#11111b"/></linearGradient></defs>
  <rect width="800" height="200" fill="url(#g)"/>
  <rect width="800" height="200" fill="none" stroke="#a6adc8" stroke-width="2" opacity="0.5" rx="14"/>
  <circle cx="660" cy="100" r="120" fill="#a6adc8" opacity="0.08"/>
  <text x="56" y="118" font-size="86">💭</text>
  <text x="170" y="92" font-family="ui-monospace,monospace" font-size="13" fill="#a6adc8" letter-spacing="3">ERROR ZAP // ARCHIVE</text>
  <text x="170" y="132" font-family="-apple-system,Segoe UI,sans-serif" font-size="30" font-weight="800" fill="#cdd6f4">My life</text>
</svg></figure>
<p>So all of my life, I knew something was perhaps different about me. I would see others socializing, having fun, playing. However, I could not do those same things the majority of the time. Noises that no one else hears, bother me intensely. Clothing is a constant source of stress, not for the looks, but the feel. And as of a few months ago, I finally found a reason. And a group of people who feel and act the same way. I found out what Aspergers Syndrome was, and relate very closely to it. While I have no official diagnosis, their is no doubt in my mind that this is me.</p>
<p>Lets start with one of my major problems, shall we? Sound. I hate any sound that is irregular, or of some certain pitch and tone. I can hear the irregularities in a fan motor, and they cause me to find a way to make it stop. Usually taping some of the vent where it blows out will cause the irregularity to go away. I can hear the buzzing of plug-in transformers, and it is painful! Going to a restaurant or nearly all public places faces me with a daunting task of dealing with all of these noises. The fluorescent lights humming, people laughing, talking, , and the various other noises that people make. They all make life very difficult, I cant simply tune them out, or ignore them. A quiet tap, is like a pounding hammer. Constantly distracting me, and causing a weird form of anger. A</p>
<p>Clothing has been a lifelong battle for me, one that is fought daily. Clothing, to most is a statement of their style, and who they are. To me, clothing is simply a daily annoyance. I dont really care what my clothes look like, so long as they are comfortable and they dont irritate me in any way. The seems, patches, fabric, and tightness are all contributers to how I feel. If my clothing doesnt feel good on my skin, my mood instantly goes bad, and I want nothing more than to be free of the bad feeling clothing. If my clothes are uncomfortable, I can not concentrate, work, or do much of anything. As a result of all this, I wear the same shirt and shorts on a daily basis. I get a lot of crap from others about this, but quite honestly they will never understand any of my reasoning. When I get a shirt, or shorts, or anything that &ldquo;feels&rdquo; good to me I hold onto it for life! I put my shirt and shorts on every day, knowing they are broken in, they wont poke or scratch my skin, and nothing about them bothers me. This alleviates a very large part of the daily stress I deal with. I will wear this clothing until it is battered, torn, and the fabric nearly see through from wear.</p>
<p>Social interaction is another huge issue for me. It has never come to me easily, and doesn&rsquo;t seem to get any easier through life. I have a huge craving for social interaction, but when i actually interact, I find myself wanting to run and hide from everyone. It is a never ending fight, one that results in me being alone the majority of the time. Which, is quite pleasant most of the time. I find that shopping late at night, is the best way to avoid a lot of these issues. So when I can, 24 hour stores are my favorite place to visit.</p>
<p>Since as far back as I can remember I have spent the majority of my time alone, doing solo things. I used to garden, build things, and various other activities most of the kids my age didn&rsquo;t do. I found them intriguing, always learning something new, and without the bother of people. I could do whatever I wanted, without fear of judgment and the prying eyes of others. To this day I still follow that same pattern. I hang out on line pretty much all day, I can get some socializing done this way, without the same fears that present themselves during real life meetings. The computer is my gateway to the world, without it, I am unsure where I would be.</p>
<p>When I do venture out into the real world, it is a constant fight. I always wonder why people look at me, what they are thinking about me, things of that nature. It makes me feel awkward. When I go places I avoid area with people, avoiding younger people with all possible effort. Young people intimidate me. When I go shopping I try not to go down isles where other people, or employees are. I don&rsquo;t like being approached by employees one bit, so they are avoided at all cost.</p>
<p>I enjoy shopping on line whenever possible, no human involvement at all. I can take my time, without worrying about bothering someone. I can research the item all I want. And of course I don&rsquo;t have to wait in lines, or deal with nearly any annoyance of the real world. I cant imagine life without the Internet!</p>
<p>All of these things have been a daily struggle for me, and for years I have searched on line to try and find people with the same issues, and struggles. Finally not too long ago I did find some answers. I found articles about others who fought clothing, and hated certain noises. These people understood how I felt. And it felt good to be understood!</p>
<p>Around this time is when I began posting on various Aspie boards, asking questions, seeking advice. The people were amazing, they all understood and could relate perfectly to what I was going through. Then I met a wonderful person on one of these boards, her name being Kate. Since that day Kate and I have been talking, chatting, and emailing fairly regularly. We speak nearly the same in our emails, and we often have a hard time distinguishing each another&rsquo;s emails. I had finally found someone who knew exactly what I was going through when I put on clothing, heard the ticking of a clock, or simply wandered aimlessly wherever. Shortly after meeting I even had to privilege of meeting her on a layover in Denver. A lot of driving, for a short meeting, but one of the best things I have done in a long time. Anyway, I mention Kate because she has helped me to understand this condition very well, and has written some amazing articles about Aspergers and living day-to-day with it. I would like to offer you the chance to read up on at least one of them, at this link:</p>
<p><a href="http://www.asdrendrewolf.org/worldnews/kg/bodylotion101_01.htm">http://www.asdrendrewolf.org/worldnews/kg/bodylotion101_01.htm</a></p>
<p>She does an amazing job of describing how she feels, and honestly exactly how I feel. So read it!</p>
<p>I also found an interesting article tonight, in another forum. Here are some excerpts, to help describe my day. This particular article is focused more on children with Aspergers, but the same applies to adults. :) Quoted from: <a href="http://www.buzzle.com/editorials/11-8-2005-80891.asp" title="Linkification: http://www.buzzle.com/editorials/11-8-2005-80891.asp">http://www.buzzle.com/editorials/11-8-2005-80891.asp</a></p>
<p>&ldquo;Asperger children are also overloaded from too many people (crowds), and some scenarios are shopping centers, school assembly/parade, public libraries, movie theatres, concerts, fairs, playgrounds in the park etc. They seem to soak in all the energy around them. Too much to smell, too much to listen to, too much to see, too much movement! &quot;</p>
<p>-Crowds have long been a great fear of mine. I avoid them at all cost!</p>
<p>&ldquo;Another source of overload for the Asperger child is voice, particularly tone of voice. Long before the words or message is decoded, the tone is instantly analyzed by the ASD child. Any hint of criticism or sarcasm is detected and taken personally. A critical tone is destructive, particularly when the Asperger child is not aware of the &lsquo;why&rsquo;. The &rsquo;loop&rsquo; effect can result in the Asperger child going over the statement long after the event took place. They try to analyze the scenario, and this causes an increase in anxiety, agitation and fear. &quot;</p>
<ul>
<li>This has also been a long time issue for me, though until very recently I had no idea why.</li>
</ul>
<p>&ldquo;Most Asperger children find it traumatic to be looked at or stared at. Asperger children feel vulnerable; unable to protect themselves from prying eyes. Staring intrudes on their own private world, and these children feel powerless to deal with it. When Asperger children become over sensitized from staring they often think they are being stared at when they&rsquo;re not. Their obsessiveness takes over and &ldquo;being stared at&rdquo; can become a major source of bother. &quot;</p>
<p>-Stare at me and I will be long gone very soon!</p>
<p>Ok I think thats about all the writing im going to do for now. Any questions please feel free to ask!</p>
<p>The &ldquo;geek&rdquo;,</p>
<p>Justin</p>
]]></content:encoded></item><item><title>Men in suits: The world’s most successful scammers.</title><link>https://errorzap.com/posts/men-in-suits-worlds-most-successful/</link><pubDate>Sun, 02 Mar 2025 07:09:00 -0700</pubDate><guid>https://errorzap.com/posts/men-in-suits-worlds-most-successful/</guid><description>They smile, shake hands, and rob you blind—all while calling it “business.” From banks to boardrooms, politicians to hedge funds, the biggest lies come wra</description><content:encoded><![CDATA[<p><img loading="lazy" src="/posts/men-in-suits-worlds-most-successful/img1.jpg"></p>
<p>They smile, shake hands, and rob you blind—all while calling it “business.” From banks to boardrooms, politicians to hedge funds, the biggest lies come wrapped in tailored fabric. They’ll sell you debt as opportunity, taxes as necessity, and corruption as strategy.</p>
<p>Meanwhile, the guy in a hoodie fixing your network or the mechanic in greasy overalls is the one actually keeping the world running.</p>
<p>Don’t be fooled by the suit. The real work happens outside the boardroom.</p>
]]></content:encoded></item><item><title>Breaking news: Colorado takeover starts tomorrow</title><link>https://errorzap.com/posts/breaking-news-colorado-takeover-starts/</link><pubDate>Tue, 18 Feb 2025 21:37:00 -0700</pubDate><guid>https://errorzap.com/posts/breaking-news-colorado-takeover-starts/</guid><description>Breaking News: The Musk Era BeginsIt&amp;rsquo;s time for me and my elite, highly-qualified executive oversight team (comprised mostly of AI, flamethrowers, and one</description><content:encoded><![CDATA[<figure style="margin:0 0 26px">
<svg viewBox="0 0 800 200" xmlns="http://www.w3.org/2000/svg" style="width:100%;border-radius:14px">
  <defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
    <stop offset="0" stop-color="#1e1e2e"/><stop offset="1" stop-color="#11111b"/></linearGradient></defs>
  <rect width="800" height="200" fill="url(#g)"/>
  <rect width="800" height="200" fill="none" stroke="#a6e3a1" stroke-width="2" opacity="0.5" rx="14"/>
  <circle cx="660" cy="100" r="120" fill="#a6e3a1" opacity="0.08"/>
  <text x="56" y="118" font-size="86">🏔</text>
  <text x="170" y="92" font-family="ui-monospace,monospace" font-size="13" fill="#a6e3a1" letter-spacing="3">ERROR ZAP // ARCHIVE</text>
  <text x="170" y="132" font-family="-apple-system,Segoe UI,sans-serif" font-size="30" font-weight="800" fill="#cdd6f4">Breaking news: Colorado takeover starts to</text>
</svg></figure>
<p>Breaking News: The Musk Era Begins</p>
<p>It&rsquo;s time for me and my elite, highly-qualified executive oversight team (comprised mostly of AI, flamethrowers, and one genetically enhanced hamster) to begin our work.</p>
<p>Starting tomorrow morning, I will personally take control of all operations in the state of Colorado. No, you don’t get a say in this. Democracy was cool, but have you tried Technocracy with rocket-powered roller skates?</p>
<p>All dissenters will be swiftly dealt with by my highly trained attack mongoose, X-Æ-A12. This little guy was raised on Tesla battery fumes and Cybertruck shrapnel—he fears nothing, respects no one, and will short your stocks on sight.</p>
<p>First order of business? Underground Hyperloop tunnels connecting every dispensary in the state. Second order of business? I haven’t decided, but I’ll probably announce it at 3 AM in a Twitter post that instantly drops the stock market by 12%.</p>
<p>Welcome to the future, peasants.</p>
]]></content:encoded></item><item><title>Chronic illness. Do you get it?</title><link>https://errorzap.com/posts/chronic-illness-do-you-get-it/</link><pubDate>Mon, 10 Feb 2014 08:35:00 -0700</pubDate><guid>https://errorzap.com/posts/chronic-illness-do-you-get-it/</guid><description>Chronic illness. Two words. Yet so much confusion lies within them. Yes I (and others, look fine) but inside we hurt, and are a mess. We take massive amoun</description><content:encoded><![CDATA[<figure style="margin:0 0 26px">
<svg viewBox="0 0 800 200" xmlns="http://www.w3.org/2000/svg" style="width:100%;border-radius:14px">
  <defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
    <stop offset="0" stop-color="#1e1e2e"/><stop offset="1" stop-color="#11111b"/></linearGradient></defs>
  <rect width="800" height="200" fill="url(#g)"/>
  <rect width="800" height="200" fill="none" stroke="#89b4fa" stroke-width="2" opacity="0.5" rx="14"/>
  <circle cx="660" cy="100" r="120" fill="#89b4fa" opacity="0.08"/>
  <text x="56" y="118" font-size="86">💊</text>
  <text x="170" y="92" font-family="ui-monospace,monospace" font-size="13" fill="#89b4fa" letter-spacing="3">ERROR ZAP // ARCHIVE</text>
  <text x="170" y="132" font-family="-apple-system,Segoe UI,sans-serif" font-size="30" font-weight="800" fill="#cdd6f4">Chronic illness. Do you get it? </text>
</svg></figure>
<p>Chronic illness. Two words. Yet so much confusion lies within them. Yes I (and others, look fine) but inside we hurt, and are a mess. We take massive amounts of pills and injections just to live (they hardly let us feel decent, let alone good)</p>
<p>I used to see someone who looked fine using wheelchairs, walking slow, resting a lot, etc and assumed it was just a lazy person. Now I know better. Far too well.</p>
<p>Never asked for all this, nor did anyone else with an illness. Just remember we fight every step of the day, we have to convince doctors to help us, find compitent docs, then fight insurance for medication the doctors want us on. Not to mention the Labs, tests, etc. It&rsquo;s all very tiring, and much like a horrible job you can&rsquo;t quit and don&rsquo;t get paid for.</p>
<p>Don&rsquo;t judge, not everyone was blessed with a working body. Nearly sitting up takes a lot of energy. I do everything possible to change my status (the list of things I try is very long) however as of yet I&rsquo;ve never found a good solution, just temporary solutions that may end up causing more problems in the end.</p>
<p>I&rsquo;ve been at this since I was 15, nearly 15 years. Half my life. All my adult life.</p>
<p>If you know someone struggling with physical or mental issues, support them, ask them what they need. Don&rsquo;t offer a ton of unproven cures (we all hear this often) and remember, looks don&rsquo;t mean a thing.</p>
<p>Make the world better, help someone that needs it. Peace!</p>
]]></content:encoded></item><item><title>Pueblo Police - You can not take photos!</title><link>https://errorzap.com/posts/pueblo-police-you-can-not-take-photos/</link><pubDate>Thu, 11 Mar 2010 21:11:00 -0700</pubDate><guid>https://errorzap.com/posts/pueblo-police-you-can-not-take-photos/</guid><description>So here I am, driving home and I see cops, and a car partly in a building. These are the things I like to photograph. So I proceeded to take photos (All th</description><content:encoded><![CDATA[<figure style="margin:0 0 26px">
<svg viewBox="0 0 800 200" xmlns="http://www.w3.org/2000/svg" style="width:100%;border-radius:14px">
  <defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
    <stop offset="0" stop-color="#1e1e2e"/><stop offset="1" stop-color="#11111b"/></linearGradient></defs>
  <rect width="800" height="200" fill="url(#g)"/>
  <rect width="800" height="200" fill="none" stroke="#f9e2af" stroke-width="2" opacity="0.5" rx="14"/>
  <circle cx="660" cy="100" r="120" fill="#f9e2af" opacity="0.08"/>
  <text x="56" y="118" font-size="86">🚨</text>
  <text x="170" y="92" font-family="ui-monospace,monospace" font-size="13" fill="#f9e2af" letter-spacing="3">ERROR ZAP // ARCHIVE</text>
  <text x="170" y="132" font-family="-apple-system,Segoe UI,sans-serif" font-size="30" font-weight="800" fill="#cdd6f4">Pueblo Police - You can not take photos!</text>
</svg></figure>
<p>So here I am, driving home and I see cops, and a car partly in a building. These are the things I like to photograph. So I proceeded to take photos (All the while staying VERY out of the way) and then a female cop approaches me and tells me they are investigating and &ldquo;I Don&rsquo;t need you taking photos&rdquo; so I ask if I can stand across the way, she reluctantly said yes.<br>
So I walk to the other curb and take an overview photo, then a male cop approaches and tells me they are investigating and if I continue they will seize my camera as evidence. I tried to state that I was not in the way, and such but it was of no use. I was told to leave, had I stayed jail was likely my next stop.</p>
<p>So my rights were likely violated, a complaint has been filled. What will happen? Nothing I bet.</p>
<p>This type of abuse by those in law enforcement is what gives them a bad name. Photography is NOT A CRIME.</p>
<p>Why is this such an issue?</p>
<p>I have heard of others being hassled (MUCH worse than this) however this is enough for me to get motivated. I will now take every chance possible to photograph cops, if they are eating, if they are doing anything&hellip;I WILL BE THERE.</p>
<p>Police, do some police work and leave photographers alone!</p>
]]></content:encoded></item><item><title>The night aliens abducted me</title><link>https://errorzap.com/posts/night-aliens-abducted-me/</link><pubDate>Tue, 28 Jul 2009 10:16:00 -0600</pubDate><guid>https://errorzap.com/posts/night-aliens-abducted-me/</guid><description>&amp;ndash;Originally Posted January 25th 2006 on Myspace &amp;ndash;This is a re-cap of my night last night. Hope it makes some sense!Ok so I went outside to light a small</description><content:encoded><![CDATA[<figure style="margin:0 0 26px">
<svg viewBox="0 0 800 200" xmlns="http://www.w3.org/2000/svg" style="width:100%;border-radius:14px">
  <defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
    <stop offset="0" stop-color="#1e1e2e"/><stop offset="1" stop-color="#11111b"/></linearGradient></defs>
  <rect width="800" height="200" fill="url(#g)"/>
  <rect width="800" height="200" fill="none" stroke="#94e2d5" stroke-width="2" opacity="0.5" rx="14"/>
  <circle cx="660" cy="100" r="120" fill="#94e2d5" opacity="0.08"/>
  <text x="56" y="118" font-size="86">👽</text>
  <text x="170" y="92" font-family="ui-monospace,monospace" font-size="13" fill="#94e2d5" letter-spacing="3">ERROR ZAP // ARCHIVE</text>
  <text x="170" y="132" font-family="-apple-system,Segoe UI,sans-serif" font-size="30" font-weight="800" fill="#cdd6f4">The night aliens abducted me</text>
</svg></figure>
<p>&ndash;Originally Posted January 25th 2006 on Myspace &ndash;<br>
This is a re-cap of my night last night. Hope it makes some sense!</p>
<p>Ok so I went outside to light a small fire lastnight, just after dark. I had it goin pretty good, and threw one more pieve of paper in it just to be sure..hehe&hellip;(I love fire, controlled fire that is).<br>
Next thing I know I am inside, I HAVE NO IDEA what is going on, shelly is panicing I panic. I dont remember anything I did for the whole day, and I am covered in mud and my glasses are missing. Next thing that happens my mom + sis show up, as does the sheriff and ambulance. They do some blood tests, find my blood sugar to be low. Aparently I was having a hard time breathing and sweating real bad. So I get a ride to the hospital in the ambulance, and get put on oxygen as I couldnt breath.<br>
Upon arrival I am immeidately brought to a room and tests begin. They ask me if I know who I am, what day it is (I never know that) and who the prez is. So in the end I was at the hospital for a while doin blood work, and several xrays. Turns out I hurt myself pretty bad falling wherever I fell. My right ankle, right wrist, and back are in extreme pain. So I am given two seperate morphine shots, which just barely cool the pain. After all is said and done they dont really know what happened, and everything comes back ok.<br>
Today (the day after) my whole body hurts really bad. My jaw hurts so bad I cant really chew, and I cant move very well at all. I have numerous cuts and scrapes all over. Luckily we found my glasses later on lastnight, nowhere near where I should have been.<br>
I still have no idea what happened, and I dont remember anything that happened up until the ambulance came. I have never experienced not knowing what is happening or how I got inside, and it was the most freightning thing I have had to deal with.<br>
Somehow I walked from the fire, past the front door apparently fell and knocked my glasses off, and then fell a few more times on the way to the door it seems. I wish never to repeat this, and hope you dont either!</p>
]]></content:encoded></item><item><title>Pueblo County Sheriff</title><link>https://errorzap.com/posts/pueblo-county-sheriff/</link><pubDate>Tue, 28 Jul 2009 10:15:00 -0600</pubDate><guid>https://errorzap.com/posts/pueblo-county-sheriff/</guid><description>&amp;ndash;originally posted January 30, 2006 on MySpace &amp;ndash;Allrighty,So earlier this morning I was at work with a friend I met on MySpace, this was our first time m</description><content:encoded><![CDATA[<figure style="margin:0 0 26px">
<svg viewBox="0 0 800 200" xmlns="http://www.w3.org/2000/svg" style="width:100%;border-radius:14px">
  <defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
    <stop offset="0" stop-color="#1e1e2e"/><stop offset="1" stop-color="#11111b"/></linearGradient></defs>
  <rect width="800" height="200" fill="url(#g)"/>
  <rect width="800" height="200" fill="none" stroke="#f9e2af" stroke-width="2" opacity="0.5" rx="14"/>
  <circle cx="660" cy="100" r="120" fill="#f9e2af" opacity="0.08"/>
  <text x="56" y="118" font-size="86">🚨</text>
  <text x="170" y="92" font-family="ui-monospace,monospace" font-size="13" fill="#f9e2af" letter-spacing="3">ERROR ZAP // ARCHIVE</text>
  <text x="170" y="132" font-family="-apple-system,Segoe UI,sans-serif" font-size="30" font-weight="800" fill="#cdd6f4">Pueblo County Sheriff</text>
</svg></figure>
<p>&ndash;originally posted January 30, 2006 on MySpace &ndash;</p>
<p>Allrighty,<br>
So earlier this morning I was at work with a friend I met on MySpace, this was our first time meeting. So that was damn cool just to begin with!<br>
So we wound up at work, and I parked out in front of the gate (being too lazy to open it and drive through). We walked up the the building and in we went. Gave her a tour, and then we sat on a couch by the front door. We were talking for about an hour, and wound up talking about ghosts and such.<br>
Now all of the sudden I hear the &ldquo;Ding dong&rdquo; noise indicating the front door has opened&hellip;this alone freaks me out. So I jump out of the couch and in front of the second front door&hellip;what do i see?<br>
Nothing less than 4 large men, dressed all in black guns drawn and pointed at me! Now I jump back in a bit of shock&hellip;Shortly after I realize they are Sheriff&rsquo;s officers and they order me not to move&hellip;im fine with that.<br>
They ask who I am, etc etc&hellip;I tell them security, they also question Rachael, who is now about to have a heart attack over by the couch. One of the officers stays with us while the rest check out the building, guns drawn.</p>
<p>Soooooo after all this they run clearances on us, and have my demonstrate the key to the building I have works.. (hey, maybe I should lock the door next time?)&hellip;Convinced that we are not criminals and mearly security, they clear with dispatch letting them know all is ok.<br>
I look outside, their are now 4 sheriff&rsquo;s cars surrounding my car.<br>
So anyway, the one officer happened to be the one who was at my house earlier this week&hellip;when I had some medical issues, so I think that helped him believe me a bit more.<br>
All in all, I was and still am quite shaken up, never had guns pointed at me! Good thing I wasn&rsquo;t holding anything, and no one got trigger happy.</p>
<p>So ya, Rachael my nights don&rsquo;t normally go like that! I hope you are not scared for life to visit me again!! LoL!</p>
<p>Soooo Lessons learned:<br>
Dont park suspicously, or in my case lazily.<br>
Dont leave doors unlocked!</p>
<p>Well, I am about ready to sleep now, been a long night!<br>
See ya!</p>
]]></content:encoded></item><item><title>I Miss</title><link>https://errorzap.com/posts/i-miss/</link><pubDate>Tue, 28 Jul 2009 10:11:00 -0600</pubDate><guid>https://errorzap.com/posts/i-miss/</guid><description>&amp;ndash;Originally Posted April 10, 2006 on Myspace &amp;ndash;I miss the days when going online required a trip to grandmas houseThe days when going online was a rare, a</description><content:encoded><![CDATA[<figure style="margin:0 0 26px">
<svg viewBox="0 0 800 200" xmlns="http://www.w3.org/2000/svg" style="width:100%;border-radius:14px">
  <defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
    <stop offset="0" stop-color="#1e1e2e"/><stop offset="1" stop-color="#11111b"/></linearGradient></defs>
  <rect width="800" height="200" fill="url(#g)"/>
  <rect width="800" height="200" fill="none" stroke="#a6adc8" stroke-width="2" opacity="0.5" rx="14"/>
  <circle cx="660" cy="100" r="120" fill="#a6adc8" opacity="0.08"/>
  <text x="56" y="118" font-size="86">💭</text>
  <text x="170" y="92" font-family="ui-monospace,monospace" font-size="13" fill="#a6adc8" letter-spacing="3">ERROR ZAP // ARCHIVE</text>
  <text x="170" y="132" font-family="-apple-system,Segoe UI,sans-serif" font-size="30" font-weight="800" fill="#cdd6f4">I Miss</text>
</svg></figure>
<p>&ndash;Originally Posted April 10, 2006 on Myspace &ndash;</p>
<p>I miss the days when going online required a trip to grandmas house<br>
The days when going online was a rare, and thrilling event for me.<br>
I could meet new people, see new things, and be forever fascinated by all that the internet was.</p>
<p>I miss the days when I could have fun! The days back at the old car-lot, where I would sit in an unused office (my office!) and play on the computer. An old 386 that barely ran Windows 95. But it was mine, my first computer. I loved it. I learned most of what I know based on that PC. I was eager to take it apart, and build it back up. Getting a new soundcard, was the most amazing thing ever. My windows could finally talk!</p>
<p>The days when worrying were limited to only, would I be able to do all the fun things I want today?<br>
The days when playing a new computer game was the thrill of a lifetime.</p>
<p>Those days seem very distant now. I have trouble finding anything to satisfy my need to have fun. Newer games are too complex and involved to just sit, and play. And the older games have lost their appeal to me.</p>
<p>The days of playing Simcity (now called Simcity Classic, gosh that makes me old!) for hours on end, and never being sick of it.</p>
<p>So I sit here, listening to a hidden piece of music in Windows XP&hellip;wondering..what is their to do? Their must be something that interests me, but yet, what is it? I cant find it, no matter how hard I try. I sit and think, how boring my life has become. And I want to change that, really, I do. But how? What does one do, to change how their life has become?</p>
<p>Everyday I wake up, only to lay in bed. Why get up, I have nothing to do! And on the remote chance I could do something, I most likely dont feel well enough to actually do anything about it. It is an endless loop, one I try desperately to break free from. But it seems the harder I try to break free, the harder it pushes me back. It is a loop made from alien material, material that can not be broken.</p>
<p>So here I am, 3am. Lost. Confused&hellip;and bored!<br>
Good day,<br>
Justin</p>
]]></content:encoded></item><item><title>Ubuntu</title><link>https://errorzap.com/posts/ubuntu/</link><pubDate>Tue, 01 May 2007 09:25:00 -0600</pubDate><guid>https://errorzap.com/posts/ubuntu/</guid><description>Recently I got fed up with windows constant problems and crashes. I have (had) an XP install on this machine and after only a few months I had more problem</description><content:encoded><![CDATA[<figure style="margin:0 0 26px">
<svg viewBox="0 0 800 200" xmlns="http://www.w3.org/2000/svg" style="width:100%;border-radius:14px">
  <defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
    <stop offset="0" stop-color="#1e1e2e"/><stop offset="1" stop-color="#11111b"/></linearGradient></defs>
  <rect width="800" height="200" fill="url(#g)"/>
  <rect width="800" height="200" fill="none" stroke="#fab387" stroke-width="2" opacity="0.5" rx="14"/>
  <circle cx="660" cy="100" r="120" fill="#fab387" opacity="0.08"/>
  <text x="56" y="118" font-size="86">🐧</text>
  <text x="170" y="92" font-family="ui-monospace,monospace" font-size="13" fill="#fab387" letter-spacing="3">ERROR ZAP // ARCHIVE</text>
  <text x="170" y="132" font-family="-apple-system,Segoe UI,sans-serif" font-size="30" font-weight="800" fill="#cdd6f4">Ubuntu</text>
</svg></figure>
<p>Recently I got fed up with windows constant problems and crashes. I have (had) an XP install on this machine and after only a few months I had more problems than I care to have, and they were weird, annoying problems with no clear fix. Of course windows usually fix is, wipe the drive and reinstall the OS. While this works, I hate it. It takes time and I customize every aspect of my system, so this takes a lot of time to reproduce each time.</p>
<p>Given that ive been a windows guy my whole life, starting with 3.1 the thought of switching to another OS, is freighting. Or at least it was.</p>
<p>And so I began an install of Ubuntu Edgy, it was a LiveCD so I expected to play with the new OS while it installed. Unfortunately I got some weird error and the cd wouldnt load, this was a compatibility problem with my motherboard (Asus m2n32sli), after googling the error I found an easy fix. All I had to do was update the BIOS, which took care of the boot problem and a few others :)</p>
<p>Upon booting into the liveCD I found it ran quite nice considering it wasnt installed and all running from memory and CD. So I installed, and well&hellip;it couldnt have been easier. Point and click the whole way. Faster than installing windows, and it seemed like less work too.</p>
<p>Upon rebooting into my new Ubuntu installation I was greated with gnome, and my learning began. Everything worked out of the box, but not quite how I wanted. The only things I needed to fix (and they were only cause im picky) was some mouse settings (scroll wheel, back button) and my video drivers. My only real problem in linux so far has been video drivers. I unfortunately have an ATI card and I spent way, way too much time trying to make this work.</p>
<p>Fastforward around 2 months, I have no installed Kubuntu Feisty and it went really well also. I have learned that linux is not windows (good one to keep in mind), ive learned many things are easier in linux, some are harder. But all in all kubuntu, ubuntu, etc, seem to be a great alternative OS.</p>
<p>We shall call this part 1. If I ever feel like it I will write part 2 :)</p>
]]></content:encoded></item><item><title>Pandora</title><link>https://errorzap.com/posts/pandora/</link><pubDate>Wed, 04 Oct 2006 07:40:00 -0600</pubDate><guid>https://errorzap.com/posts/pandora/</guid><description>Pandora is an amazing product offering to show you new music that you will like. And it does in fact do what it claims.I have for a long time wanted to fin</description><content:encoded><![CDATA[<figure style="margin:0 0 26px">
<svg viewBox="0 0 800 200" xmlns="http://www.w3.org/2000/svg" style="width:100%;border-radius:14px">
  <defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
    <stop offset="0" stop-color="#1e1e2e"/><stop offset="1" stop-color="#11111b"/></linearGradient></defs>
  <rect width="800" height="200" fill="url(#g)"/>
  <rect width="800" height="200" fill="none" stroke="#cba6f7" stroke-width="2" opacity="0.5" rx="14"/>
  <circle cx="660" cy="100" r="120" fill="#cba6f7" opacity="0.08"/>
  <text x="56" y="118" font-size="86">🎵</text>
  <text x="170" y="92" font-family="ui-monospace,monospace" font-size="13" fill="#cba6f7" letter-spacing="3">ERROR ZAP // ARCHIVE</text>
  <text x="170" y="132" font-family="-apple-system,Segoe UI,sans-serif" font-size="30" font-weight="800" fill="#cdd6f4">Pandora</text>
</svg></figure>
<p>Pandora is an amazing product offering to show you new music that you will like. And it does in fact do what it claims.</p>
<p>I have for a long time wanted to find music that was similar to artists I liked, but I had no easy way of doing this. Along comes Pandora.</p>
<p>You simply enter an artist or a song, and Pandora does its magic and starts playing songs that are similar. It really does an amazing job and works very well. It requires nearly no effort on your part, you just sit and listen. Which is how it should be!</p>
<p>Pandora does not allow you to play specific songs due to licensing agreements, but this is really no issue considering how well it performs at playing music you will like. You rate songs as you go along (if you want to) and it learns what you like as time goes by.</p>
<p>I would highly recomend Pandora to anyone who is looking to hear new and amazing music with very little effort.</p>
<p>Oh ya, its free.</p>
<p><a href="http://www.pandora.com/">http://www.pandora.com/</a></p>
]]></content:encoded></item><item><title>Logitech® Cordless Desktop® MX™ 5000 Laser= Piece of crap</title><link>https://errorzap.com/posts/logitech-cordless-desktop-mx-5000/</link><pubDate>Wed, 04 Oct 2006 07:27:00 -0600</pubDate><guid>https://errorzap.com/posts/logitech-cordless-desktop-mx-5000/</guid><description>So recently I purchased the Logitech® Cordless Desktop® MX™ 5000 Laser . I was excited, a new toy! That excitement quickly vanished and turned to frustrati</description><content:encoded><![CDATA[<figure style="margin:0 0 26px">
<svg viewBox="0 0 800 200" xmlns="http://www.w3.org/2000/svg" style="width:100%;border-radius:14px">
  <defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
    <stop offset="0" stop-color="#1e1e2e"/><stop offset="1" stop-color="#11111b"/></linearGradient></defs>
  <rect width="800" height="200" fill="url(#g)"/>
  <rect width="800" height="200" fill="none" stroke="#f38ba8" stroke-width="2" opacity="0.5" rx="14"/>
  <circle cx="660" cy="100" r="120" fill="#f38ba8" opacity="0.08"/>
  <text x="56" y="118" font-size="86">🖱</text>
  <text x="170" y="92" font-family="ui-monospace,monospace" font-size="13" fill="#f38ba8" letter-spacing="3">ERROR ZAP // ARCHIVE</text>
  <text x="170" y="132" font-family="-apple-system,Segoe UI,sans-serif" font-size="30" font-weight="800" fill="#cdd6f4">Logitech® Cordless Desktop® MX™ 5000 Laser</text>
</svg></figure>
<p>So recently I purchased the <a href="http://www.logitech.com/index.cfm/products/details/US/EN,CRID=2158,CONTENTID=10776">Logitech® Cordless Desktop® MX™ 5000 Laser</a> . I was excited, a new toy! That excitement quickly vanished and turned to frustration and anger&hellip;</p>
<p>Let me tell you what is wrong with this product.</p>
<ol>
<li>It uses Logitech&rsquo;s craptastic Setpoint software. It randomly resets settings to default, so if you set a button to do something, once and a while it randomly just reverts to default settings. Logitech&rsquo;s fix for this is to reinstall setpoint. Guess what? That doesnt work.</li>
<li>Its a wireless keyboard and mouse, but you better not have the mouse or keyboard more than a couple feet from the bluetooth adapter. They claim it has amazing range. It actually has a range of only a few feet, and even then isnt very reliable. It will periodically loose connection and then ALL of the sudden your mouse will move all over the screen, as if it were controlled by a higher power.</li>
<li>The keyboard has this amazing power save feature, which results in double letters once and a while. So if you leave it alone and begin typing, you will have two of whatever the first letter is you type.</li>
<li>The display, is pretty much useless. The angle is wrong (for me anyway) to be able to see it clearly. Oh and reference problem #1&hellip;if you change the settings for the display, they will revert to default every once and a while.</li>
</ol>
<p>Now I must say the mouse itself was quite nice, and felt good to use. If it werent for the lag, and connection problems it would be a nice little mouse.</p>
<p>All in all I felt this was a terrible wast of money so I took this lovely set back. This is the FIRST time I have ever had trouble with Logitech gear and I was very disapointed. I replaced the combo with a Saitek keyboard (which glows, and works great) and a different Logitech mouse, which I used for about two weeks before returning. This was due to the setpoint problems as well, and Logitech seems to be unable to fix this problem. And from searching online I am far from the only one with these issues.</p>
<p>Bottom line, the Logitech MX 5000 is a poorly designed and poorly supported combo. I would highly recomend you stay away from it.</p>
<p>I would also recomend you do not purchase ANY Logitech mouse that uses Setpoint software. Unless you dont mind the default settings.</p>
<p>I have been a longtime Logitech fan, and a loyal customer. After this I am unsure if I will ever use their products. Logitech, I am sorry to lose you as a good companion!</p>
]]></content:encoded></item><item><title>Firefox Download Action Blank Fix</title><link>https://errorzap.com/posts/firefox-download-action-blank-fix/</link><pubDate>Tue, 27 Jun 2006 11:55:00 -0600</pubDate><guid>https://errorzap.com/posts/firefox-download-action-blank-fix/</guid><description>So I came across this wonderful problem earlier, and it took more searching than I like to find a fix. So here it is for anyone else who has the problem.Pr</description><content:encoded><![CDATA[<figure style="margin:0 0 26px">
<svg viewBox="0 0 800 200" xmlns="http://www.w3.org/2000/svg" style="width:100%;border-radius:14px">
  <defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
    <stop offset="0" stop-color="#1e1e2e"/><stop offset="1" stop-color="#11111b"/></linearGradient></defs>
  <rect width="800" height="200" fill="url(#g)"/>
  <rect width="800" height="200" fill="none" stroke="#fab387" stroke-width="2" opacity="0.5" rx="14"/>
  <circle cx="660" cy="100" r="120" fill="#fab387" opacity="0.08"/>
  <text x="56" y="118" font-size="86">🦊</text>
  <text x="170" y="92" font-family="ui-monospace,monospace" font-size="13" fill="#fab387" letter-spacing="3">ERROR ZAP // ARCHIVE</text>
  <text x="170" y="132" font-family="-apple-system,Segoe UI,sans-serif" font-size="30" font-weight="800" fill="#cdd6f4">Firefox Download Action Blank Fix</text>
</svg></figure>
<p>So I came across this wonderful problem earlier, and it took more searching than I like to find a fix. So here it is for anyone else who has the problem.</p>
<p>Problem: Clicking &ldquo;view &amp; edit actions&rdquo; in firefox to set file download actions results in an empty window with no possibility of changing anything</p>
<p>Solution: Taken from: <a href="https://bugzilla.mozilla.org/show">https://bugzilla.mozilla.org/show</a>_bug.cgi?id=308204</p>
<p>replaced the blanks in all installation paths of the mimetypes.rdf (located in your profile directory)<br>
C:\Program Files...) by an underscore : (C:\Program_Files...) and it works<br>
now perfectly&hellip;</p>
<p>So..replace all the blanks on the path= part and all works fine after a restart of firefox. Why? heck if I know, it just works!</p>
]]></content:encoded></item></channel></rss>