<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: C W</title>
    <description>The latest articles on DEV Community by C W (@moersebene).</description>
    <link>https://dev.to/moersebene</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4021314%2F03f47fcf-d610-4210-b273-e848d38d27fc.png</url>
      <title>DEV Community: C W</title>
      <link>https://dev.to/moersebene</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/moersebene"/>
    <language>en</language>
    <item>
      <title>I spent about a week and a half building a desktop app for Fooocus, then I panicked about the wrong thing.</title>
      <dc:creator>C W</dc:creator>
      <pubDate>Tue, 18 Aug 2026 08:13:04 +0000</pubDate>
      <link>https://dev.to/moersebene/i-spent-about-a-week-and-a-half-building-a-desktop-app-for-fooocus-then-i-panicked-about-the-wrong-253p</link>
      <guid>https://dev.to/moersebene/i-spent-about-a-week-and-a-half-building-a-desktop-app-for-fooocus-then-i-panicked-about-the-wrong-253p</guid>
      <description>&lt;p&gt;I spent about a week and a half building &lt;a href="https://github.com/chantleyw/Fooocus-Front" rel="noopener noreferrer"&gt;Fooocus Front&lt;/a&gt;, a Windows desktop app that installs, launches and drives &lt;a href="https://github.com/lllyasviel/Fooocus" rel="noopener noreferrer"&gt;Fooocus&lt;/a&gt; without a terminal, a console window or a setup guide.&lt;/p&gt;

&lt;p&gt;Then someone told me about ComfyUI.&lt;/p&gt;

&lt;p&gt;That annoying and sinking feeling is what this post is about, and if I'm being honest I still feel it as I'm writing this post. But having gone and checked properly, I'd say I probably have been worrying about the wrong thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built, briefly
&lt;/h2&gt;

&lt;p&gt;Fooocus is excellent and deliberately minimal: one page, no sampler dropdowns, no CFG sliders. I'm a visual person and wanted something that feels like a place to make pictures, so I built one.&lt;/p&gt;

&lt;p&gt;The interesting parts weren't the interface. They were a hidden launch (every Fooocus &lt;code&gt;.bat&lt;/code&gt; ends in &lt;code&gt;pause&lt;/code&gt;, which blocks forever with no console to press a key in, and Python block-buffers to a pipe so a quiet startup looks like it's frozen), a small Python bridge that runs inside Fooocus and puts jobs onto its own internal queue rather than scraping gradio, and an installer that sets up the right PyTorch build for whatever graphics card you own.&lt;/p&gt;

&lt;p&gt;That last one taught me the most. The obvious way to check whether an Arc machine is already configured is to run &lt;code&gt;python -c "import torch"&lt;/code&gt; and see if it works but it doesn't. Torch's Intel build needs a DLL search path the launcher sets up separately, so the probe fails on a perfectly healthy install. A check that reports a working setup as broken would trigger exactly the destructive reinstall it exists to prevent. It asks pip what's installed instead.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/chantleyw/Fooocus-Front/releases/tag/v0.1.0-beta" rel="noopener noreferrer"&gt;release notes&lt;/a&gt; have the full feature list. This post is about what happened next.&lt;/p&gt;

&lt;h2&gt;
  
  
  The panic
&lt;/h2&gt;

&lt;p&gt;"ComfyUI already exists" hit me about a day ago and it still stings. I built a UI. ComfyUI is a UI. Case closed.&lt;/p&gt;

&lt;p&gt;Except when I actually checked:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ComfyUI is a node graph.&lt;/strong&gt; I wanted something &lt;em&gt;simpler&lt;/em&gt; than Fooocus's one-page form. ComfyUI goes in the opposite direction. Arguing I should have switched is arguing I should have solved a complexity problem by adopting more complexity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Intel Arc is not a solved path there.&lt;/strong&gt; I have an A770. Running ComfyUI on Arc means community install scripts or a third-party portable XPU build. Both are real options, both are maintained by people who aren't the ComfyUI team, and neither is "download and go."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SwarmUI is worse for my hardware.&lt;/strong&gt; SwarmUI is the honest competitor, since it puts a friendly UI over a ComfyUI backend. But its maintainer's own answer in the &lt;a href="https://github.com/mcmonkeyprojects/SwarmUI/discussions/474" rel="noopener noreferrer"&gt;Intel GPU discussion&lt;/a&gt; amounts to this: Swarm runs a stock ComfyUI as its backend, so install the IPEX prerequisites yourself and hope. Mine is verified on the card I own, and it generates from the moment the installer finishes.&lt;/p&gt;

&lt;p&gt;So on the evidence, the week and a half I spent wasn't a duplicate. I'd like to say that settled it for me, but it hasn't quite yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing I should have been worried about
&lt;/h2&gt;

&lt;p&gt;While writing this post I went and read the part of the Fooocus README I skipped like the fool I am.&lt;/p&gt;

&lt;p&gt;Fooocus is in &lt;a href="https://github.com/lllyasviel/Fooocus" rel="noopener noreferrer"&gt;limited long-term support, bug fixes only&lt;/a&gt;, with no current plans to adopt newer model architectures. It's SDXL and it will stay SDXL. The README itself points people who want Flux towards WebUI Forge, ComfyUI or SwarmUI. The last release was v2.5.0, in 2024.&lt;/p&gt;

&lt;p&gt;I built a careful installer, a repair system and a native interface for an engine that has stopped moving.&lt;/p&gt;

&lt;p&gt;That's the actual critique, and nobody made it to me. The obvious objection, "ComfyUI exists," was wrong and easy to answer. The real one was sitting in the README I already opened.&lt;/p&gt;

&lt;p&gt;There's a second one I found the same afternoon: my Intel Arc setup installs &lt;code&gt;intel-extension-for-pytorch&lt;/code&gt;, which &lt;a href="https://intel.github.io/intel-extension-for-pytorch/" rel="noopener noreferrer"&gt;Intel retired at the end of March 2026&lt;/a&gt; after upstreaming support into native PyTorch. My installer's headline feature is built on a package that's deprecated. Moving that path over to native PyTorch XPU is going into the next release.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where that leaves it
&lt;/h2&gt;

&lt;p&gt;Not in the bin. A few things survive the reckoning.&lt;/p&gt;

&lt;p&gt;The deprecation is less dramatic than it sounds. The wheels still exist and still work, and Fooocus pins &lt;code&gt;torch==2.1.0&lt;/code&gt; anyway, which is exactly what my Arc branch installs. It isn't arbitrarily stale, it's matched to what Fooocus asks for. The migration also looks more tractable than I first assumed: RTX 50-series owners already run Fooocus on torch 2.7 and later to get Blackwell kernels, so the codebase tolerates a newer torch better than its pins suggest.&lt;/p&gt;

&lt;p&gt;The bridge design matters more than I realised. Because it talks to Fooocus's internal queue rather than its web UI, retargeting it at a fork is a small change rather than a rewrite. Several forks exist, though check their commit history before trusting any of them; the one most often recommended hasn't shipped since 2024.&lt;/p&gt;

&lt;p&gt;And SDXL being frozen isn't the same as SDXL being bad. It still makes good images, on hardware people already own.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lesson, such as it is
&lt;/h2&gt;

&lt;p&gt;"Check whether someone already built this" is not the useful question. Something has always already been built.&lt;/p&gt;

&lt;p&gt;The useful question is whether the thing that exists solves &lt;em&gt;your&lt;/em&gt; problem, and then, separately, whether the foundation you're building on is still moving. I got the first one right by accident and missed the second one completely.&lt;/p&gt;

&lt;p&gt;If you're about to start something like this, don't be like me: read the project status section of the README before you write a line. It's usually two paragraphs, and it's usually the most important thing on the page.---&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;a href="https://github.com/chantleyw/Fooocus-Front/releases/tag/v0.1.0-beta" rel="noopener noreferrer"&gt;Fooocus Front v0.1.0-beta&lt;/a&gt; is out now. GPL-3.0, Windows only, unsigned installer, and Intel Arc is the only card it's been verified on. Every bit of the actual image generation is &lt;a href="https://github.com/lllyasviel/Fooocus" rel="noopener noreferrer"&gt;Fooocus&lt;/a&gt;, by lllyasviel and its contributors. They solved the hard part.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>rust</category>
      <category>tauri</category>
      <category>ai</category>
    </item>
    <item>
      <title>Puppergram: gram by gram, day by day.</title>
      <dc:creator>C W</dc:creator>
      <pubDate>Sun, 16 Aug 2026 18:50:57 +0000</pubDate>
      <link>https://dev.to/moersebene/puppergram-gram-by-gram-day-by-day-1dk1</link>
      <guid>https://dev.to/moersebene/puppergram-gram-by-gram-day-by-day-1dk1</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/weekend-2026-08-13"&gt;Weekend Challenge: Dog Days Edition&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;My dog just had ten puppies.&lt;/p&gt;

&lt;p&gt;If you have never done this, here is the part nobody warns you about: newborn puppies all look identical, they cannot regulate their own temperature, and the only reliable sign that one is in trouble is that it stops gaining weight. Not the crying or looking thin, but &lt;strong&gt;Weight.&lt;/strong&gt; A puppy that fails to gain for 24 hours is in serious trouble a day or more before anything is visible to you.&lt;/p&gt;

&lt;p&gt;So you weigh them. Every day, all ten, and you write it in a notebook. And then at 2am on day five you are staring at a page of numbers trying to work out whether green is genuinely falling behind or whether you are just tired.&lt;/p&gt;

&lt;p&gt;That is what I built. &lt;strong&gt;Puppergram is not a weight log with a calendar attached — it is an early-warning system.&lt;/strong&gt; You record weights; it tells you which puppy needs attention tonight.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs0z63do9h5wqqjnaa75b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs0z63do9h5wqqjnaa75b.png" alt="The critical alert firing on a six-day-old litter" width="799" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Seven hard-coded rules run over every weight you have entered:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Severity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Failure to gain&lt;/td&gt;
&lt;td&gt;No weight increase in 24h&lt;/td&gt;
&lt;td&gt;Warning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weight loss&lt;/td&gt;
&lt;td&gt;Any drop vs. previous entry&lt;/td&gt;
&lt;td&gt;Warning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Below birth weight&lt;/td&gt;
&lt;td&gt;Current &amp;lt; birth weight after 48h&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Critical&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Significant loss&lt;/td&gt;
&lt;td&gt;More than 10% below peak weight&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Critical&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slow doubling&lt;/td&gt;
&lt;td&gt;Under 1.8× birth weight by day 10&lt;/td&gt;
&lt;td&gt;Warning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Litter divergence&lt;/td&gt;
&lt;td&gt;More than 25% below litter median on day N&lt;/td&gt;
&lt;td&gt;Warning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing entry&lt;/td&gt;
&lt;td&gt;No weight logged in 36h&lt;/td&gt;
&lt;td&gt;Info&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Nothing is inferred, predicted or modelled. The rule table is printed inside the app as well, because at 3am you are entitled to know exactly what set off the alarm.&lt;/p&gt;

&lt;p&gt;Every alert carries an action, never just a label:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Green (Caeser) is 11% below its peak weight of 470 g.&lt;/strong&gt;&lt;br&gt;
Contact your vet today. Losses of this size in a neonate escalate quickly. Start supplemental feeding and check box temperature while you wait.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;🐕 &lt;a href="https://puppergram.pages.dev" rel="noopener noreferrer"&gt;puppergram.pages.dev&lt;/a&gt;&lt;/strong&gt; — press &lt;strong&gt;Load demo litter&lt;/strong&gt;. You land on a six-day-old litter of seven with one puppy already in trouble, not an empty form.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🔍 &lt;a href="https://puppergram.pages.dev/verify" rel="noopener noreferrer"&gt;puppergram.pages.dev/verify&lt;/a&gt;&lt;/strong&gt; — the buyer's half. No wallet, no account, nothing installed.&lt;/p&gt;

&lt;p&gt;Works offline once loaded, and installs as a PWA. The whelping box is in an outbuilding with no signal, so an app that needs a network is dead weight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On a phone — the 3am screen:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiht28tvnrnpmqu4hyvmm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiht28tvnrnpmqu4hyvmm.png" alt="The weigh flow: one puppy per screen, giant keypad, hold to talk" width="780" height="1688"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On desktop — the whole litter at once:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbr3apzjlcr5gjh9woukb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbr3apzjlcr5gjh9woukb.png" alt="The litter matrix" width="800" height="416"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Read green's row: &lt;code&gt;+32 +30 +32 ±0 −2 −48&lt;/code&gt;. That stall is the entire product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/chantleyw" rel="noopener noreferrer"&gt;
        chantleyw
      &lt;/a&gt; / &lt;a href="https://github.com/chantleyw/puppergram" rel="noopener noreferrer"&gt;
        puppergram
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Gram by gram, day by day. Neonatal litter monitor for dog breeders: weight-led early warning from birth to eight weeks.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Puppergram&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Gram by gram, day by day.&lt;/strong&gt; A neonatal litter monitor for dog breeders — birth to eight weeks.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Puppergram watches every puppy's daily weight gain and tells you which one needs attention &lt;em&gt;tonight&lt;/em&gt;, before there is anything to see.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/chantleyw/puppergram/docs/screenshots/alert.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fchantleyw%2Fpuppergram%2FHEAD%2Fdocs%2Fscreenshots%2Falert.png" alt="The triage engine firing a critical alert on a six-day-old litter"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://puppergram.pages.dev" rel="nofollow noopener noreferrer"&gt;https://puppergram.pages.dev&lt;/a&gt; · &lt;strong&gt;Verify a passport:&lt;/strong&gt; &lt;a href="https://puppergram.pages.dev/verify" rel="nofollow noopener noreferrer"&gt;https://puppergram.pages.dev/verify&lt;/a&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;The problem&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Neonatal mortality in dogs runs at roughly 10–30% in the first three weeks, and weight is the single most predictive vital sign — a puppy that fails to gain for 24 hours, or slips below its birth weight, is in serious trouble long before anything is visible. Most hobby breeders record weights in a paper notebook, where a stalling trend is invisible until it is a crisis. Puppergram is not a weight log with a calendar attached; it is an early-warning system, and the triage engine is the product.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How alerts work&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Every rule is hard-coded, transparent, and applied only…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/chantleyw/puppergram" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/chantleyw/puppergram.git
&lt;span class="nb"&gt;cd &lt;/span&gt;puppergram &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;It runs with no API keys.&lt;/strong&gt; Voice degrades to the browser's own recognition, and the keypad is always there.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;React 18 + TypeScript + Vite + Tailwind, Dexie over IndexedDB, Recharts, deployed on Cloudflare Pages with Pages Functions. No accounts, no auth, no server-side database — everything lives on the device.&lt;/p&gt;

&lt;h3&gt;
  
  
  Nothing computed is ever stored
&lt;/h3&gt;

&lt;p&gt;The whole app is one pure function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;buildLitterView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;litter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;puppies&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;weights&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;care&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nx"&gt;LitterView&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Weights in, everything out: day columns, daily gains, litter medians, alerts, milestone state, feeding volumes, temperature targets. There is no refresh button and no recalculate step because there is nothing to keep in sync. Log one weight by voice and the matrix, chart, alert banner, timeline and care cards all re-render from the same derivation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Birth weight is not a stored field.&lt;/strong&gt; It is the earliest weight entry. Store it separately and the two eventually disagree.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;now&lt;/code&gt; is a parameter rather than a &lt;code&gt;Date.now()&lt;/code&gt; call, which makes every time-window rule deterministic — and testable.&lt;/p&gt;

&lt;h3&gt;
  
  
  118 tests, because a false alarm is worse than no app
&lt;/h3&gt;

&lt;p&gt;A false critical at 3am destroys trust in everything else the app says. So each threshold is tested in both directions — 10% below peak does &lt;em&gt;not&lt;/em&gt; fire, 10.1% does — and there is a test asserting that a &lt;strong&gt;normally growing litter produces zero alerts.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two pieces of deliberate restraint came out of writing those:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Below birth weight has a 48-hour grace window.&lt;/strong&gt; Healthy newborns dip for the first day or two. Firing on that would train me to ignore the app by day three.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Litter divergence needs at least three weighed puppies.&lt;/strong&gt; A median of two is just an average, and "below average" is not a finding.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Two features that came from using it on my own litter
&lt;/h3&gt;

&lt;p&gt;I built this while the litter was already a week old, which surfaced things I would never have designed for:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backlogging.&lt;/strong&gt; I had a spreadsheet full of weights and had only typed in day 0 and day 9. The realistic case is not "I will enter each weight as I take it." So the weigh flow records against &lt;em&gt;any&lt;/em&gt; day, with chips showing which days are complete, partial, or empty — the gaps are visible rather than remembered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Eighteen collar colours.&lt;/strong&gt; I had ten puppies and the app had exactly ten collars. Running out mid-whelp is a real failure, not a theoretical one.&lt;/p&gt;

&lt;h3&gt;
  
  
  The collar spine
&lt;/h3&gt;

&lt;p&gt;Collar colour is the app's entire visual language. Every row carries a yarn-coloured spine; the chart draws each puppy's line in its literal collar colour; the weigh flow tints its header to whoever you are holding. &lt;strong&gt;The result is that the chart needs no legend anywhere&lt;/strong&gt; — you already identify puppies by exactly this system, from the wool round their necks.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;h3&gt;
  
  
  🎙️ Best Use of ElevenLabs
&lt;/h3&gt;

&lt;p&gt;You are holding a wet puppy in one hand and a kitchen scale in the other, in a dim shed, at 3am. &lt;strong&gt;Typing is the worst possible interface.&lt;/strong&gt; Saying &lt;em&gt;"blue, two forty five"&lt;/em&gt; is the best one.&lt;/p&gt;

&lt;p&gt;ElevenLabs Scribe (&lt;code&gt;scribe_v2&lt;/code&gt;) handles transcription through a Cloudflare Pages Function using a server-side key. &lt;strong&gt;There is no key entry field anywhere in the UI.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I use ElevenLabs for speech-to-text only, and that is a deliberate split.&lt;/strong&gt; Transcription is where accuracy decides whether the feature works at all — mishearing &lt;em&gt;"two forty five"&lt;/em&gt; as 240 writes a wrong weight into what is effectively a medical record, and that error propagates into every gain, median and alert downstream. That earns a real model. Reading a line back, though, is solved: every browser does it offline, instantly, free. A cloud readback would be silent exactly when the app is needed most. So &lt;strong&gt;ElevenLabs transcribes, and the device speaks.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Breeders do not talk like a form, so the parser handles what people actually say:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Spoken&lt;/th&gt;
&lt;th&gt;Parsed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;blue two forty five&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;245 g&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;blue two hundred and forty five&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;245 g&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;blue two four five&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;245 g&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;blue one ten&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;110 g&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The interesting case is the &lt;strong&gt;elided hundred&lt;/strong&gt; — &lt;em&gt;"two forty five"&lt;/em&gt; means 245, so a single digit followed by a tens word becomes hundreds-plus-tens. It also copes with punctuation Scribe adds (&lt;code&gt;"Blue, two forty-five"&lt;/code&gt; is a real transcript, now a test case), homophones (&lt;code&gt;gray&lt;/code&gt;/&lt;code&gt;grey&lt;/code&gt;, &lt;code&gt;for&lt;/code&gt;/&lt;code&gt;four&lt;/code&gt;), and refuses anything outside a plausible 50–20000 g range rather than recording it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A parse is never committed automatically.&lt;/strong&gt; It appears as a confirmation chip with one-tap Save, Correct or Discard. Voice you cannot correct is worse than typing.&lt;/p&gt;

&lt;p&gt;Three layers, degrading silently: &lt;strong&gt;ElevenLabs → Web Speech API → the keypad, which is always on screen.&lt;/strong&gt; Any non-200 means downgrade and continue. A voice feature that dies at 3am because a quota ran out would be worse than useless.&lt;/p&gt;

&lt;h3&gt;
  
  
  ⛓️ Best Use of Solana
&lt;/h3&gt;

&lt;p&gt;The problem: a buyer is handed a printed "health record" that anyone could have typed five minutes ago. There is no way to tell a real growth history from an invented one.&lt;/p&gt;

&lt;p&gt;Puppergram already holds a timestamped, day-by-day record — exactly the artifact worth making tamper-evident.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I use Solana as a timestamping notary. No NFT is minted, and nothing but a hash goes on chain.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At handover the app builds a canonical passport (sorted keys, no whitespace, integers only, so the digest is reproducible byte-for-byte years later), takes its SHA-256, and writes one SPL Memo to devnet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here is a real sealed puppy — paste it into &lt;a href="https://explorer.solana.com/tx/jQbLD3m3zZQCGUtQ8hWfUujyRYqriLKrJcwdUPNuH137QXqZA3Lz7CMzeRs4TYxkzMjBFvC19b6ZMsK3BV5PdwK?cluster=devnet" rel="noopener noreferrer"&gt;Solana Explorer&lt;/a&gt; with the cluster set to devnet:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;jQbLD3m3zZQCGUtQ8hWfUujyRYqriLKrJcwdUPNuH137QXqZA3Lz7CMzeRs4TYxkzMjBFvC19b6ZMsK3BV5PdwK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The entire on-chain footprint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Program log: Memo (len 71): "PGRAM1:13de6b0ceda53db495804660c51cddbfe47c2c915fa1b609739c0ff1c4ef423b"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A prefix and a hash. No name, no dam, no weights, no photo. Total cost: 0.00008 SOL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The buyer's half needs nothing.&lt;/strong&gt; They scan a QR that carries the whole passport — gzipped into the URL fragment, so it never even reaches my server — and &lt;code&gt;/verify&lt;/code&gt; re-hashes it locally and compares against the chain. No wallet, no account, no app.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhij1kheeya2yg9g3ns1z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhij1kheeya2yg9g3ns1z.png" alt="The verify page" width="799" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Both verdicts are confirmed on real hardware: sealing a puppy, scanning its QR with a phone, and getting &lt;strong&gt;✓ Record verified&lt;/strong&gt; — and editing a single gram produces a clear mismatch, with both fingerprints shown side by side.&lt;/p&gt;

&lt;p&gt;Being straight about the limits: this proves a record has not been &lt;strong&gt;edited since sealing&lt;/strong&gt;. It does not prove the weights were true in the first place. It is tamper-evidence, not truth — and it is on devnet, so it is a demonstration rather than a durable guarantee.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Puppergram is a breeder's record-keeping tool. It is not veterinary advice and not a health certificate.&lt;/strong&gt; If something looks wrong, call your vet. I built it because I was doing this arithmetic on paper at 2am with ten puppies and a head torch, and a computer should have been doing it for me.&lt;/p&gt;

&lt;p&gt;MIT licensed.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>typescript</category>
      <category>learning</category>
    </item>
    <item>
      <title>The Ghost in the Machine: Hunting a Memory Leak I Couldn't See the Source Of.</title>
      <dc:creator>C W</dc:creator>
      <pubDate>Wed, 15 Jul 2026 08:09:42 +0000</pubDate>
      <link>https://dev.to/moersebene/the-ghost-in-the-machine-hunting-a-memory-leak-i-couldnt-see-the-source-of-54f1</link>
      <guid>https://dev.to/moersebene/the-ghost-in-the-machine-hunting-a-memory-leak-i-couldnt-see-the-source-of-54f1</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Smash Stories&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most bug stories start with a stack trace in your own code. This one starts with a stack trace in code I'd never seen, couldn't read, and wasn't allowed to touch — a closed-source, decades-old ERP that a critical overnight process depended on. No source, no debugger, and no PRs to link at the end of this post, because there was never any of my own code to change.&lt;/p&gt;

&lt;p&gt;What I &lt;em&gt;did&lt;/em&gt; have was a process that fell over in the small hours, a vendor whose only advice was to "call support," and a stubborn refusal to accept "it just does that sometimes" as an answer.&lt;/p&gt;

&lt;p&gt;Here's how I smashed a bug I could only ever observe from the outside.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;p&gt;Every night, an integration job that shuttled financial settlement data between two systems would throw this at me:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;System.OutOfMemoryException
   at System.Text.Encoding.GetChars(Byte[] bytes, Int32 index, Int32 count)
   at System.Text.Encoding.Convert(...)
   at &amp;lt;VendorLib&amp;gt;.SQLSupport.Extensions.ToByteArray(String s)
   at &amp;lt;VendorLib&amp;gt;.SQLSupport.Helper.Serializer.ToDataBuffer(IEntity entity)
   at &amp;lt;VendorLib&amp;gt;.SQLSupport.ServiceProxy.ActivityServiceProxy.GetNextRecord(...)
   ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;OutOfMemoryException&lt;/code&gt;. On a server with tens of gigabytes of RAM. The recommended fix from the vendor? Just a phone number.&lt;/p&gt;

&lt;p&gt;The errors clustered in a roughly 20-minute window in the early hours, and I'd learn later that interactive users were hitting the same wall during the day without anyone connecting the dots. The problem had apparently been shrugged at for a long time.&lt;/p&gt;

&lt;h2&gt;
  
  
  False trail #1: "It's corrupt data"
&lt;/h2&gt;

&lt;p&gt;My first theory was a poisoned record. The stack trace dies while serialising a string into a byte buffer — &lt;code&gt;ToByteArray&lt;/code&gt; reading some field's length and trying to allocate. A garbage length value would make it try to allocate gigabytes in one shot and throw instantly. That fit the "fails on a specific record" idea perfectly.&lt;/p&gt;

&lt;p&gt;Except it didn't hold up. The errors pointed at &lt;em&gt;multiple different datasets&lt;/em&gt;, on &lt;em&gt;different internal methods&lt;/em&gt;, not one record in one place. When even a security/session-setup call throws &lt;code&gt;OutOfMemory&lt;/code&gt;, you're not looking at one bad row — you're looking at a process that's already on its knees before it even gets to your data.&lt;/p&gt;

&lt;h2&gt;
  
  
  False trail #2: "The server's out of memory"
&lt;/h2&gt;

&lt;p&gt;The obvious next thought: But here's the thing about a &lt;code&gt;.NET&lt;/code&gt; &lt;code&gt;OutOfMemoryException&lt;/code&gt;: &lt;strong&gt;it very often has nothing to do with physical RAM.&lt;/strong&gt; It means the process couldn't get the memory &lt;em&gt;it&lt;/em&gt; asked for — and a 32-bit process can only ever address about 2GB, no matter how much RAM the machine has. That ceiling is baked into the architecture, not the hardware.&lt;/p&gt;

&lt;p&gt;So I opened Task Manager, found the long-lived process behind the failing component, and watched it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;140 MB.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The process throwing "out of memory" was sitting at 140 megabytes. The server had oceans of free RAM. This was never a RAM problem. It was an &lt;em&gt;address space&lt;/em&gt; problem — or it would be, if something were filling that 2GB up. But at 140MB? Something didn't add up. Unless I was looking at the wrong moment in the cycle.&lt;/p&gt;

&lt;p&gt;That's when I stopped guessing and started measuring.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stakeout: teaching myself what the process did all day
&lt;/h2&gt;

&lt;p&gt;I couldn't attach a profiler to vendor software in production. So I did the crude, reliable thing: a scheduled script that logged the process's committed memory, working set, and handle count once an hour, to a CSV. Keyed off the service account rather than the process ID, so it would re-find the right process automatically each day even across daily server restarts.&lt;/p&gt;

&lt;p&gt;Then I waited a day and looked at the curve.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="k"&gt;Time&lt;/span&gt;    &lt;span class="k"&gt;CommitMB&lt;/span&gt;   &lt;span class="k"&gt;Handles&lt;/span&gt;
&lt;span class="mf"&gt;10&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;00&lt;/span&gt;      &lt;span class="mf"&gt;293&lt;/span&gt;       &lt;span class="mf"&gt;2889&lt;/span&gt;
&lt;span class="mf"&gt;11&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;00&lt;/span&gt;      &lt;span class="mf"&gt;387&lt;/span&gt;       &lt;span class="mf"&gt;3779&lt;/span&gt;
&lt;span class="mf"&gt;12&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;00&lt;/span&gt;      &lt;span class="mf"&gt;485&lt;/span&gt;       &lt;span class="mf"&gt;4650&lt;/span&gt;
&lt;span class="mf"&gt;13&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;00&lt;/span&gt;      &lt;span class="mf"&gt;578&lt;/span&gt;       &lt;span class="mf"&gt;5596&lt;/span&gt;
&lt;span class="err"&gt;...&lt;/span&gt;        &lt;span class="err"&gt;...&lt;/span&gt;        &lt;span class="err"&gt;...&lt;/span&gt;
&lt;span class="mf"&gt;21&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;00&lt;/span&gt;     &lt;span class="mf"&gt;1361&lt;/span&gt;      &lt;span class="mf"&gt;13429&lt;/span&gt;
&lt;span class="mf"&gt;22&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;00&lt;/span&gt;     &lt;span class="mf"&gt;1451&lt;/span&gt;      &lt;span class="mf"&gt;14361&lt;/span&gt;   &lt;span class="err"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="k"&gt;flatlines&lt;/span&gt; &lt;span class="k"&gt;here&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;users&lt;/span&gt; &lt;span class="k"&gt;log&lt;/span&gt; &lt;span class="k"&gt;off&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mf"&gt;02&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;00&lt;/span&gt;     &lt;span class="mf"&gt;1456&lt;/span&gt;      &lt;span class="mf"&gt;14492&lt;/span&gt;
&lt;span class="mf"&gt;03&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;00&lt;/span&gt;      &lt;span class="mf"&gt;178&lt;/span&gt;       &lt;span class="mf"&gt;1781&lt;/span&gt;   &lt;span class="err"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="k"&gt;reboot&lt;/span&gt; &lt;span class="k"&gt;resets&lt;/span&gt; &lt;span class="k"&gt;it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There it was. A dead-straight climb of &lt;strong&gt;~96 MB every single hour&lt;/strong&gt;, from a low baseline right up toward the ceiling — and then a hard reset overnight. A textbook memory leak, drawn in a perfectly clean line.&lt;/p&gt;

&lt;p&gt;Two details made it click:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Handles rose in lockstep with memory&lt;/strong&gt; — about 970 per hour, tracking the megabytes almost proportionally. Memory &lt;em&gt;and&lt;/em&gt; handles leaking together is the fingerprint of unreleased references: every leaked object clings to both a chunk of memory and an OS handle. In an old reference-counted component, that's a missed release on a hot path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The leak flatlined when people logged off at night.&lt;/strong&gt; It was &lt;em&gt;usage-driven&lt;/em&gt; — every operation leaked a little, so when the work stopped, the growth stopped. Not a background timer; a per-operation leak.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And the "why 02h00?" finally made sense. The leak built all day and by the wee hours the process was near its ceiling &lt;em&gt;and&lt;/em&gt; its free space was fragmented. That's exactly when the overnight job attempted its single largest allocation — a big data pull. An already-exhausted, fragmented 2GB address space couldn't hand over one contiguous block that size, and &lt;em&gt;bang&lt;/em&gt; — &lt;code&gt;OutOfMemoryException&lt;/code&gt;. The daily reboot then wiped the slate, and the cycle began again, which is precisely why nobody had ever caught it in daylight.&lt;/p&gt;

&lt;p&gt;The 140MB I'd seen earlier? I'd happened to look just after a reboot. The stakeout caught the whole story the snapshot couldn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Confirming the culprit
&lt;/h2&gt;

&lt;p&gt;The process was a COM+ surrogate host. Its executable path gave away the last piece:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight batchfile"&gt;&lt;code&gt;&lt;span class="kd"&gt;C&lt;/span&gt;:\WINDOWS\SysWOW64\dllhost.exe &lt;span class="na"&gt;/Processid&lt;/span&gt;:&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="kd"&gt;GUID&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SysWOW64&lt;/code&gt; = 32-bit on 64-bit Windows (yes, the naming is backwards). Matching that GUID against the registered COM+ applications pinned it to one specific component — the vendor's data-access layer that &lt;em&gt;every&lt;/em&gt; operation, interactive and batch, funnelled through. One leaking process, every code path. That's why the overnight job and the daytime users failed for the same reason.&lt;/p&gt;

&lt;p&gt;Diagnosis complete:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A 32-bit COM+ component leaks memory and handles at ~96 MB/hr under load (unreleased references). Over a day it approaches the 2GB address-space ceiling; fragmentation plus the overnight job's large allocation tips it into &lt;code&gt;OutOfMemory&lt;/code&gt;. The nightly reboot masks it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So not RAM, or corrupt record. Just a reference leak meeting a 32-bit wall.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smashing it (without a compiler)
&lt;/h2&gt;

&lt;p&gt;I couldn't fix the leak since it's not my code. So the goal shifted from &lt;em&gt;fix the bug&lt;/em&gt; to &lt;em&gt;make the system resilient despite an unfixable dependency.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The component ran under COM+, and COM+ has a feature made for exactly this: &lt;strong&gt;application recycling&lt;/strong&gt;. You can tell it to tear down and respawn the process when it crosses a memory threshold — automatically, mid-life, without a server reboot.&lt;/p&gt;

&lt;p&gt;I set the memory limit to &lt;strong&gt;1 GB&lt;/strong&gt; (&lt;code&gt;1048576&lt;/code&gt; KB — a clean 1024×1024, comfortably below the 2GB wall with a full gigabyte of headroom so any large allocation still finds contiguous space):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;COM+ application → Properties → Pooling and Recycling → Memory limit (KB) = &lt;code&gt;1048576&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There was one nervous moment. The component's own logs kept saying &lt;code&gt;hasrecycled=[False]&lt;/code&gt; while messages were in process — it refused to recycle while busy, and it was &lt;em&gt;always&lt;/em&gt; busy. I braced for the fix to do nothing. But that flag was a per-instant status, not a permanent refusal: COM+ waits for a gap between operations and takes it. Under real load there were enough sub-second gaps, and it started recycling right at the 1GB mark.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before and after
&lt;/h2&gt;

&lt;p&gt;The same homemade logger that caught the bug proved the fix. Same CSV, different shape.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt; memory climbs to ~1.45GB, address space fragments, overnight job fails almost nightly, users hit errors as the day wears on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt; the process now cycles up and down — climbs to 1GB, recycles to baseline, climbs again — never approaching the ceiling. The tell-tale sign it was genuinely recycling and not just rebooting: &lt;strong&gt;the process ID changed mid-day, without a server restart&lt;/strong&gt;, with memory dropping to baseline at that exact moment.&lt;/p&gt;

&lt;p&gt;The overnight and daytime errors stopped. Days of clean logs, including busy days that used to be the worst offenders. Harmony restored — not by changing a line of the offending code, but by boxing it in so its worst habit could never reach the failure point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The twist: the bug has a sibling
&lt;/h2&gt;

&lt;p&gt;Here's the part that keeps it from being a tidy ending. A second environment runs its &lt;em&gt;own&lt;/em&gt; independent copy of the same software — same component, same leak, its own separate install. Fixing one did nothing for the other. The same recycling mitigation now has to be replicated there. A leak in shared-but-copied software isn't one bug; it's one bug per copy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm proud of, and what I learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Proud of:&lt;/strong&gt; solving it with nothing but observation. No source, no profiler, no vendor cooperation — just a stack trace read carefully, a snapshot that lied, and a homemade logger that told the truth. The whole diagnosis came from treating a black box as knowable if you measure it patiently enough.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Learned:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;OutOfMemoryException&lt;/code&gt; is a liar's name.&lt;/strong&gt; It rarely means "the machine is out of RAM." Nine times out of ten it means a single process hit its own ceiling — and for 32-bit processes that ceiling is ~2GB no matter what the server has.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A single snapshot can send you the wrong way.&lt;/strong&gt; 140MB "proved" it wasn't memory. Only the trend over time told the real story. Measure the curve, not the point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory and handles rising together&lt;/strong&gt; is a gift of a clue — it points straight at unreleased references rather than a runaway cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You don't always get to fix the bug but sometimes you fix the blast radius.&lt;/strong&gt; Recycling didn't cure the leak; it made the leak unable to hurt anyone. For a dependency you can't change, resilience &lt;em&gt;around&lt;/em&gt; it is a legitimate win.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A reboot that "fixes" something every night is a bug wearing a disguise.&lt;/strong&gt; If a scheduled restart is load-bearing, there's an unsmashed bug hiding behind it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The ghost is still in the machine — I never exorcised the leak itself. But it's in a box now, recycled back to harmless before it can ever reach the wall. Sometimes smashing a bug means making sure it can never land the hit.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>dotnet</category>
      <category>devops</category>
    </item>
    <item>
      <title>Dōjō 道場 — karate made me do it.</title>
      <dc:creator>C W</dc:creator>
      <pubDate>Sat, 11 Jul 2026 07:46:00 +0000</pubDate>
      <link>https://dev.to/moersebene/dojo-dao-chang-karate-made-me-do-it-1abj</link>
      <guid>https://dev.to/moersebene/dojo-dao-chang-karate-made-me-do-it-1abj</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/weekend-2026-07-09"&gt;Weekend Challenge: Passion Edition&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Dōjō (道場) is a Shito-Ryu based karate kata training companion, built around the Passion Edition prompt taken literally, to say that passion is the fire that drives us.&lt;/p&gt;

&lt;p&gt;The home screen is a living flame that grows with your training, fed by both your streak and the effort you put in each day. If you show up consistently, it builds and feeds the flame; if you put in a hard session, it flares up that same day. It's a real canvas particle fire and not a static graphic.&lt;/p&gt;

&lt;p&gt;The core is a count-along kata drill with a live sensei voice. Pick a kata, set the tempo, and a sensei counts you through it in Japanese — ichi, ni, san… — on the beat, with a kiai shout on the traditional counts and an optional metronome. Pause/resume mid-drill, and every kata's counts are editable, since those conventions vary by branch. Finishing a drill logs the session and feeds the flame.&lt;/p&gt;

&lt;p&gt;Rounding it out: a kata hall grouped by their respective lineage (Pinan, Shuri-te, Naha-te), with a belt-and-history view, and a one-tap Kiai button to test the chosen voice.&lt;/p&gt;

&lt;p&gt;I have been practicing karate for 30 years and my goal was to build something that feels like stepping into a dōjō, not opening a fitness dashboard — and to make the voice feel like a real sensei counting beside you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;


&lt;div class="ltag-netlify"&gt;
  &lt;iframe src="https://dojoh.netlify.app" title="Netlify embed"&gt;
  &lt;/iframe&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/chantleyw" rel="noopener noreferrer"&gt;
        chantleyw
      &lt;/a&gt; / &lt;a href="https://github.com/chantleyw/dojo" rel="noopener noreferrer"&gt;
        dojo
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Dōjō · 道場&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;A Shito-Ryu (糸東流) training companion with a live sensei voice.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Dōjō is a karate practice app built around a single emotional idea: &lt;em&gt;passion is the
fire that drives us&lt;/em&gt;. The home screen shows a flame that grows with your training
streak — a cold ember at zero, a roaring fire past 30 days. Pick a kata and a sensei
voice counts you through it in Japanese, on the beat — &lt;em&gt;ichi, ni, san…&lt;/em&gt; — with a kiai
shout on the traditional counts and an optional metronome tick.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Built for the DEV.to Weekend Challenge (Passion Edition).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;✨ Highlights&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A flame that grows with your streak.&lt;/strong&gt; Layered SVG plume with a Framer Motion
flicker, driven by &lt;code&gt;fire = min(1, streak / 30)&lt;/code&gt;. Reduced-motion holds it still.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A live count-along drill.&lt;/strong&gt; Choose a kata; the sensei counts in Japanese on the
beat, kiai-ing on the traditional counts. Tempo…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/chantleyw/dojo" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;The stack is Vite + React + TypeScript + Tailwind, with Framer Motion, Zustand for state, the Web Audio API for the metronome, and Netlify Functions for the backend. State persists per-device in localStorage — so no accounts are required.&lt;/p&gt;

&lt;p&gt;The sensei voice (ElevenLabs)&lt;br&gt;
This is the core of the app, and getting it to feel live drove most of the interesting decisions.&lt;/p&gt;

&lt;p&gt;A custom voice, built in Voice Design. The sensei is a purpose-built deep, gravelly grand-master voice created in ElevenLabs Voice Design — the kind of voice that could bark a battle command. The app pulls every voice on the account into a picker, but defaults to the sensei.&lt;/p&gt;

&lt;p&gt;Authentic Japanese counting. Numerals are generated programmatically as hiragana (いち, に, さん…, composing じゅういち for 11, and so on) and spoken with Turbo v2.5 using language_code: "ja". That language flag was the unlock because without it, an English-leaning accented voice pronounces さん like the English word "san"; with it, you get a proper sahn pronunciation.&lt;/p&gt;

&lt;p&gt;Making it land on the beat. A count drill can't afford a network round-trip per number. So on "Begin," the app pre-generates one clip per unique word, caches them as object URLs keyed by text, then plays the cached audio on each metronome beat. The count and the kiai are also given separate expressiveness profiles — the counts stay crisp and consistent, while the kiai runs at low stability / high style for a raw, shouted delivery.&lt;/p&gt;

&lt;p&gt;Keeping the key safe. The link is public, so the ElevenLabs key can never touches the browser. A tiny Netlify Function proxies /api/tts and /api/voices, holding the key in an env var and forwarding requests server-side. The client only ever talks to my own domain.&lt;/p&gt;

&lt;p&gt;Never hard-failing. If the proxy returns a non-200 — a quota cap, a rate limit — the app falls back to the browser's built-in speech synthesis (preferring a Japanese voice) and keeps counting. I actually hit this live during testing when a capped key ran dry, and the drill kept running on the fallback voice without a hiccup. Generation is also concurrency-limited with retries so the free tier's request limits don't drop individual clips.&lt;/p&gt;

&lt;p&gt;The flame is a canvas particle system — rising flame particles, floating embers, and an additive glow, all scaling with a single fire value from 0 to 1. Rather than tie that purely to the streak, I compute it from both consistency and effort: fire = 0.7 × (streak / 30) + 0.4 × (today's minutes / 45), capped at 1. A long streak carries a strong baseline, a hard session flares it up the same day, and a single huge day can't max it out alone. Reduced-motion users get a still flame instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;Best Use of ElevenLabs. The sensei voice is the core of the app: a custom generated Voice Design grand-master voice, spoken with Turbo v2.5 and language_code: "ja" for authentic Japanese counting, pre-generated and cached per word so it lands on the beat, with separate expressiveness profiles for the counts and the kiai which are all served through a serverless proxy that keeps the API key off of the client, with a seamless fallback to the browser voice so a public link never hard-fails.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>elevenlabs</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
