<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Bhaumic Singh — Blog</title>
    <link>https://bhaumicsingh.dev</link>
    <description>Short, practical notes on building web and android apps, tools, systems, and experiments by Bhaumic Singh.</description>
    <language>en-us</language>
    <lastBuildDate>Mon, 23 Mar 2026 00:00:00 GMT</lastBuildDate>
    <managingEditor>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</managingEditor>
    <webMaster>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</webMaster>
    <atom:link href="https://bhaumicsingh.dev/rss" rel="self" type="application/rss+xml" />
    <image>
      <url>https://bhaumicsingh.dev/web-app-manifest-192x192.png</url>
      <title>Bhaumic Singh — Blog</title>
      <link>https://bhaumicsingh.dev</link>
    </image>
    <item>
      <title>ShashinMori: Google Killed Free Storage, So I Committed Crimes Against Backend Architecture</title>
      <link>https://bhaumicsingh.dev/blog/shashinmori</link>
      <guid isPermaLink="true">https://bhaumicsingh.dev/blog/shashinmori</guid>
      <description>A full-stack Flutter + Fastify odyssey to reclaim unlimited original quality storage from dead Pixel phones. Spoiler: OAuth tokens are involved.</description>
      <pubDate>Mon, 23 Mar 2026 00:00:00 GMT</pubDate>
      <author>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</author>
      <content:encoded><![CDATA[<h1 id="shashinmori-google-killed-free-storage-so-i-committed-crimes-against-backend-architecture"><a href="#shashinmori-google-killed-free-storage-so-i-committed-crimes-against-backend-architecture">ShashinMori: Google Killed Free Storage, So I Committed Crimes Against Backend Architecture</a></h1>
<blockquote>
<p>"Why pay $3/month for Google One when you can spend 3 weeks building a private photo gallery instead?"</p>
</blockquote>
<p>It started with Google Photos.</p>
<p>Specifically, it started with Google Photos deciding that my perfectly functional 2016 Pixel — a phone Google <strong>themselves</strong> promised unlimited original-quality storage to forever — no longer deserved unlimited original-quality storage. Forever, apparently, was a marketing term.</p>
<p>Fifteen gigabytes. Shared with Gmail. For a family photo archive.</p>
<p>Fine. I'll build my own.</p>
<h2 id="the-architecture-that-got-out-of-hand"><a href="#the-architecture-that-got-out-of-hand">The Architecture That Got Out of Hand</a></h2>
<p>Here is the pitch: old Pixel devices already have Google Photos running locally. Google Photos backs up whatever it finds in a watched folder. So the plan was to run the backend <strong>on the Pixel itself</strong>, dump uploads into a sync folder, let Google Photos do the backup, then purge the originals after 12 hours. Previews stay forever. Gallery reads from Firestore metadata and preview files.</p>
<p>Reader, I drew a flowchart. It had six boxes. By the time I shipped, it had 14.</p>
<p>The actual pipeline:</p>
<ol>
<li>Flutter app authenticates via Firebase OAuth</li>
<li>Upload goes to the Fastify API using the <strong>tus protocol</strong> — resumable uploads, because uploads die midway and life is pain</li>
<li>A BullMQ job fires off the upload worker</li>
<li>Worker moves the original to <code>/sdcard/ShashinMori/&#x3C;uid>/...</code> (Google Photos picks it up automatically)</li>
<li>Worker runs Jimp to generate an optimized preview</li>
<li>Worker writes metadata to Firestore</li>
<li>A 12-hour cleanup job purges the original from the sync folder after backup</li>
<li>Gallery endpoints serve from Firestore metadata + the retained preview</li>
</ol>
<p>This is what "simple photo backup app" looks like at 2 AM.</p>
<h2 id="the-tech-stack-annotated"><a href="#the-tech-stack-annotated">The Tech Stack, Annotated</a></h2>
<p><strong>Flutter + Riverpod + GoRouter</strong> on the frontend. Riverpod is the state management solution that makes you feel vaguely competent until BuildContext eats your soul. GoRouter is fine. Dart is fine. Writing Dart is like TypeScript with training wheels, which is either reassuring or insulting depending on your perspective.</p>
<p><strong>Fastify + TypeScript</strong> on the backend. Faster than Express, more opinionated than I expected, very happy with it once the plugin system clicked.</p>
<p><strong>tus protocol</strong> for uploads. The resumable upload standard that nobody talks about until you're dealing with spotty mobile connections and 50MB RAW files. <code>@tus/server</code> handles the chunked upload dance. The client-side <code>tus_client_dart</code> package does the same on Flutter. When it works, it's magic. Getting there was not magic.</p>
<p><strong>BullMQ + Upstash Redis</strong> for the job queue. Fire and forget. The worker processes asynchronously so the upload endpoint returns fast and the heavy Jimp processing doesn't block anything.</p>
<p><strong>Jimp</strong> for image processing. Pure JavaScript, no native binaries, runs on the Pixel without drama. Slower than Sharp. Doesn't require compiling native modules on ARM. Trade-offs.</p>
<h2 id="the-design-decisions-i-will-defend"><a href="#the-design-decisions-i-will-defend">The Design Decisions I Will Defend</a></h2>
<p>This is an <strong>append-only archive</strong>. No delete. No download. Intentionally.</p>
<p>The point is preservation, not management. Photos go in. They stay in. Google Photos has a copy. The preview is always there. Nothing accidentally disappears because someone's toddler tapped delete.</p>
<p>Multi-user isolation is enforced at the Firestore level — every record is scoped to a UID, so family members sharing the backend can't see each other's photos.</p>
<p>Is it overkill for a family gallery? Absolutely. Is every architectural decision technically justified? Yes, actually. That's the worst kind of overkill.</p>
<h2 id="what-i-actually-learned"><a href="#what-i-actually-learned">What I Actually Learned</a></h2>
<p>File uploads are a protocol, not a feature. If you're doing anything beyond a single-chunk POST, just use tus from the start.</p>
<p>BullMQ with Redis makes async job queues embarrassingly easy. The worker model — receive upload, enqueue job, return 200, process later — is the correct mental model for anything involving I/O you don't control.</p>
<p>Running a backend on a Pixel device is completely viable and deeply unhinged. Termux + PM2 + Docker-compatible deployment targets. I have regrets about the path I took to learn this.</p>
<h2 id="the-numbers"><a href="#the-numbers">The Numbers</a></h2>
<ul>
<li><strong>6</strong> npm packages that actually matter, surrounded by 30 that exist</li>
<li><strong>1</strong> old Pixel phone running a production API server in my living room</li>
<li><strong>0</strong> delete endpoints, on purpose</li>
<li><strong>12 hours</strong> before originals get purged from the sync folder</li>
<li><strong>Indefinite</strong> preview retention, because that's the whole point</li>
</ul>
<p>ShashinMori is open source. The architecture diagram alone is worth cloning it.</p>
<p><a href="https://github.com/Mic-360/shashinmori">Grab it on GitHub</a>. Your old Pixel deserves a second life.</p>]]></content:encoded>
    </item>
    <item>
      <title>I Built Vercel on a Phone. A Rooted Android Phone.</title>
      <link>https://bhaumicsingh.dev/blog/minishinobi-pocket-vercel</link>
      <guid isPermaLink="true">https://bhaumicsingh.dev/blog/minishinobi-pocket-vercel</guid>
      <description>Normal people pay $20/month for Vercel. I built the entire thing on a Snapdragon 660 with 4 GB of RAM running in a terminal emulator. This is normal behavior.</description>
      <pubDate>Wed, 04 Mar 2026 00:00:00 GMT</pubDate>
      <author>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</author>
      <content:encoded><![CDATA[<h1 id="i-turned-my-old-phone-into-a-vercel-clone-please-dont-ask-why"><a href="#i-turned-my-old-phone-into-a-vercel-clone-please-dont-ask-why">I Turned My Old Phone Into a Vercel Clone. Please Don't Ask Why.</a></h1>
<blockquote>
<p>Normal people: pays $20/month for hobby project hosting.
Me: roots phone, installs Termux, rewrites deployment infrastructure from scratch at midnight.</p>
</blockquote>
<p>Both are valid approaches to the problem. I'm not here to judge. I'm here to explain the sequence of decisions that led to me configuring Nginx on a Snapdragon 660 while the phone was also playing a lo-fi playlist.</p>
<h2 id="the-inciting-incident"><a href="#the-inciting-incident">The Inciting Incident</a></h2>
<p>I had a rooted Android phone sitting in a drawer. I wanted to host some side projects. Vercel exists. Railway exists. Render exists.</p>
<p>I chose the drawer phone.</p>
<h2 id="what-is-minishinobi"><a href="#what-is-minishinobi">What Is MiniShinobi</a></h2>
<p>It's a self-hosted, Vercel-like deployment platform that runs entirely inside <a href="https://termux.dev">Termux</a> on a rooted Android device. You connect a GitHub repo, hit <strong>Deploy</strong>, and your project goes live on a real subdomain over HTTPS via Cloudflare Tunnel — all from a device that charges next to your bed at night.</p>
<p>The stack: Node.js backend, Vite/React dashboard, Nginx reverse proxy, PM2 for process management, <code>sql.js</code> for the database, and <code>cloudflared</code> for tunnels. All of it supervised by PM2 on a phone.</p>
<h2 id="well-just-use-sqljs"><a href="#well-just-use-sqljs">"We'll Just Use sql.js"</a></h2>
<p>The first design constraint: you cannot compile <code>better-sqlite3</code> on ARM Termux. Native Node addon compilation on ARM is a special kind of unpleasant that I don't recommend.</p>
<p>So the database runs on <code>sql.js</code> — SQLite compiled to WebAssembly. Zero native compilation, zero segfaults. Every write flushes the entire database buffer to disk. The database is small. The phone has 4 GB of RAM. This is fine.</p>
<p>It's cursed in a way that is technically completely fine.</p>
<h2 id="well-just-use-child_process"><a href="#well-just-use-child_process">"We'll Just Use child_process"</a></h2>
<p>Similarly, <code>node-pty</code> needs native compilation. You know how that story ends.</p>
<p>So the entire deployment engine — <code>git clone</code>, <code>npm install</code>, <code>npm run build</code>, framework detection, process spawn — runs through Node's built-in <code>child_process.spawn</code>. Every byte of stdout and stderr is line-buffered and streamed live to the browser via Server-Sent Events. You watch your build happen in real-time from the dashboard, which is serving you that stream from a phone in your drawer.</p>
<p>Does it feel like it should work? No. Does it work? The phone is serving things to the open internet right now.</p>
<h2 id="well-just-use-pm2"><a href="#well-just-use-pm2">"We'll Just Use PM2"</a></h2>
<p><code>pm2 start ecosystem.config.js</code> and everything wakes up: the Express backend, Nginx, and <code>cloudflared</code>. Three supervised processes, one config file. If anything crashes, PM2 restarts it. On a phone.</p>
<p>This is the part where I discovered PM2 is genuinely great and I should have been using it everywhere.</p>
<h2 id="the-build-queue-the-unsexy-correct-decision"><a href="#the-build-queue-the-unsexy-correct-decision">The Build Queue (The Unsexy Correct Decision)</a></h2>
<p>4 GB of RAM sounds fine until <code>npm run build</code> on a React app invites the OOM killer to the party.</p>
<p>There's a serialized build queue. One build at a time. If you deploy two projects simultaneously, one waits. It's not exciting. It's the right call for a device with less RAM than most laptops have VRAM.</p>
<p>I also added 1 GB of swap. Don't look at me like that.</p>
<h2 id="cloudflare-tunnels-are-legitimately-magic"><a href="#cloudflare-tunnels-are-legitimately-magic">Cloudflare Tunnels Are Legitimately Magic</a></h2>
<p><code>cloudflared</code> is the part that makes all of this actually work. You run one daemon, write a YAML config, and <code>localhost:3000</code> is suddenly reachable at <code>your-project.yourdomain.com</code> over HTTPS with a valid certificate.</p>
<p>No port forwarding. No public IP. No router config. No DynDNS prayers. Every deployment gets its own named tunnel. Every project gets its own subdomain. The phone sitting in a drawer is serving real traffic on the real internet.</p>
<h2 id="does-it-actually-work"><a href="#does-it-actually-work">Does It Actually Work?</a></h2>
<p>Build logs stream live. Deployments go live. GitHub OAuth works. Static sites and Node apps both deploy. The framework detector auto-detects Next.js, Vite, and plain static sites. SSL certificates are valid.</p>
<p>Running on a Snapdragon 660, 4 GB RAM, PixelExperience Android 13. The phone is also charging. The lo-fi playlist is still going.</p>
<p>Did I need to build this? No. Do I have a full deployment dashboard running on a rooted Android phone for exactly $0/month? Yes.</p>
<p>That's just engineering.</p>
<hr>
<p><a href="https://github.com/Mic-360/MiniShinobi">MiniShinobi is open source.</a> If you have a rooted Android phone gathering dust and a questionable relationship with free time, the README has every command. I already did the part where you discover why you shouldn't do this. You get to skip straight to the part where it works.</p>]]></content:encoded>
    </item>
    <item>
      <title>Hakai: I Wrote a Rust + Bun Hybrid to Delete node_modules and I Have No Regrets</title>
      <link>https://bhaumicsingh.dev/blog/hakai-purge</link>
      <guid isPermaLink="true">https://bhaumicsingh.dev/blog/hakai-purge</guid>
      <description>npkill was too slow. So I built a parallel directory destroyer in Rust with a Bun TUI because apparently I hate free time.</description>
      <pubDate>Sun, 01 Mar 2026 00:00:00 GMT</pubDate>
      <author>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</author>
      <content:encoded><![CDATA[<h1 id="hakai-i-wrote-a-rust--bun-hybrid-to-delete-node_modules-and-i-have-no-regrets"><a href="#hakai-i-wrote-a-rust--bun-hybrid-to-delete-node_modules-and-i-have-no-regrets">Hakai: I Wrote a Rust + Bun Hybrid to Delete node_modules and I Have No Regrets</a></h1>
<blockquote>
<p>"Throughout the filesystem and the disk, I alone am the honored one."</p>
</blockquote>
<p>It started, as all terrible ideas do, with a minor inconvenience.</p>
<p>I ran <code>npkill</code> on my projects folder -- five years of side projects, abandoned tutorials, and repos I swore I'd finish. It found 50,000+ directories. Twelve seconds just to <em>scan</em>. Sequential size calculation. And when I hit delete on a 5GB <code>node_modules</code>? The UI froze while Node.js heroically attempted thousands of blocking I/O calls.</p>
<p>Unacceptable.</p>
<p>So naturally, I chose the hardest possible approach.</p>
<h2 id="the-architecture-nobody-asked-for"><a href="#the-architecture-nobody-asked-for">The Architecture Nobody Asked For</a></h2>
<p>I call it <strong>hakai (破壊)</strong> -- "destruction" in Japanese -- because naming things is the second hardest problem in computer science, and for once, this one was easy.</p>
<p>The core insight: JavaScript is not built for high-performance filesystem work. Rust is. So the engine -- scanning, size calculation, deletion, risk analysis -- is compiled Rust. <code>rayon</code> gives me a work-stealing thread pool that distributes directory traversal across every CPU core. It's embarrassingly parallel and I'm not embarrassed at all.</p>
<p>But here's where the yak-shaving gets beautiful.</p>
<p>Building TUIs in Rust is... an experience. So the frontend -- keybinds, diff-based rendering, regex search, multi-select -- runs in <strong>Bun</strong>. Sub-50ms startup, 60fps rendering, and I already knew TypeScript.</p>
<p>The two halves talk over a newline-delimited JSON IPC protocol on stdin/stdout pipes. Rust fires scan results, sizes, and deletion progress down stdout. Bun catches it on stdin, parses each line, renders without blocking. One JSON object per line, trivially debuggable with <code>jq</code>.</p>
<p>A chaotic marriage that infuriates purists on both sides. Perfect.</p>
<h2 id="the-benchmarks-of-spite"><a href="#the-benchmarks-of-spite">The Benchmarks of Spite</a></h2>
<p>Here's what happens when you massively over-engineer a file deletion tool:</p>



































<div class="table-wrapper"><table><thead><tr><th>Operation</th><th>npkill (Node)</th><th>hakai (Rust+Bun)</th><th>Gain</th></tr></thead><tbody><tr><td>Scan 50k dirs</td><td>~8-12s</td><td>&#x3C;1s</td><td><strong>10-15x</strong></td></tr><tr><td>Size calculation</td><td>Sequential</td><td>Parallel (rayon)</td><td><strong>8-12x</strong></td></tr><tr><td>Delete 5GB folder</td><td>~45s (blocks UI)</td><td>Async, non-blocking</td><td><strong>4-6x</strong></td></tr><tr><td>Cold startup</td><td>~400ms</td><td>&#x3C;50ms</td><td><strong>8x</strong></td></tr></tbody></table></div>
<p>When you multi-select 100 folders and hit enter, <code>hakai</code> fires up to 8 concurrent deletion tasks via <code>tokio</code>. The UI never blocks. The scan never pauses. Folders just vanish. It finishes before your hand reaches the mouse.</p>
<h2 id="the-weekend-that-kept-growing"><a href="#the-weekend-that-kept-growing">The Weekend That Kept Growing</a></h2>
<p>Let me be transparent about the scope creep.</p>
<p>What started as "make npkill but faster" turned into: cross-platform path handling for Windows' 260-character MAX_PATH limit (solved with <code>\\?\</code> UNC prefixes). Fixing TTY detection for Windows Git Bash, which is somehow still broken in 2026. Adding risk analysis that flags orphaned <code>node_modules</code> missing a <code>package.json</code> so you don't nuke a monorepo root. Building a headless mode with <code>--json</code> output for CI pipelines because scope creep doesn't stop until the PR is merged.</p>
<p>"It works on my machine" wasn't good enough. It had to work on <em>everyone's</em> machine.</p>
<h2 id="was-it-worth-it"><a href="#was-it-worth-it">Was It Worth It?</a></h2>
<p>I spent a full weekend configuring Rust cross-compilation targets, debugging IPC buffer edge cases, and fighting <code>tokio</code> task cancellation semantics -- all to save roughly 10 seconds of my day once a month.</p>
<p>Let's do the math. If I use <code>hakai</code> twice a month and save 12 seconds each time, I'll break even on development time in approximately... 47 years.</p>
<p>But that benchmark table looks <em>really</em> good in a README.</p>
<p>So yes. Obviously worth it.</p>
<p>Grab <a href="https://github.com/Mic-360/hakai">hakai on GitHub</a>. Your <code>node_modules</code> had it coming.</p>]]></content:encoded>
    </item>
    <item>
      <title>Winmole: I Gave My Terminal Admin Rights. What Could Go Wrong?</title>
      <link>https://bhaumicsingh.dev/blog/winmole-system-utility</link>
      <guid isPermaLink="true">https://bhaumicsingh.dev/blog/winmole-system-utility</guid>
      <description>Because apparently &apos;clean up system files&apos; in Windows actually means &apos;leave half a gigabyte of telemetry data just in case&apos;.</description>
      <pubDate>Sat, 28 Feb 2026 00:00:00 GMT</pubDate>
      <author>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</author>
      <content:encoded><![CDATA[<h1 id="winmole-i-gave-my-terminal-admin-rights-what-could-go-wrong"><a href="#winmole-i-gave-my-terminal-admin-rights-what-could-go-wrong">Winmole: I Gave My Terminal Admin Rights. What Could Go Wrong?</a></h1>
<blockquote>
<p>Windows built-in disk cleanup: freed 2MB. My hidden AppData caches: 40GB. The math here is upsetting.</p>
</blockquote>
<p>My C: drive bar turned red.</p>
<p>You know the one. That international symbol for "everything is about to break and it is entirely your fault." Standard protocol: download a third-party cleaner. So I went looking.</p>
<p>Option A: "free" cleaners bundled with three toolbars that want to be my default browser and cap you at 500MB of deletion before demanding a $40 annual subscription.</p>
<p>Option B: Actually pay $40 a year for something the OS should natively do, wrapped in an interface that looks like a 2004 Vegas slot machine.</p>
<p>I looked at macOS. CleanMyMac. DaisyDisk. Beautiful, fast, honest about what they're deleting.</p>
<p>I looked back at my red C: drive bar.</p>
<p>"Fine. I'll do it myself."</p>
<h2 id="the-wimo-architecture-powershell-does-the-work-go-makes-it-look-good"><a href="#the-wimo-architecture-powershell-does-the-work-go-makes-it-look-good">The WiMo Architecture: PowerShell Does the Work, Go Makes It Look Good</a></h2>
<p>Windows PowerShell is basically the native language of the Windows API. It can touch the registry, query WMI, talk to Winget, manage services. It's deeply powerful and its TUI capabilities are, charitably, equivalent to arranging rocks in mud.</p>
<p>Go, with the Bubble Tea and Lip Gloss frameworks, renders genuinely slick real-time terminal UIs.</p>
<p>So: PowerShell handles the heavy lifting. Compiled Go binaries handle the visuals. The two halves cooperate.</p>
<p><strong>The command surface:</strong></p>

































<div class="table-wrapper"><table><thead><tr><th>Command</th><th>What it does</th></tr></thead><tbody><tr><td><code>wimo clean</code></td><td>Deep system cleanup — temp files, browser caches, Windows Update leftovers, system logs</td></tr><tr><td><code>wimo uninstall</code></td><td>Interactive app uninstaller pulling from Registry, Winget, and local programs</td></tr><tr><td><code>wimo optimize</code></td><td>Flush DNS, clear icon and thumbnail caches, trim SSD, restart services</td></tr><tr><td><code>wimo analyze</code></td><td>Go TUI — real-time directory scanner with live size updates</td></tr><tr><td><code>wimo status</code></td><td>Go TUI — card dashboard: CPU, RAM, disk, network, processes, health score</td></tr><tr><td><code>wimo purge</code></td><td>Nuke build artifacts: <code>node_modules</code>, <code>.next</code>, <code>target</code>, <code>__pycache__</code>, Flutter cache</td></tr></tbody></table></div>
<p>28+ cleanup targets across user caches, browser data, package managers, and dev tool leftovers.</p>
<h2 id="the-4-layer-safe-delete-system"><a href="#the-4-layer-safe-delete-system">The 4-Layer Safe Delete System</a></h2>
<p>I gave this thing admin rights. That is not a decision to make lightly.</p>
<p>Every deletion goes through four checks before anything touches disk:</p>
<ol>
<li><strong>Protected path check</strong> — <code>C:\Windows</code>, <code>C:\Program Files</code>, and other system-critical directories are hardcoded off-limits</li>
<li><strong>User data pattern check</strong> — Documents, Desktop, Downloads, Pictures are always preserved</li>
<li><strong>Recycle bin fallback</strong> — files go to Recycle Bin when possible, not permanent deletion</li>
<li><strong>Dry-run preview</strong> — <code>wimo clean --dry-run</code> shows exactly what would be removed before committing</li>
</ol>
<p>You can run it with admin rights and not lose your Documents folder. That felt important to test thoroughly.</p>
<h2 id="the-purge-command-born-of-pure-developer-spite"><a href="#the-purge-command-born-of-pure-developer-spite">The Purge Command: Born of Pure Developer Spite</a></h2>
<p><code>wimo purge</code> was written specifically because I discovered my D: drive had 15GB of <code>node_modules</code> from projects I haven't touched since 2023. Plus Rust <code>target</code> directories. Plus Python <code>__pycache__</code> scattered across everything.</p>
<p>PowerShell runspace pools run up to 16 concurrent threads for size calculation and deletion. <code>System.IO.Directory.EnumerateFiles()</code> replaces <code>Get-ChildItem -Recurse</code> for scanning that is actually fast. It finds the artifacts, calculates their sizes in parallel, and removes them concurrently.</p>
<p>The <code>--paths</code> and <code>--depth</code> flags let you scope it. <code>wimo purge --paths "C:\Projects" --depth 5</code> stays in your lane.</p>
<h2 id="the-go-tui-because-staring-at-plain-output-is-suffering"><a href="#the-go-tui-because-staring-at-plain-output-is-suffering">The Go TUI: Because Staring at Plain Output Is Suffering</a></h2>
<p><code>wimo analyze</code> hands off to a compiled Go binary that renders a live-updating directory tree as it scans. Alt-screen mode. Goroutine-per-directory concurrency. The sizes appear in real time as the scan progresses.</p>
<p><code>wimo status</code> renders a 6-card dashboard — CPU, Memory, Disk, Network, Processes, System — with a health score badge, a Catppuccin-inspired sage green palette, and 2-second auto-refresh via <code>gopsutil</code>. Responsive layout: stacks to a single column below 70 terminal columns.</p>
<p>These are optional binaries. If you haven't built them, the PowerShell wrapper tells you the build command. It doesn't just fail silently.</p>
<h2 id="the-admin-awareness-detail-im-proud-of"><a href="#the-admin-awareness-detail-im-proud-of">The Admin-Awareness Detail I'm Proud Of</a></h2>
<p>WiMo badges admin-required tasks in the UI. If you're running as a standard user, it skips those tasks gracefully and tells you why. It doesn't crash, doesn't silently fail, doesn't demand elevation and then complain.</p>
<p>That's not a flashy feature. It's just correct behavior that most tools in this space don't bother with.</p>
<h2 id="did-i-need-to-build-this"><a href="#did-i-need-to-build-this">Did I Need To Build This?</a></h2>
<p>Could I have right-clicked and deleted the folders manually? Yes.</p>
<p>Could I have bought a larger SSD for cheaper than the time this took? Probably.</p>
<p>But now I have a tool that doesn't try to install a search toolbar, doesn't cap deletion behind a paywall, and actually tells me what it's about to do before it does it.</p>
<p>I consider that a win. Check out <a href="https://github.com/Mic-360/winmole">Winmole on GitHub</a>. Your C: drive bar doesn't have to be red.</p>]]></content:encoded>
    </item>
    <item>
      <title>TabiNeko: Teaching Chrome That a Wrist Flick Means &quot;Go Back&quot; Was Harder Than It Sounds</title>
      <link>https://bhaumicsingh.dev/blog/tabineko-mouse-gestures</link>
      <guid isPermaLink="true">https://bhaumicsingh.dev/blog/tabineko-mouse-gestures</guid>
      <description>A completely normal story about building a mouse-gesture Chrome extension because the back button was, apparently, too far away.</description>
      <pubDate>Fri, 27 Feb 2026 00:00:00 GMT</pubDate>
      <author>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</author>
      <content:encoded><![CDATA[<h1 id="tabineko-teaching-chrome-that-a-wrist-flick-means-go-back-was-harder-than-it-sounds"><a href="#tabineko-teaching-chrome-that-a-wrist-flick-means-go-back-was-harder-than-it-sounds">TabiNeko: Teaching Chrome That a Wrist Flick Means "Go Back" Was Harder Than It Sounds</a></h1>
<blockquote>
<p>My right hand is 4cm from the back button. I built an entire Chrome extension instead of moving it.</p>
</blockquote>
<p>It started with a reasonable thought.</p>
<p>"I wish I could just swipe to go back."</p>
<p>That's it. No technical problem. No user pain. Just a fleeting preference about mouse navigation that a normal person would think once and immediately forget. I, instead, spent a weekend building a gesture recognition engine, a canvas-based trail renderer, a Manifest V3 service worker, and a full design system for a 400px popup.</p>
<p>The back button is still 4cm away. I've just stopped using it.</p>
<h2 id="manifest-v3-googles-gift-to-extension-developers-who-thought-life-was-too-easy"><a href="#manifest-v3-googles-gift-to-extension-developers-who-thought-life-was-too-easy">Manifest V3: Google's Gift to Extension Developers Who Thought Life Was Too Easy</a></h2>
<p>You know what Manifest V3 took away? The ability to run persistent background pages. Fine, that's a service worker now. You know what else it took? Inline scripts in the popup HTML, because Content Security Policy says no.</p>
<p>That second one meant I needed a build step that extracts inline scripts into separate files at compile time. Not a workaround. That's just the job now. I wrote a custom <code>extract-inline.ts</code> script — 74 lines — that exists entirely because Chrome's CSP decided my popup was suspicious.</p>
<p>I'm fine. The script is fine. We're all fine.</p>
<h2 id="ill-just-write-a-quick-content-script"><a href="#ill-just-write-a-quick-content-script">"I'll Just Write a Quick Content Script"</a></h2>
<p>Famous last words.</p>
<p>The content script injects into every page on the internet. It listens for right-click drag events. It detects direction. It sends a message to the background service worker. Done.</p>
<p>What actually ended up in the content script:</p>
<p>A canvas element gets injected into every page. High-DPI support, because gesture trails on a retina display need to be crisp. Quadratic curve smoothing so the trail looks fluid instead of jagged. Real-time cardinal direction labels rendered mid-gesture with arrow symbols. A <code>gesture.ts</code> module with direction detection via accumulated angle deltas.</p>
<p>For going back. And forward. And switching tabs. And scrolling to the top.</p>
<h2 id="the-circle-gesture-problem"><a href="#the-circle-gesture-problem">The Circle Gesture Problem</a></h2>
<p>I need to talk about circle detection for a moment.</p>
<p>The naive approach: did the mouse path close on itself? Easy. Done. Ship it.</p>
<p>What I actually implemented: accumulated signed angle changes using <code>atan2</code> cross-products across consecutive path points, checked whether the total exceeds 300 degrees of arc, verified path closure within 80 pixels of the starting point.</p>
<p>This is for refreshing the page. The keyboard shortcut is F5. It has been F5 for thirty years. My circle detection algorithm is more sophisticated than my first three jobs combined.</p>
<p>I have no regrets.</p>
<h2 id="the-gesture-map"><a href="#the-gesture-map">The Gesture Map</a></h2>
<p>Seven gestures. Seven actions.</p>





































<div class="table-wrapper"><table><thead><tr><th>Gesture</th><th>Action</th></tr></thead><tbody><tr><td>Swipe Left</td><td>Go Back</td></tr><tr><td>Swipe Right</td><td>Go Forward</td></tr><tr><td>Swipe Up</td><td>Scroll to Top</td></tr><tr><td>Swipe Down</td><td>Scroll to Bottom</td></tr><tr><td>Right then Down</td><td>Next Tab</td></tr><tr><td>Left then Up</td><td>Previous Tab</td></tr><tr><td>Circle</td><td>Refresh Page</td></tr></tbody></table></div>
<p>All triggered by right-click drag. No accidental fires on normal right-click. The direction detection only commits once you've moved enough to make intent clear.</p>
<h2 id="the-build-pipeline-that-grew-a-pipeline"><a href="#the-build-pipeline-that-grew-a-pipeline">The Build Pipeline That Grew a Pipeline</a></h2>
<p>It's a Chrome extension. You write JS, load it unpacked, test it. Simple.</p>
<p>My build pipeline:</p>
<ol>
<li><strong>Astro</strong> compiles the popup</li>
<li><strong>Bun</strong> bundles the content and background scripts</li>
<li>Custom CSP extraction script extracts inline scripts</li>
<li><strong>Sharp</strong> generates four icon sizes from source</li>
<li>Pack script ZIPs everything for distribution</li>
</ol>
<p>Five tools. For seven features. I wrote more build infrastructure than I wrote browser logic. That's my brand.</p>
<h2 id="the-popup-design-system"><a href="#the-popup-design-system">The Popup Design System</a></h2>
<p>The popup is 400px wide. It shows a gesture reference table. It will never need server-side rendering. It has no dynamic data whatsoever.</p>
<p>I gave it a full design system. Custom CSS variables. A sage-green palette with named tokens like <code>sage-300</code> and <code>sage-600</code>. Named shadow tokens. A cubic-bezier easing curve. In Astro.</p>

<div class="shiki-code-block" data-code="--shadow-soft: 0 4px 16px rgba(46, 47, 44, 0.04);">
  <div class="shiki-header">
    <span class="shiki-lang">css</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#24292E">--shadow-soft: 0 4px 16px rgba(46, 47, 44, 0</span><span style="color:#6F42C1">.04</span><span style="color:#24292E">);</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#E1E4E8">--shadow-soft: 0 4px 16px rgba(46, 47, 44, 0</span><span style="color:#B392F0">.04</span><span style="color:#E1E4E8">);</span></span></code></pre></div>
</div>
<p>For a popup.</p>
<h2 id="does-it-actually-work"><a href="#does-it-actually-work">Does It Actually Work?</a></h2>
<p>Yes. Surprisingly well.</p>
<p>The gesture trail is smooth. The direction detection is accurate. Switching tabs with a two-direction swipe is genuinely faster than Ctrl+Tab once you've muscle-memorized it. The circle-to-refresh has completely replaced F5 in my daily workflow.</p>
<p>I created a problem, solved it more thoroughly than necessary, and now can't imagine browsing without it. Standard engineering outcome.</p>
<p><strong>TabiNeko</strong> is open source. You can use the back button instead. Both are valid. I won't judge you. I'm too busy drawing circles on web pages to have opinions about other people's navigation choices.</p>
<p><a href="https://github.com/Mic-360/tabineko">Grab it on GitHub</a>.</p>]]></content:encoded>
    </item>
    <item>
      <title>From Screenshot to Solution: Stop Forwarding Me Vague Tickets</title>
      <link>https://bhaumicsingh.dev/blog/screenshot-to-solution</link>
      <guid isPermaLink="true">https://bhaumicsingh.dev/blog/screenshot-to-solution</guid>
      <description>Learn how AI Feedbacks leverages Google Gemini to turn visually useless bug reports into actionable prompts for coding agents.</description>
      <pubDate>Thu, 26 Feb 2026 00:00:00 GMT</pubDate>
      <author>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</author>
      <content:encoded><![CDATA[<h1 id="the-ai-sees-your-stack-trace-and-judges-you"><a href="#the-ai-sees-your-stack-trace-and-judges-you">The AI Sees Your Stack Trace and Judges You</a></h1>
<blockquote>
<p>"It's broken." — an actual Jira ticket, assigned to you, with no further context, a screenshot of the wrong screen, and the energy of someone who has never once thought about what a developer needs to fix a bug.</p>
</blockquote>
<p>Bug reporting in 2026. Still a game of telephone. Still completely broken.</p>
<p>We're in a timeline where AI writes production code, but the standard bug report is still a screenshot cropped to a vague red box, pasted into Slack, followed by four words and the silent prayer that you're also a psychic. By the time that reaches an AI coding agent, the context has fully decayed. The agent writes the wrong fix. You spend 20 minutes explaining what was actually broken. Classic.</p>
<p><strong>AI Feedbacks</strong> exists specifically because I got tired of receiving "it doesn't work" as actionable development context.</p>
<h2 id="the-problem-has-a-name-context-decay"><a href="#the-problem-has-a-name-context-decay">The Problem Has a Name: Context Decay</a></h2>
<p>When a bug fires on the client side, useful information exists for exactly a few seconds: the DOM state at the moment of failure, the console errors, the failed network requests, the exact UI configuration. Then the user closes the tab, writes "the button did something weird," and submits.</p>
<p>By the time that reaches Cursor or Windsurf, the prompt is so thin it's basically a riddle. Agents hallucinate. Developers reconstruct the crash scene manually before they can even ask the agent to start fixing it.</p>
<p>The technical term is "context decay." The honest term is "why does this happen every single sprint."</p>
<h2 id="the-fix-capture-everything-at-the-moment-it-exists"><a href="#the-fix-capture-everything-at-the-moment-it-exists">The Fix: Capture Everything at the Moment It Exists</a></h2>
<p>The Chrome Extension runs silently in the background. When the user crops and submits a bug report, it automatically:</p>
<ul>
<li>Grabs the exact area of the screen they flagged</li>
<li>Captures every <code>console.error</code> and <code>console.warn</code> from the session (yes, including the ones you've been ignoring)</li>
<li>Packages all failed network requests (4xx/5xx) from the last 60 seconds</li>
<li>Captures the DOM state at the time of failure</li>
<li>Sends the whole bundle to the backend — without the user doing anything else</li>
</ul>
<p>The developer doesn't receive a blurry screenshot. They receive a structured, agent-ready prompt.</p>
<h2 id="google-gemini-does-the-heavy-lifting"><a href="#google-gemini-does-the-heavy-lifting">Google Gemini Does the Heavy Lifting</a></h2>
<p>The backend is built with <strong>Next.js 15</strong> and powered by <strong>Google Gemini 3 Flash Preview</strong> for multimodal analysis. Gemini sees the screenshot, reads the error logs, cross-references the network failures, and synthesizes everything into a prompt that actually gives your coding agent something to work with on the first pass.</p>

<div class="shiki-code-block" data-code="const result = streamText({
  model: google(&#x27;gemini-3-flash-preview&#x27;),
  messages: [
    {
      role: &#x27;user&#x27;,
      content: [
        { type: &#x27;text&#x27;, text: &#x60;Analyze this bug: ${description}&#x60; },
        { type: &#x27;image&#x27;, image: screenshotBuffer },
      ],
    },
  ],
})">
  <div class="shiki-header">
    <span class="shiki-lang">ts</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#D73A49">const</span><span style="color:#005CC5"> result</span><span style="color:#D73A49"> =</span><span style="color:#6F42C1"> streamText</span><span style="color:#24292E">({</span></span>
<span class="line"><span style="color:#24292E">  model: </span><span style="color:#6F42C1">google</span><span style="color:#24292E">(</span><span style="color:#032F62">'gemini-3-flash-preview'</span><span style="color:#24292E">),</span></span>
<span class="line"><span style="color:#24292E">  messages: [</span></span>
<span class="line"><span style="color:#24292E">    {</span></span>
<span class="line"><span style="color:#24292E">      role: </span><span style="color:#032F62">'user'</span><span style="color:#24292E">,</span></span>
<span class="line"><span style="color:#24292E">      content: [</span></span>
<span class="line"><span style="color:#24292E">        { type: </span><span style="color:#032F62">'text'</span><span style="color:#24292E">, text: </span><span style="color:#032F62">`Analyze this bug: ${</span><span style="color:#24292E">description</span><span style="color:#032F62">}`</span><span style="color:#24292E"> },</span></span>
<span class="line"><span style="color:#24292E">        { type: </span><span style="color:#032F62">'image'</span><span style="color:#24292E">, image: screenshotBuffer },</span></span>
<span class="line"><span style="color:#24292E">      ],</span></span>
<span class="line"><span style="color:#24292E">    },</span></span>
<span class="line"><span style="color:#24292E">  ],</span></span>
<span class="line"><span style="color:#24292E">})</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> result</span><span style="color:#F97583"> =</span><span style="color:#B392F0"> streamText</span><span style="color:#E1E4E8">({</span></span>
<span class="line"><span style="color:#E1E4E8">  model: </span><span style="color:#B392F0">google</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'gemini-3-flash-preview'</span><span style="color:#E1E4E8">),</span></span>
<span class="line"><span style="color:#E1E4E8">  messages: [</span></span>
<span class="line"><span style="color:#E1E4E8">    {</span></span>
<span class="line"><span style="color:#E1E4E8">      role: </span><span style="color:#9ECBFF">'user'</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8">      content: [</span></span>
<span class="line"><span style="color:#E1E4E8">        { type: </span><span style="color:#9ECBFF">'text'</span><span style="color:#E1E4E8">, text: </span><span style="color:#9ECBFF">`Analyze this bug: ${</span><span style="color:#E1E4E8">description</span><span style="color:#9ECBFF">}`</span><span style="color:#E1E4E8"> },</span></span>
<span class="line"><span style="color:#E1E4E8">        { type: </span><span style="color:#9ECBFF">'image'</span><span style="color:#E1E4E8">, image: screenshotBuffer },</span></span>
<span class="line"><span style="color:#E1E4E8">      ],</span></span>
<span class="line"><span style="color:#E1E4E8">    },</span></span>
<span class="line"><span style="color:#E1E4E8">  ],</span></span>
<span class="line"><span style="color:#E1E4E8">})</span></span></code></pre></div>
</div>
<p>Feed it a screenshot and a vague description. Get back a prompt that Cursor can actually use. Wild concept.</p>
<h2 id="the-dashboard-because-youll-forget-about-last-tuesdays-bug"><a href="#the-dashboard-because-youll-forget-about-last-tuesdays-bug">The Dashboard: Because You'll Forget About Last Tuesday's Bug</a></h2>
<p>The web dashboard has natural language semantic search because nobody remembers the exact tag they used two weeks ago.</p>
<p>Search for:</p>
<ul>
<li><em>"that auth error"</em></li>
<li><em>"the white flash on mobile"</em></li>
<li><em>"whatever was broken with the chart last week"</em></li>
</ul>
<p>It just finds it. Not because you tagged correctly in a 14-step triage process. Because you described it like a human.</p>
<p>The UI is monochrome, high-contrast, deliberately no-frills. No decorative gradients competing for attention while you're just trying to ship a fix. The <code>#87ae73</code> accent color is the most exciting thing in the design system, and that's intentional.</p>
<h2 id="who-this-is-for"><a href="#who-this-is-for">Who This Is For</a></h2>
<p>If you use Cursor, Windsurf, GitHub Copilot, or any agentic coding tool — you already know the dirty secret: output quality is 100% constrained by prompt quality. AI Feedbacks automates the hardest part of a good bug prompt: capturing and structuring all the context at the exact moment it exists, before the user closes the tab and forgets everything.</p>
<p>Better context in. First-pass fixes out. Feed your agents better.</p>
<p><a href="https://github.com/Mic-360/ai-feedbacks">github.com/Mic-360/ai-feedbacks</a></p>]]></content:encoded>
    </item>
    <item>
      <title>Guarding Your Code: The Rise of Mamoru-WAF</title>
      <link>https://bhaumicsingh.dev/blog/guarding-your-code-mamoru-waf</link>
      <guid isPermaLink="true">https://bhaumicsingh.dev/blog/guarding-your-code-mamoru-waf</guid>
      <description>Discover how Mamoru-kun, a lightweight Go-based guardian, brings enterprise-grade security and a stunning TUI to your hobby projects.</description>
      <pubDate>Wed, 25 Feb 2026 00:00:00 GMT</pubDate>
      <author>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</author>
      <content:encoded><![CDATA[<h1 id="nobody-will-hack-my-side-project-right"><a href="#nobody-will-hack-my-side-project-right">Nobody Will Hack My Side Project... Right?</a></h1>
<blockquote>
<p>Narrator: The bots were already scanning it.</p>
</blockquote>
<p>Hot take: the same <code>sqlmap</code> script hammering Fortune 500 APIs is also currently probing your portfolio project with 50 daily active users. The bots do not care that you're "just a hobby project." They are not reading your README.</p>
<p>The WAF market's solution for this is extremely helpful: $200/month for Cloudflare WAF, or AWS WAF where you pay per rule, per request, and probably per time you think about security. ModSecurity is technically free if you enjoy reading 300 pages of Apache documentation at midnight.</p>
<p>Nobody built a WAF for the person who just wants their side project to stop getting SQL-injected.</p>
<p>So I did.</p>
<h2 id="introducing-mamoru-waf"><a href="#introducing-mamoru-waf">Introducing Mamoru-WAF</a></h2>
<p>Single Go binary. Zero external dependencies. No Redis cluster. No Postgres spanning three Availability Zones just to store block logs. Just:</p>

<div class="shiki-code-block" data-code="go build -o mamoru ./cmd/server
./mamoru --tui">
  <div class="shiki-header">
    <span class="shiki-lang">bash</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6F42C1">go</span><span style="color:#032F62"> build</span><span style="color:#005CC5"> -o</span><span style="color:#032F62"> mamoru</span><span style="color:#032F62"> ./cmd/server</span></span>
<span class="line"><span style="color:#6F42C1">./mamoru</span><span style="color:#005CC5"> --tui</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">go</span><span style="color:#9ECBFF"> build</span><span style="color:#79B8FF"> -o</span><span style="color:#9ECBFF"> mamoru</span><span style="color:#9ECBFF"> ./cmd/server</span></span>
<span class="line"><span style="color:#B392F0">./mamoru</span><span style="color:#79B8FF"> --tui</span></span></code></pre></div>
</div>
<p>You're protected. That's the whole pitch.</p>
<h2 id="the-part-where-it-does-real-security"><a href="#the-part-where-it-does-real-security">The Part Where It Does Real Security</a></h2>
<p>Mamoru covers the OWASP Top 10 staples that are most likely to ruin your week:</p>
<p><strong>Regex pattern matching</strong> catches SQLi (<code>union</code>, <code>select</code>, <code>drop</code>, <code>sleep</code>), XSS (<code>&#x3C;script></code>, <code>onerror=</code>), and path traversal (<code>../</code>) before the request reaches your backend. Your backend doesn't even know it happened.</p>
<p><strong>Token-bucket rate limiting</strong> runs per-IP, per-path, configurable. That <code>/api/login</code> endpoint you never hardened? It now has a bouncer.</p>
<p><strong>Request body inspection</strong> means your contact form also can't be an attack vector anymore. Yes, POST bodies are checked too.</p>
<p>The rule engine uses a dead-simple Go interface: <code>Name()</code> and <code>Evaluate()</code>. Two methods. You can write a custom rule in 5 lines of Go. Try doing that in AWS WAF without a certification.</p>
<h2 id="hot-reload-because-downtime-is-for-enterprises"><a href="#hot-reload-because-downtime-is-for-enterprises">Hot Reload: Because Downtime Is For Enterprises</a></h2>
<p>The <code>AtomicEngine</code> handles live rule reloading. Edit <code>rules.yaml</code>, save it, and the new rules are live in <strong>2 seconds</strong> — no restarts, no dropped connections, no maintenance windows.</p>
<p>You can toggle between BLOCK mode (hard blocks) and DETECT mode (logs only, nothing blocked) with a keypress. Useful for when you want to watch what's happening before you commit to blocking it.</p>
<h2 id="the-tui-nobody-asked-for"><a href="#the-tui-nobody-asked-for">The TUI Nobody Asked For</a></h2>
<p>I built a Catppuccin sage-green terminal dashboard because WAF logs are otherwise a wall of unreadable JSON and I couldn't live with that.</p>

<div class="shiki-code-block" data-code="🛡 MAMORU-WAF                     Mode: BLOCK  │  Backend: ● UP
 Total: 1250    Blocked: 23    Rate: 2    Detected: 0
─────────────────────────────────────────────────────────────────
 Live Events
  12:30:01 [OK]    192.168.1.5  GET    /
  12:30:03 [BLOCK] 10.0.0.50    POST   /login  pattern_filter:pattern_match
  12:30:05 [RATE]  10.0.0.50    GET    /api    rate_limiter:rate_limit
─────────────────────────────────────────────────────────────────
 q quit  │  r reload  │  m mode  │  c clear  │  h help">
  <div class="shiki-header">
    <span class="shiki-lang">text</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span>🛡 MAMORU-WAF                     Mode: BLOCK  │  Backend: ● UP</span></span>
<span class="line"><span> Total: 1250    Blocked: 23    Rate: 2    Detected: 0</span></span>
<span class="line"><span>─────────────────────────────────────────────────────────────────</span></span>
<span class="line"><span> Live Events</span></span>
<span class="line"><span>  12:30:01 [OK]    192.168.1.5  GET    /</span></span>
<span class="line"><span>  12:30:03 [BLOCK] 10.0.0.50    POST   /login  pattern_filter:pattern_match</span></span>
<span class="line"><span>  12:30:05 [RATE]  10.0.0.50    GET    /api    rate_limiter:rate_limit</span></span>
<span class="line"><span>─────────────────────────────────────────────────────────────────</span></span>
<span class="line"><span> q quit  │  r reload  │  m mode  │  c clear  │  h help</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>🛡 MAMORU-WAF                     Mode: BLOCK  │  Backend: ● UP</span></span>
<span class="line"><span> Total: 1250    Blocked: 23    Rate: 2    Detected: 0</span></span>
<span class="line"><span>─────────────────────────────────────────────────────────────────</span></span>
<span class="line"><span> Live Events</span></span>
<span class="line"><span>  12:30:01 [OK]    192.168.1.5  GET    /</span></span>
<span class="line"><span>  12:30:03 [BLOCK] 10.0.0.50    POST   /login  pattern_filter:pattern_match</span></span>
<span class="line"><span>  12:30:05 [RATE]  10.0.0.50    GET    /api    rate_limiter:rate_limit</span></span>
<span class="line"><span>─────────────────────────────────────────────────────────────────</span></span>
<span class="line"><span> q quit  │  r reload  │  m mode  │  c clear  │  h help</span></span></code></pre></div>
</div>
<p>Built with Bubble Tea. Real-time event log. Backend health panel. Interactive hotkeys. <code>r</code> reloads rules live. <code>d</code> toggles detect mode. <code>q</code> when you've seen enough and need to go to bed.</p>
<p>Nobody asked for a WAF with a terminal dashboard this nice. I built it anyway. It slaps.</p>
<h2 id="the-honest-assessment"><a href="#the-honest-assessment">The Honest Assessment</a></h2>
<p>Your side project is getting bot-scanned. "I don't have anything worth stealing" is not a threat model. Neither is "I'll add security later."</p>
<p>Mamoru is a 15MB Docker image, a YAML file you can read while half-asleep, and a Go binary that takes 60 seconds to get running.</p>

<div class="shiki-code-block" data-code="go build -o mamoru ./cmd/server">
  <div class="shiki-header">
    <span class="shiki-lang">bash</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6F42C1">go</span><span style="color:#032F62"> build</span><span style="color:#005CC5"> -o</span><span style="color:#032F62"> mamoru</span><span style="color:#032F62"> ./cmd/server</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">go</span><span style="color:#9ECBFF"> build</span><span style="color:#79B8FF"> -o</span><span style="color:#9ECBFF"> mamoru</span><span style="color:#9ECBFF"> ./cmd/server</span></span></code></pre></div>
</div>
<p>That's it. Mamoru-kun is now watching your traffic so you don't have to.</p>
<p><a href="https://github.com/Mic-360/mamoru-waf">GitHub</a> — the CONTRIBUTING.md has a guide. The rule interface is two methods. There is no valid excuse.</p>]]></content:encoded>
    </item>
    <item>
      <title>Pulse Loop: I Built a Whole App to Loop the Same 8 Bars for 3 Hours Straight</title>
      <link>https://bhaumicsingh.dev/blog/spotify-looper-mastery</link>
      <guid isPermaLink="true">https://bhaumicsingh.dev/blog/spotify-looper-mastery</guid>
      <description>Spotify has existed for twenty years and still cannot loop a specific segment of a song. So I built Pulse Loop. Millisecond-accurate A-B looping. For musicians. For obsessives. For me.</description>
      <pubDate>Mon, 23 Feb 2026 00:00:00 GMT</pubDate>
      <author>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</author>
      <content:encoded><![CDATA[<h1 id="pulse-loop-i-built-a-whole-app-to-loop-the-same-8-bars-for-3-hours-straight"><a href="#pulse-loop-i-built-a-whole-app-to-loop-the-same-8-bars-for-3-hours-straight">Pulse Loop: I Built a Whole App to Loop the Same 8 Bars for 3 Hours Straight</a></h1>
<blockquote>
<p>Spotify: twenty years of existence, billions in revenue, zero A-B loop implementations. I handled it.</p>
</blockquote>
<p>Let me tell you what Spotify gives you.</p>
<p>Play. Pause. Skip. Shuffle (broken in a new way every year). Repeat song — meaning the entire track, including the 90-second spoken intro you hate. A seek bar that is functionally useless at precise scrubbing speeds. That's it. That is the complete feature set for a company worth $50 billion.</p>
<p>No loop region. No segment markers. No "skip this intro automatically every single time forever" feature.</p>
<p>It's <code>while (position >= end) { seek(start); }</code>. A junior dev could ship this on a Tuesday. They haven't shipped it in twenty years.</p>
<p>So: <strong>Pulse Loop</strong>.</p>
<h2 id="what-it-actually-does"><a href="#what-it-actually-does">What It Actually Does</a></h2>
<p>Two modes. Online and offline.</p>
<p><strong>Online mode</strong> connects to your Spotify account via OAuth 2.0. You browse your top tracks, recently played, or search. You set a millisecond-accurate start point and end point. You toggle loop on. The app monitors playback position and seeks back to start the moment it hits your end marker. Sub-120ms seek latency via <code>spotify_sdk</code>. The loop is seamless.</p>
<p>Three playback modes:</p>
<ul>
<li><strong>Normal</strong> — loops the full track</li>
<li><strong>Loop</strong> — repeats your selected start-to-end segment continuously</li>
<li><strong>Skip</strong> — skips your selected segment and keeps playing the rest</li>
</ul>
<p>That last one is the feature I didn't know I needed. Set it on every song with a terrible bridge. Gone forever.</p>
<p><strong>Offline mode</strong> doesn't need Spotify at all. Upload local audio files — MP3, WAV, M4A, OGG, FLAC, AAC — and get the same loop controls powered by <code>just_audio</code>. Files persist to app cache on Android and IndexedDB on web, so they survive restarts. Automatic metadata extraction from <code>Artist - Title</code> filename format. Full offline, full loop, no account required.</p>
<h2 id="the-tech-because-it-matters"><a href="#the-tech-because-it-matters">The Tech, Because It Matters</a></h2>
<p><strong>Flutter + Riverpod</strong> with code generation. Riverpod's <code>@riverpod</code> annotations generate the boilerplate you'd otherwise write by hand. It's the state management solution that actually scales beyond three providers without turning into a BuildContext archaeology dig.</p>
<p><strong><code>spotify_sdk</code></strong> for Spotify playback control and <code>flutter_web_auth_2</code> for the OAuth 2.0 flow. The auth flow is the part that will make you briefly consider quitting software development. Then it works and you forget.</p>
<p><strong><code>just_audio</code></strong> for offline playback. Rock solid, cross-platform, handles every audio format I threw at it. The seek precision on local files is significantly better than Spotify's remote playback latency, which is either satisfying or depressing depending on how you look at it.</p>
<p><strong>Material 3 Expressive</strong> theming with dynamic colors. The UI pulls palette colors from album art. Your loop controller matches your album. It's completely unnecessary and I spent more time on it than I should have.</p>
<h2 id="the-part-where-i-justify-this-whole-thing"><a href="#the-part-where-i-justify-this-whole-thing">The Part Where I Justify This Whole Thing</a></h2>
<p>I play guitar. Learning from recordings means looping the same passage repeatedly. Standard workflow: play the section, miss the end, seek back manually, miss the start, give up, play the whole song again.</p>
<p>With Pulse Loop: set start marker, set end marker, play. The 8-bar passage repeats until your fingers get it right. No seeking. No distraction. The loop is just there, waiting, patient, accurate to the millisecond.</p>
<p>This is the entire reason the project exists. Everything else — the Skip mode, the offline support, the dynamic theming — is scope creep that turned out to be genuinely useful.</p>
<h2 id="is-this-worth-it-over-just-scrubbing-manually"><a href="#is-this-worth-it-over-just-scrubbing-manually">Is This Worth It Over Just Scrubbing Manually?</a></h2>
<p>The seek bar on a phone screen, at the precision needed for a 4-second riff inside a 4-minute track, is basically a game of chance. You will miss. Repeatedly.</p>
<p>Pulse Loop sets the markers once. They stay. Every time that track plays, the loop is already there.</p>
<p>Spotify won't build this. They've had twenty years and chose not to. That feature gap is the entire reason this exists, and honestly, I'm fine with that.</p>
<p>Open source. Free. Works right now.</p>
<ul>
<li><a href="https://github.com/Mic-360/spotify_looper">Source on GitHub</a></li>
<li><a href="https://spotify-looper-cc1ad.web.app/">Try it live</a></li>
</ul>
<p>Go loop the good part. The 8 bars that actually hit. Because Spotify certainly isn't going to help you find them.</p>]]></content:encoded>
    </item>
    <item>
      <title>Google Redesigned Material Again. My Widgets Are Having an Identity Crisis.</title>
      <link>https://bhaumicsingh.dev/blog/material3-expressive-flutter</link>
      <guid isPermaLink="true">https://bhaumicsingh.dev/blog/material3-expressive-flutter</guid>
      <description>Material 3 Expressive dropped in 2025 with spring physics, morphing shapes, and wavy loaders. My hardcoded BorderRadius.circular(8) buttons filed for emotional support.</description>
      <pubDate>Wed, 04 Feb 2026 00:00:00 GMT</pubDate>
      <author>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</author>
      <content:encoded><![CDATA[<h1 id="google-redesigned-material-again-my-widgets-are-having-an-identity-crisis"><a href="#google-redesigned-material-again-my-widgets-are-having-an-identity-crisis">Google Redesigned Material Again. My Widgets Are Having an Identity Crisis.</a></h1>
<blockquote>
<p>"If your app doesn't morph, bounce, or use 15 different shades of mauve, are you even designing in 2026?"</p>
</blockquote>
<p>Let's be honest: most Flutter apps look exactly like someone ran <code>flutter create</code>, added a few <code>Center</code> widgets, picked a primary color, and shipped it. Standard pill buttons. Standard app bar. Standard "I copied this directly from a 2021 tutorial and never looked back" energy.</p>
<p>It's everywhere. Your users notice. They just haven't uninstalled you yet.</p>
<p>Google's answer arrived in May 2025: <strong>Material 3 Expressive (M3E)</strong>. The most research-backed update to Material Design ever shipped. Specifically engineered for the crime of playing it safe. If your buttons don't have spring physics and your containers are still hardcoded to <code>BorderRadius.circular(8)</code>, M3E is here to make that feel embarrassing.</p>
<p><img src="/blog/material-3e-banner.jpg" alt="M3E Banner"></p>
<h2 id="the-im-not-reading-80-pages-of-design-spec-summary"><a href="#the-im-not-reading-80-pages-of-design-spec-summary">The "I'm Not Reading 80 Pages of Design Spec" Summary</a></h2>
<p>M3E — announced May 2025, still dodging mainstream adoption in 2026 because change is hard and <code>CircularProgressIndicator</code> technically works — is not a theme change. It's a fundamental shift in how interfaces <em>feel</em>. Four-point cheat sheet:</p>
<ol>
<li><strong>Color with purpose</strong>: Not "make the button blue." Using bold container colors to actively guide the user's eye instead of hoping they find the CTA themselves.</li>
<li><strong>Shape flexibility</strong>: Buttons that morph. Containers that aren't just rectangles. Expressive active indicators. <code>BorderRadius.circular(8)</code> is retired. Let it go.</li>
<li><strong>Springy physics</strong>: Spatial Springs for movement, Effects Springs for color transitions. Real objects bounce when you tap them. Linear <code>easeInOut</code> curves make your UI feel like a PowerPoint. Choose accordingly.</li>
<li><strong>New components</strong>: Button groups, split buttons, wavy loading indicators. Yes, the wavy loader. It makes waiting for a network response feel almost deliberate.</li>
</ol>
<h2 id="my-buttons-before-and-after"><a href="#my-buttons-before-and-after">My Buttons, Before and After</a></h2>
<p><strong>Before M3E:</strong> <code>ElevatedButton</code> with default styling. Technically a button. Communicates nothing about the emotional investment required to tap it.</p>
<p><strong>After M3E:</strong> The same action, but the button has a physical presence. It responds. It has opinions about being pressed. Users notice this without being able to articulate why.</p>
<p>The research backs this up — M3E components tested higher for perceived quality, trustworthiness, and "this app was made by someone who cared." You can argue about whether spring physics are necessary. You cannot argue with conversion metrics.</p>
<h2 id="the-components-youre-still-not-using"><a href="#the-components-youre-still-not-using">The Components You're Still Not Using</a></h2>
<h3 id="the-wavy-loader-upgrade-from-the-circle-of-shame"><a href="#the-wavy-loader-upgrade-from-the-circle-of-shame">The Wavy Loader (Upgrade From the Circle of Shame)</a></h3>
<p>The standard <code>CircularProgressIndicator</code> says: "something is happening, probably, we'll see."</p>
<p>M3E's wavy loader says: "we are actively working on this and the app has not frozen."</p>
<p>These are meaningfully different messages. The wavy one looks alive. Ship the wavy one.</p>
<h3 id="button-groups-and-split-buttons"><a href="#button-groups-and-split-buttons">Button Groups and Split Buttons</a></h3>
<p>Finally, grouped actions that don't look like you threw four individual buttons into a <code>Row</code> and called it done. Split buttons give you a primary action with an overflow option that stays in visual context. No more "wait, where's the secondary action" UX archaeology.</p>
<h3 id="spatial-springs-and-effects-springs"><a href="#spatial-springs-and-effects-springs">Spatial Springs and Effects Springs</a></h3>
<p><code>Curves.easeInOut</code> is retired. We are doing physics now.</p>
<p><strong>Spatial Springs</strong> handle movement — the kind of bounce when an element enters or responds to a tap that makes the interaction feel physical rather than programmatic. <strong>Effects Springs</strong> handle color transitions — fades that feel organic rather than linear.</p>
<p>The math is non-trivial. The result is that your app feels premium without anyone being able to explain why. That's the goal.</p>
<h2 id="the-workflow-skip-the-spec-use-an-agent"><a href="#the-workflow-skip-the-spec-use-an-agent">The Workflow: Skip the Spec, Use an Agent</a></h2>
<p>You could spend three days in the Material spec. Or you could point the <code>material3-expressive-flutter</code> skill at your AI coding agent and start shipping while it handles the implementation.</p>
<p>I've been using this skill to bridge the gap while official Flutter SDK support finishes catching up to the spec. It gives your agent the exact context needed to implement components correctly — not hallucinated approximations, not components that look close but break on the fourth interaction.</p>
<video src="/blog/build-montage.mp4" controls width="100%" height="auto">
  Your browser does not support the video tag.
</video>
_Actual footage of me shipping features faster than the Flutter team updates their changelog._
<h2 id="how-to-get-started-four-real-steps"><a href="#how-to-get-started-four-real-steps">How to Get Started (Four Real Steps)</a></h2>
<ol>
<li><strong>Enable Material 3</strong>: <code>useMaterial3: true</code> in your <code>ThemeData</code>. Non-negotiable table stakes.</li>
<li><strong>Use <code>ColorScheme.fromSeed</code></strong>: Stop manually typing hex codes. Dynamic palettes exist. Let them work.</li>
<li><strong>Point your agent at the skill</strong>: The <code>material3-expressive-flutter</code> reference files cover the components that aren't in the SDK yet. Force it to read the spec so you don't have to.</li>
<li><strong>Embrace shape morphing</strong>: Use the shape packages. <code>BorderRadius.circular(8)</code> has had a good run. It's done now.</li>
</ol>
<h2 id="the-result"><a href="#the-result">The Result</a></h2>
<p>When you combine M3E principles with proper motion physics, you get an app that feels like it was made on purpose. No more "this looks like a template" feedback. No more "did you build this yourself?" in a tone that implies doubt.</p>
<video src="/blog/montage.mp4" controls width="100%" height="auto">
  Your browser does not support the video tag.
</video>
_That's the dopamine hit your users have been quietly hoping for._
<h2 id="conclusion"><a href="#conclusion">Conclusion</a></h2>
<p>M3E is the difference between a utility tool and an actual experience. Your users don't know what spring physics are. They just know your app feels better than the other one.</p>
<p>Install the skill. Let the agent handle the physics math. Stop shipping gray-on-white list views.</p>
<p>Or don't. It is genuinely your call. The <code>CircularProgressIndicator</code> will still be there.</p>
<hr>
<p><strong>Next Steps:</strong></p>
<ul>
<li>Check out the <a href="https://github.com/Mic-360/m3e-flutter-agent-skill">M3E Agent Skills</a> repo for more details.</li>
</ul>
<hr>
<p><em>Written while my AI agent refactored 50 buttons to use springy animations because I refused to do it manually.</em></p>]]></content:encoded>
    </item>
    <item>
      <title>I Left React for Python. The Intervention Was Unnecessary.</title>
      <link>https://bhaumicsingh.dev/blog/from-react-to-reflex</link>
      <guid isPermaLink="true">https://bhaumicsingh.dev/blog/from-react-to-reflex</guid>
      <description>I had a perfectly working React dashboard. I rewrote it in Reflex because &apos;full-stack Python&apos; sounded clean on paper. It was not clean on paper. It was not clean anywhere.</description>
      <pubDate>Sat, 24 Jan 2026 00:00:00 GMT</pubDate>
      <author>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</author>
      <content:encoded><![CDATA[<h1 id="i-left-react-for-python-the-intervention-was-unnecessary"><a href="#i-left-react-for-python-the-intervention-was-unnecessary">I Left React for Python. The Intervention Was Unnecessary.</a></h1>
<blockquote>
<p>Or: How I Spent a Week Learning That Python Was Already Good At Everything Except This</p>
</blockquote>
<p><img src="/blog/why-reflex.png" alt="Why Reflex"></p>
<p>So there I was with a perfectly functional React + Vite dashboard. Clean TypeScript. 260 lines. Fast builds. It worked. Users were happy. Life was good.</p>
<p>Then I had the worst kind of idea: "Let's rewrite it in Python."</p>
<p>I was the one who said it. I have no one else to blame. I went in voluntarily. The retrospective was not kind.</p>
<h2 id="itll-be-easy-the-docs-lied"><a href="#itll-be-easy-the-docs-lied">"It'll Be Easy," The Docs Lied</a></h2>
<p>"Reflex is just Python." That's what the GitHub readme said. "You won't even need to touch JavaScript," tech Twitter added, with the confidence of someone who has never actually shipped this.</p>
<p><strong>Narrator:</strong> <em>It was not just Python. The JavaScript simply hid in the shadows and waited.</em></p>
<h3 id="what-i-expected"><a href="#what-i-expected">What I Expected</a></h3>

<div class="shiki-code-block" data-code="# Beautiful, Pythonic code that definitely just works
def dashboard():
    return render_my_beautiful_ui()">
  <div class="shiki-header">
    <span class="shiki-lang">python</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6A737D"># Beautiful, Pythonic code that definitely just works</span></span>
<span class="line"><span style="color:#D73A49">def</span><span style="color:#6F42C1"> dashboard</span><span style="color:#24292E">():</span></span>
<span class="line"><span style="color:#D73A49">    return</span><span style="color:#24292E"> render_my_beautiful_ui()</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D"># Beautiful, Pythonic code that definitely just works</span></span>
<span class="line"><span style="color:#F97583">def</span><span style="color:#B392F0"> dashboard</span><span style="color:#E1E4E8">():</span></span>
<span class="line"><span style="color:#F97583">    return</span><span style="color:#E1E4E8"> render_my_beautiful_ui()</span></span></code></pre></div>
</div>
<h3 id="what-i-got"><a href="#what-i-got">What I Got</a></h3>

<div class="shiki-code-block" data-code="# This is React with extra steps, worse autocomplete, and a confused IDE
def business_row(business: dict) -> rx.Component:
    return rx.table.row(
        rx.table.cell(
            rx.hstack(
                rx.box(
                    rx.icon(&#x22;building-2&#x22;, size=18, style={&#x22;color&#x22;: COLORS[&#x22;mauve&#x22;]}),
                    style={&#x22;background&#x22;: COLORS[&#x22;surface0&#x22;]},
                    class_name=&#x22;w-10 h-10 rounded-full flex items-center justify-center&#x22;,
                ),
                # ... 50 more lines of this">
  <div class="shiki-header">
    <span class="shiki-lang">python</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6A737D"># This is React with extra steps, worse autocomplete, and a confused IDE</span></span>
<span class="line"><span style="color:#D73A49">def</span><span style="color:#6F42C1"> business_row</span><span style="color:#24292E">(business: </span><span style="color:#005CC5">dict</span><span style="color:#24292E">) -> rx.Component:</span></span>
<span class="line"><span style="color:#D73A49">    return</span><span style="color:#24292E"> rx.table.row(</span></span>
<span class="line"><span style="color:#24292E">        rx.table.cell(</span></span>
<span class="line"><span style="color:#24292E">            rx.hstack(</span></span>
<span class="line"><span style="color:#24292E">                rx.box(</span></span>
<span class="line"><span style="color:#24292E">                    rx.icon(</span><span style="color:#032F62">"building-2"</span><span style="color:#24292E">, </span><span style="color:#E36209">size</span><span style="color:#D73A49">=</span><span style="color:#005CC5">18</span><span style="color:#24292E">, </span><span style="color:#E36209">style</span><span style="color:#D73A49">=</span><span style="color:#24292E">{</span><span style="color:#032F62">"color"</span><span style="color:#24292E">: </span><span style="color:#005CC5">COLORS</span><span style="color:#24292E">[</span><span style="color:#032F62">"mauve"</span><span style="color:#24292E">]}),</span></span>
<span class="line"><span style="color:#E36209">                    style</span><span style="color:#D73A49">=</span><span style="color:#24292E">{</span><span style="color:#032F62">"background"</span><span style="color:#24292E">: </span><span style="color:#005CC5">COLORS</span><span style="color:#24292E">[</span><span style="color:#032F62">"surface0"</span><span style="color:#24292E">]},</span></span>
<span class="line"><span style="color:#E36209">                    class_name</span><span style="color:#D73A49">=</span><span style="color:#032F62">"w-10 h-10 rounded-full flex items-center justify-center"</span><span style="color:#24292E">,</span></span>
<span class="line"><span style="color:#24292E">                ),</span></span>
<span class="line"><span style="color:#6A737D">                # ... 50 more lines of this</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D"># This is React with extra steps, worse autocomplete, and a confused IDE</span></span>
<span class="line"><span style="color:#F97583">def</span><span style="color:#B392F0"> business_row</span><span style="color:#E1E4E8">(business: </span><span style="color:#79B8FF">dict</span><span style="color:#E1E4E8">) -> rx.Component:</span></span>
<span class="line"><span style="color:#F97583">    return</span><span style="color:#E1E4E8"> rx.table.row(</span></span>
<span class="line"><span style="color:#E1E4E8">        rx.table.cell(</span></span>
<span class="line"><span style="color:#E1E4E8">            rx.hstack(</span></span>
<span class="line"><span style="color:#E1E4E8">                rx.box(</span></span>
<span class="line"><span style="color:#E1E4E8">                    rx.icon(</span><span style="color:#9ECBFF">"building-2"</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">size</span><span style="color:#F97583">=</span><span style="color:#79B8FF">18</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{</span><span style="color:#9ECBFF">"color"</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">COLORS</span><span style="color:#E1E4E8">[</span><span style="color:#9ECBFF">"mauve"</span><span style="color:#E1E4E8">]}),</span></span>
<span class="line"><span style="color:#FFAB70">                    style</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{</span><span style="color:#9ECBFF">"background"</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">COLORS</span><span style="color:#E1E4E8">[</span><span style="color:#9ECBFF">"surface0"</span><span style="color:#E1E4E8">]},</span></span>
<span class="line"><span style="color:#FFAB70">                    class_name</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">"w-10 h-10 rounded-full flex items-center justify-center"</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#E1E4E8">                ),</span></span>
<span class="line"><span style="color:#6A737D">                # ... 50 more lines of this</span></span></code></pre></div>
</div>
<p><code>class_name</code>. <code>style</code>. <code>rx.hstack</code>. Bro. This is JSX wearing a Python trench coat. I rewrote React. In Python. The React is still there. It's just hiding.</p>
<p><img src="/blog/wrapper_nightmare.png" alt="The Wrapper Nightmare"></p>
<h2 id="the-python-experience-i-was-promised-vs-what-i-received"><a href="#the-python-experience-i-was-promised-vs-what-i-received">The "Python Experience" I Was Promised vs. What I Received</a></h2>
<h3 id="string-concatenation-thats-a-legacy-feature-now"><a href="#string-concatenation-thats-a-legacy-feature-now">String Concatenation? That's a Legacy Feature Now.</a></h3>
<p>Remember simple string concatenation? Gone.</p>
<p><strong>Before (React):</strong></p>

<div class="shiki-code-block" data-code="<img src={&#x60;https://s3.amazonaws.com/${image}&#x60;} />">
  <div class="shiki-header">
    <span class="shiki-lang">jsx</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#24292E">&#x3C;</span><span style="color:#22863A">img</span><span style="color:#6F42C1"> src</span><span style="color:#D73A49">=</span><span style="color:#24292E">{</span><span style="color:#032F62">`https://s3.amazonaws.com/${</span><span style="color:#24292E">image</span><span style="color:#032F62">}`</span><span style="color:#24292E">} /></span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#E1E4E8">&#x3C;</span><span style="color:#85E89D">img</span><span style="color:#B392F0"> src</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">{</span><span style="color:#9ECBFF">`https://s3.amazonaws.com/${</span><span style="color:#E1E4E8">image</span><span style="color:#9ECBFF">}`</span><span style="color:#E1E4E8">} /></span></span></code></pre></div>
</div>
<p><strong>After (Reflex):</strong></p>

<div class="shiki-code-block" data-code="rx.image(
    src=&#x22;https://s3.amazonaws.com/&#x22; + log[&#x22;personImage&#x22;].to(str),
    # Because log[&#x22;personImage&#x22;] isn&#x27;t a string.
    # It&#x27;s a Var. A special Reflex type.
    # That you need to explicitly cast to a string.
    # In Python.
    # The language literally famous for duck typing.
    # We are doing explicit string casting in duck typing Python. Let that sink in.
)">
  <div class="shiki-header">
    <span class="shiki-lang">python</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#24292E">rx.image(</span></span>
<span class="line"><span style="color:#E36209">    src</span><span style="color:#D73A49">=</span><span style="color:#032F62">"https://s3.amazonaws.com/"</span><span style="color:#D73A49"> +</span><span style="color:#24292E"> log[</span><span style="color:#032F62">"personImage"</span><span style="color:#24292E">].to(</span><span style="color:#005CC5">str</span><span style="color:#24292E">),</span></span>
<span class="line"><span style="color:#6A737D">    # Because log["personImage"] isn't a string.</span></span>
<span class="line"><span style="color:#6A737D">    # It's a Var. A special Reflex type.</span></span>
<span class="line"><span style="color:#6A737D">    # That you need to explicitly cast to a string.</span></span>
<span class="line"><span style="color:#6A737D">    # In Python.</span></span>
<span class="line"><span style="color:#6A737D">    # The language literally famous for duck typing.</span></span>
<span class="line"><span style="color:#6A737D">    # We are doing explicit string casting in duck typing Python. Let that sink in.</span></span>
<span class="line"><span style="color:#24292E">)</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#E1E4E8">rx.image(</span></span>
<span class="line"><span style="color:#FFAB70">    src</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">"https://s3.amazonaws.com/"</span><span style="color:#F97583"> +</span><span style="color:#E1E4E8"> log[</span><span style="color:#9ECBFF">"personImage"</span><span style="color:#E1E4E8">].to(</span><span style="color:#79B8FF">str</span><span style="color:#E1E4E8">),</span></span>
<span class="line"><span style="color:#6A737D">    # Because log["personImage"] isn't a string.</span></span>
<span class="line"><span style="color:#6A737D">    # It's a Var. A special Reflex type.</span></span>
<span class="line"><span style="color:#6A737D">    # That you need to explicitly cast to a string.</span></span>
<span class="line"><span style="color:#6A737D">    # In Python.</span></span>
<span class="line"><span style="color:#6A737D">    # The language literally famous for duck typing.</span></span>
<span class="line"><span style="color:#6A737D">    # We are doing explicit string casting in duck typing Python. Let that sink in.</span></span>
<span class="line"><span style="color:#E1E4E8">)</span></span></code></pre></div>
</div>
<h3 id="empty-strings-straight-to-jail"><a href="#empty-strings-straight-to-jail">Empty Strings: Straight to Jail.</a></h3>
<p>My favorite error message of the entire ordeal:</p>

<div class="shiki-code-block" data-code="Error: A <Select.Item /> must have a value prop that is not an empty string.">
  <div class="shiki-header">
    <span class="shiki-lang">text</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span>Error: A &#x3C;Select.Item /> must have a value prop that is not an empty string.</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span>Error: A &#x3C;Select.Item /> must have a value prop that is not an empty string.</span></span></code></pre></div>
</div>
<p>The underlying React component — which I am not supposed to be thinking about because this is Python now — was offended by an empty string. So I had to create a <code>__all__</code> sentinel value and convert it back to an empty string in the state handler. In Python. Where <code>""</code> is a perfectly valid falsy value that the entire language ecosystem accepts without complaint.</p>
<p>Beautiful architecture. Truly.</p>
<h2 id="styling-tailwind-strings-in-a-python-file-that-compiles-to-react"><a href="#styling-tailwind-strings-in-a-python-file-that-compiles-to-react">Styling: Tailwind Strings in a Python File That Compiles to React</a></h2>
<p>"Use Tailwind," they said. So I did.</p>

<div class="shiki-code-block" data-code="class_name=&#x22;rounded-xl border p-5 hover:border-[#45475a] transition-colors duration-200&#x22;">
  <div class="shiki-header">
    <span class="shiki-lang">python</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#24292E">class_name</span><span style="color:#D73A49">=</span><span style="color:#032F62">"rounded-xl border p-5 hover:border-[#45475a] transition-colors duration-200"</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#E1E4E8">class_name</span><span style="color:#F97583">=</span><span style="color:#9ECBFF">"rounded-xl border p-5 hover:border-[#45475a] transition-colors duration-200"</span></span></code></pre></div>
</div>
<p>This is HTML. With extra steps. I am writing Tailwind class strings in Python, which Reflex compiles to React, which renders to HTML. Three layers of indirection to write what a <code>.tsx</code> file would give me directly — with syntax highlighting, autocomplete, and type checking via the Tailwind VS Code extension.</p>
<p>Instead, my IDE thinks I'm writing a very long, very sad novel.</p>
<h2 id="the-build-process-comparison-nobody-asked-for"><a href="#the-build-process-comparison-nobody-asked-for">The Build Process Comparison Nobody Asked For</a></h2>
<p><img src="/blog/waiting_for_build.png" alt="Waiting for Build"></p>
<p><strong>React + Vite:</strong></p>
<ul>
<li><code>npm run dev</code> — 200ms. I sneeze and it's running.</li>
<li>Hot reload — instant.</li>
<li>Build — 2 seconds.</li>
</ul>
<p><strong>Reflex:</strong></p>
<ul>
<li><code>reflex run</code> — wait for Python to initialize.</li>
<li>Wait for Reflex to compile the AST.</li>
<li>Wait for the frontend to build. (It spins up Node anyway. We're running Node. In the Python framework. Outstanding.)</li>
<li>Wait for the backend to start.</li>
<li>Changed a component's padding? Full recompile. See you in 30 seconds.</li>
<li>Port 8000 in use? Cool, we'll use 8001. Now your API calls all fail silently.</li>
<li>Please enjoy these Pydantic v1 deprecation warnings as a complimentary side dish.</li>
</ul>
<h2 id="the-definitive-list-of-benefits-of-reflex-over-react"><a href="#the-definitive-list-of-benefits-of-reflex-over-react">The Definitive List of Benefits of Reflex Over React</a></h2>
<p>Here it is. Complete and unabridged.</p>
<ol>
<li>You write "Python" instead of JavaScript.</li>
<li>...</li>
<li>That's it. That's the full value proposition.</li>
</ol>
<p>Except you're not actually writing Python. You're writing React component trees using Python syntax. You still think in components. You still manage state like hooks. You still write Tailwind strings. The only real change is the language — and that language is demonstrably worse at this specific task because it wasn't designed for the DOM.</p>
<p>The actual differences you get:</p>
<ul>
<li>Worse error messages (stack traces that cross a language barrier are a special kind of chaos)</li>
<li>Slower build times (you run Python AND Node)</li>
<li>A massive abstraction layer you cannot debug when it breaks</li>
<li>Documentation described as "rapidly evolving," which is documentation-speak for "incomplete"</li>
</ul>
<h2 id="the-final-tally"><a href="#the-final-tally">The Final Tally</a></h2>
<p><img src="/blog/react_vs_reflex.png" alt="React vs Reflex"></p>
<p><strong>Before (React):</strong> 260 lines of TypeScript. Fast dev loop. Immediate feedback.</p>
<p><strong>After (Reflex):</strong> 456 lines of Python. <code>Var</code> types pretending to be Python while outputting JS strings. Slower everything.</p>
<p><strong>Both versions:</strong> Still compile to React. Still need Node. Still run JavaScript in the browser.</p>
<p>We added Python as a middleman to a pipeline that begins and ends with JavaScript. Python, the language, is acting as a transpiler for React. We played ourselves at championship level.</p>
<p><img src="/blog/reflex-code.png" alt="Reflex Code IDE"></p>
<h2 id="why-does-this-framework-exist"><a href="#why-does-this-framework-exist">Why Does This Framework Exist?</a></h2>
<p>Honest question. The pitch is "write web apps in Python." The reality is "write React in Python syntax while fighting type systems that make no sense and watching builds measured in geological time."</p>
<p>If you want Python: use FastAPI. Use Django. Use HTMX if you need interactivity without touching JS. These are proven tools designed for the exact tasks they perform.</p>
<p>If you want a modern SPA: write React. Or Svelte. Or Vue. Anything where the tooling was built for the DOM.</p>
<p>Don't use a framework trying to be both that ends up failing at both.</p>
<h2 id="hot-take-conclusion"><a href="#hot-take-conclusion">Hot Take Conclusion</a></h2>
<p>Would I recommend Reflex? Only if you:</p>
<ul>
<li>Actively enjoy debugging type systems that shouldn't exist.</li>
<li>Need a legitimate reason to get up and make coffee while your frontend "compiles."</li>
<li>Believe "it's Python so it must be simpler" is a valid architectural decision and not just a coping mechanism.</li>
</ul>
<p>For everyone else: <strong>learn React</strong>. One weekend. You'll be infinitely more productive, your tooling will work, and you won't find yourself casting duck-typed variables to strings before a string concatenation.</p>
<hr>
<p>If you enjoy staring at code that shouldn't exist, here's the project I migrated:</p>
<div class="w-full items-center justify-center flex">
<a href="https://github.com/Mic-360/tryon-dash/tree/reflex" target="_blank" class="text-blue-600 underline flex items-center flex-col">
<svg width="32" height="32" viewBox="0 0 16 16" fill="currentColor">
  <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"></path>
</svg>
<span class="ml-2 text-lg font-semibold capitalize">
  TryOn Dash Reflex Code
</span>
</a>
</div>
<hr>
<p><em>Written while waiting for Reflex to recompile for the 47th time today.</em></p>]]></content:encoded>
    </item>
    <item>
      <title>Unhinged Guide to Doom Coding: Android Edition</title>
      <link>https://bhaumicsingh.dev/blog/doom-coding</link>
      <guid isPermaLink="true">https://bhaumicsingh.dev/blog/doom-coding</guid>
      <description>Stop scrolling Instagram reels and start SSH-ing into your production server from your phone. A guide to the most aggressive mobile dev setup ever conceived.</description>
      <pubDate>Fri, 23 Jan 2026 00:00:00 GMT</pubDate>
      <author>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</author>
      <content:encoded><![CDATA[<h1 id="unhinged-guide-to-doom-coding-android-edition"><a href="#unhinged-guide-to-doom-coding-android-edition">Unhinged Guide to Doom Coding: Android Edition</a></h1>
<blockquote>
<p>"If you aren't shipping features while your friends are paying $20 for a mid drink at the club, are you even a dev?"</p>
</blockquote>
<p>Nobody:
Me at 2 AM in an Uber: <em>opens Termux</em></p>
<p>Welcome to <strong>Doom Coding</strong> — doom scrolling except instead of passively destroying your attention span, you're actively destroying your work-life balance. More productive. Same outcome.</p>
<p>While normal people "go outside" and "exist in the physical world," we're SSH-ing into production from the back of a ride-share. Is it healthy? Incredible question, we're moving on.</p>
<p>After this 5-minute setup, you'll be able to code anywhere you have cell service. Even in the air. Even on a run. Even at the club. These are all real options you will have. Whether you should use them is a completely separate conversation.</p>
<p><img src="https://github.com/user-attachments/assets/bda532b6-3963-4ace-907b-ebbf9460032c" alt="Code in the air"></p>
<p><img src="https://github.com/user-attachments/assets/f5449004-0a76-44ee-9922-a5cce1423f93" alt="Code on a run">
<em>Literally higher than your server's uptime. Zone 2 heart rate. Production logs open. Living the dream.</em></p>
<h2 id="the-stack-minimalism-is-for-people-whove-never-had-scope-creep"><a href="#the-stack-minimalism-is-for-people-whove-never-had-scope-creep">The Stack (Minimalism Is for People Who've Never Had Scope Creep)</a></h2>
<p>The Holy Quadrinity of mobile dev enlightenment:</p>
<ol>
<li><strong>Termux</strong>: Your new home. A terminal emulator that turns your $1,200 TikTok consumption device into an actual power tool.</li>
<li><strong>Node.js/Bun</strong>: Because of course we're using JavaScript environments. It's 2026.</li>
<li><strong>Claude Code</strong>: The paid AI deity for people with budget lines.</li>
<li><strong>OpenCode + Antigravity Auth</strong>: The "I have principles" AI deity for everyone aggressively dodging API metering.</li>
</ol>
<h2 id="step-1-the-sacrifice-termux-setup"><a href="#step-1-the-sacrifice-termux-setup">Step 1: The Sacrifice (Termux Setup)</a></h2>
<p>Download Termux. <strong>Not from the Play Store.</strong> That version is archaeological. Get it from F-Droid or GitHub like someone who reads changelogs.</p>
<p>Open it up and run the ritual:</p>

<div class="shiki-code-block" data-code="pkg update &#x26;&#x26; pkg upgrade">
  <div class="shiki-header">
    <span class="shiki-lang">bash</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6F42C1">pkg</span><span style="color:#032F62"> update</span><span style="color:#24292E"> &#x26;&#x26; </span><span style="color:#6F42C1">pkg</span><span style="color:#032F62"> upgrade</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">pkg</span><span style="color:#9ECBFF"> update</span><span style="color:#E1E4E8"> &#x26;&#x26; </span><span style="color:#B392F0">pkg</span><span style="color:#9ECBFF"> upgrade</span></span></code></pre></div>
</div>
<p>Install Ubuntu (no, I don't care about your Alpine evangelism, I want <code>apt-get</code> to work):</p>

<div class="shiki-code-block" data-code="pkg install proot-distro
proot-distro install ubuntu
proot-distro login ubuntu">
  <div class="shiki-header">
    <span class="shiki-lang">bash</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6F42C1">pkg</span><span style="color:#032F62"> install</span><span style="color:#032F62"> proot-distro</span></span>
<span class="line"><span style="color:#6F42C1">proot-distro</span><span style="color:#032F62"> install</span><span style="color:#032F62"> ubuntu</span></span>
<span class="line"><span style="color:#6F42C1">proot-distro</span><span style="color:#032F62"> login</span><span style="color:#032F62"> ubuntu</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">pkg</span><span style="color:#9ECBFF"> install</span><span style="color:#9ECBFF"> proot-distro</span></span>
<span class="line"><span style="color:#B392F0">proot-distro</span><span style="color:#9ECBFF"> install</span><span style="color:#9ECBFF"> ubuntu</span></span>
<span class="line"><span style="color:#B392F0">proot-distro</span><span style="color:#9ECBFF"> login</span><span style="color:#9ECBFF"> ubuntu</span></span></code></pre></div>
</div>
<p>Update your brand new chroot:</p>

<div class="shiki-code-block" data-code="apt update &#x26;&#x26; apt upgrade -y
apt install curl git nodejs npm -y">
  <div class="shiki-header">
    <span class="shiki-lang">bash</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6F42C1">apt</span><span style="color:#032F62"> update</span><span style="color:#24292E"> &#x26;&#x26; </span><span style="color:#6F42C1">apt</span><span style="color:#032F62"> upgrade</span><span style="color:#005CC5"> -y</span></span>
<span class="line"><span style="color:#6F42C1">apt</span><span style="color:#032F62"> install</span><span style="color:#032F62"> curl</span><span style="color:#032F62"> git</span><span style="color:#032F62"> nodejs</span><span style="color:#032F62"> npm</span><span style="color:#005CC5"> -y</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">apt</span><span style="color:#9ECBFF"> update</span><span style="color:#E1E4E8"> &#x26;&#x26; </span><span style="color:#B392F0">apt</span><span style="color:#9ECBFF"> upgrade</span><span style="color:#79B8FF"> -y</span></span>
<span class="line"><span style="color:#B392F0">apt</span><span style="color:#9ECBFF"> install</span><span style="color:#9ECBFF"> curl</span><span style="color:#9ECBFF"> git</span><span style="color:#9ECBFF"> nodejs</span><span style="color:#9ECBFF"> npm</span><span style="color:#79B8FF"> -y</span></span></code></pre></div>
</div>
<p>Congratulations. Your phone is now a development machine. You can tell people you "work entirely in the terminal" while waiting for your cold brew. They won't know what that means. It'll feel good anyway.</p>
<h2 id="step-2-choose-your-ai-deity"><a href="#step-2-choose-your-ai-deity">Step 2: Choose Your AI Deity</a></h2>
<p>Two paths. One has a monthly billing cycle. The other has character.</p>
<h3 id="path-a-the-i-have-a-budget-line-for-this-option-claude-code"><a href="#path-a-the-i-have-a-budget-line-for-this-option-claude-code">Path A: The "I Have a Budget Line for This" Option (Claude Code)</a></h3>
<p>If you're fine with paying Anthropic to technically do your job while you drink coffee:</p>

<div class="shiki-code-block" data-code="curl -fsSL https://claude.ai/install.sh | bash
claude --remote">
  <div class="shiki-header">
    <span class="shiki-lang">bash</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6F42C1">curl</span><span style="color:#005CC5"> -fsSL</span><span style="color:#032F62"> https://claude.ai/install.sh</span><span style="color:#D73A49"> |</span><span style="color:#6F42C1"> bash</span></span>
<span class="line"><span style="color:#6F42C1">claude</span><span style="color:#005CC5"> --remote</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">curl</span><span style="color:#79B8FF"> -fsSL</span><span style="color:#9ECBFF"> https://claude.ai/install.sh</span><span style="color:#F97583"> |</span><span style="color:#B392F0"> bash</span></span>
<span class="line"><span style="color:#B392F0">claude</span><span style="color:#79B8FF"> --remote</span></span></code></pre></div>
</div>
<p>Done. Go back to your avocado toast.</p>
<h3 id="path-b-the-i-have-principles-option-opencode--antigravity-auth"><a href="#path-b-the-i-have-principles-option-opencode--antigravity-auth">Path B: The "I Have Principles" Option (OpenCode + Antigravity Auth)</a></h3>
<p>For those of us who prefer to keep money for important things — RAM, mechanical switches, that one keyboard you've been eyeing for six months.</p>
<p>We use <strong>OpenCode</strong>.</p>
<p>I found the Antigravity plugin while responsibly not doom-scrolling, and it genuinely simplified my life. No monthly AI subscription just to get an LLM to tell me why my regex is objectively wrong.</p>
<p>Install OpenCode:</p>

<div class="shiki-code-block" data-code="curl -fsSL https://opencode.ai/install | bash">
  <div class="shiki-header">
    <span class="shiki-lang">bash</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6F42C1">curl</span><span style="color:#005CC5"> -fsSL</span><span style="color:#032F62"> https://opencode.ai/install</span><span style="color:#D73A49"> |</span><span style="color:#6F42C1"> bash</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">curl</span><span style="color:#79B8FF"> -fsSL</span><span style="color:#9ECBFF"> https://opencode.ai/install</span><span style="color:#F97583"> |</span><span style="color:#B392F0"> bash</span></span></code></pre></div>
</div>
<p>Now add the <strong><a href="https://github.com/NoeFabris/opencode-antigravity-auth">Antigravity Auth plugin</a></strong>. This lets you tap Google's Gemini and Claude quotas via OAuth like someone who knows how OAuth works.</p>
<p>Edit your config at <code>~/.config/opencode/opencode.json</code> (yes, with <code>nano</code> on your phone screen, you can do it, I believe in you):</p>

<div class="shiki-code-block" data-code="{
  &#x22;$schema&#x22;: &#x22;https://opencode.ai/config.json&#x22;,
  &#x22;plugin&#x22;: [&#x22;opencode-antigravity-auth@latest&#x22;],
  &#x22;provider&#x22;: {
    &#x22;google&#x22;: {
      &#x22;models&#x22;: {
        // Paste the Antigravity auth schema here for all the free-tier models.
        // I&#x27;m not pasting all 60 lines. Figure it out.
      }
    }
  }
}">
  <div class="shiki-header">
    <span class="shiki-lang">json</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#24292E">{</span></span>
<span class="line"><span style="color:#005CC5">  "$schema"</span><span style="color:#24292E">: </span><span style="color:#032F62">"https://opencode.ai/config.json"</span><span style="color:#24292E">,</span></span>
<span class="line"><span style="color:#005CC5">  "plugin"</span><span style="color:#24292E">: [</span><span style="color:#032F62">"opencode-antigravity-auth@latest"</span><span style="color:#24292E">],</span></span>
<span class="line"><span style="color:#005CC5">  "provider"</span><span style="color:#24292E">: {</span></span>
<span class="line"><span style="color:#005CC5">    "google"</span><span style="color:#24292E">: {</span></span>
<span class="line"><span style="color:#005CC5">      "models"</span><span style="color:#24292E">: {</span></span>
<span class="line"><span style="color:#6A737D">        // Paste the Antigravity auth schema here for all the free-tier models.</span></span>
<span class="line"><span style="color:#6A737D">        // I'm not pasting all 60 lines. Figure it out.</span></span>
<span class="line"><span style="color:#24292E">      }</span></span>
<span class="line"><span style="color:#24292E">    }</span></span>
<span class="line"><span style="color:#24292E">  }</span></span>
<span class="line"><span style="color:#24292E">}</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#E1E4E8">{</span></span>
<span class="line"><span style="color:#79B8FF">  "$schema"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"https://opencode.ai/config.json"</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#79B8FF">  "plugin"</span><span style="color:#E1E4E8">: [</span><span style="color:#9ECBFF">"opencode-antigravity-auth@latest"</span><span style="color:#E1E4E8">],</span></span>
<span class="line"><span style="color:#79B8FF">  "provider"</span><span style="color:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#79B8FF">    "google"</span><span style="color:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#79B8FF">      "models"</span><span style="color:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#6A737D">        // Paste the Antigravity auth schema here for all the free-tier models.</span></span>
<span class="line"><span style="color:#6A737D">        // I'm not pasting all 60 lines. Figure it out.</span></span>
<span class="line"><span style="color:#E1E4E8">      }</span></span>
<span class="line"><span style="color:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span></code></pre></div>
</div>
<p>Now login:</p>

<div class="shiki-code-block" data-code="opencode auth login">
  <div class="shiki-header">
    <span class="shiki-lang">bash</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6F42C1">opencode</span><span style="color:#032F62"> auth</span><span style="color:#032F62"> login</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">opencode</span><span style="color:#9ECBFF"> auth</span><span style="color:#9ECBFF"> login</span></span></code></pre></div>
</div>
<p><strong>CRITICAL NOTE</strong>: Android Termux does not support the full OpenCode TUI natively unless you have the patience of someone who has never experienced latency. You're a terminal purist now. Use the <code>--web</code> flag or your screen will look like it's having a medical event.</p>

<div class="shiki-code-block" data-code="opencode web --hostname [YOUR_TAILSCALE_IP] --port 4096">
  <div class="shiki-header">
    <span class="shiki-lang">bash</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <div class="shiki-theme shiki-light"><pre class="shiki github-light" style="background-color:#fff;color:#24292e" tabindex="0"><code><span class="line"><span style="color:#6F42C1">opencode</span><span style="color:#032F62"> web</span><span style="color:#005CC5"> --hostname</span><span style="color:#24292E"> [YOUR_TAILSCALE_IP] --port 4096</span></span></code></pre></div>
  <div class="shiki-theme shiki-dark"><pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">opencode</span><span style="color:#9ECBFF"> web</span><span style="color:#79B8FF"> --hostname</span><span style="color:#E1E4E8"> [YOUR_TAILSCALE_IP] --port 4096</span></span></code></pre></div>
</div>
<h2 id="step-3-tailscale-the-packet-wormhole"><a href="#step-3-tailscale-the-packet-wormhole">Step 3: Tailscale (The Packet Wormhole)</a></h2>
<p>Running <code>opencode web</code> means your browser needs to reach that proot-distro Ubuntu instance without Android's sandbox filing a complaint. Enter <strong>Tailscale</strong>.</p>
<ol>
<li><strong>Install the app.</strong> It's in the Play Store. This one you can get from there.</li>
<li><strong>Login.</strong> Use the same account you use everywhere else.</li>
<li><strong>Turn it on.</strong> You're now part of the mesh.</li>
</ol>
<p>Find your phone's Tailscale IP — the one that looks like <code>100.x.y.z</code>. That is your new identity. Use it in the <code>opencode web</code> command so you can hit your dev environment from Chrome, or from your laptop if you're feeling spectacularly lazy.</p>
<p><img src="https://github.com/user-attachments/assets/e5da61ab-828d-4f40-920d-840306c284ed" alt="Tailscale status"></p>
<h2 id="step-4-ascend-to-godhood"><a href="#step-4-ascend-to-godhood">Step 4: Ascend to Godhood</a></h2>
<p>You are now coding from something that fits in your pocket.</p>
<p>You can push to production at a farmers market. Fix a bug during a movie. Ship features between sets at the gym. All of these are real actions you can now take. None of them are actions you should take without thinking carefully about your life choices first.</p>
<p><img src="https://github.com/user-attachments/assets/d92df1cd-570b-49b0-96f7-a5ce31644a6f" alt="Claude coding"></p>
<h2 id="pro-tips-for-the-committed"><a href="#pro-tips-for-the-committed">Pro Tips for the Committed</a></h2>
<ul>
<li><strong>Document everything</strong>: Always ask your AI to update <code>CLAUDE.md</code> or <code>SHIPPING.md</code>. Your future coherent self will be grateful that your 2 AM self left notes.</li>
<li><strong>The Club Protocol</strong>: If you're coding at the club, crank the brightness to max. Everyone should know you are categorically more important than whatever the DJ is doing right now.</li>
</ul>
<p><img src="https://github.com/user-attachments/assets/f3d05c00-a47a-4ebe-8db3-1795a6a0798c" alt="Code at the club">
<em>Me when the drop hits but the prod logs are showing 500s.</em></p>
<h2 id="troubleshooting-otherwise-known-as-skill-issues"><a href="#troubleshooting-otherwise-known-as-skill-issues">Troubleshooting (Otherwise Known As Skill Issues)</a></h2>
<p>If it doesn't work:</p>
<ol>
<li><strong>Termux Sleep</strong>: Android loves murdering background processes. Enable "Keep CPU awake" in the Termux notification tray or your session dies faster than a startup with no runway.</li>
<li><strong>Node version drama</strong>: If <code>npm</code> acts up, <code>pkg reinstall nodejs-lts</code>. Do not negotiate with it.</li>
<li><strong>OpenCode TUI</strong>: I told you. Use <code>--web</code>. If you forgot: classic skill issue. Noted for the retrospective.</li>
</ol>
<hr>
<p>Happy Doom Coding. Go ship something before the concept of "healthy work-life balance" accidentally enters your mind and ruins everything.</p>]]></content:encoded>
    </item>
    <item>
      <title>Auto Copilot Review: Outsourcing My Insecurities to an LLM on Every Git Add</title>
      <link>https://bhaumicsingh.dev/blog/auto-copilot-review</link>
      <guid isPermaLink="true">https://bhaumicsingh.dev/blog/auto-copilot-review</guid>
      <description>Because hitting `git commit -m &quot;fix maybe?&quot;` requires too much confidence in my own code.</description>
      <pubDate>Thu, 25 Dec 2025 00:00:00 GMT</pubDate>
      <author>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</author>
      <content:encoded><![CDATA[<h1 id="auto-copilot-review-your-code-is-bad-and-an-ai-knows-it"><a href="#auto-copilot-review-your-code-is-bad-and-an-ai-knows-it">Auto Copilot Review: Your Code Is Bad and an AI Knows It</a></h1>
<blockquote>
<p>Writing code is easy. Reviewing your own code is functionally impossible because you already convinced yourself it was fine an hour ago.</p>
</blockquote>
<p>You know that moment. Cursor hovering over the commit button. The cold creep of dread. Did you leave a <code>console.log("here")</code> in? Did you break a type in a file you only vaguely remember touching? Did you rename that variable to something embarrassing?</p>
<p>You have two options:</p>
<ol>
<li>Scroll through the diff, lie to yourself, and push to master.</li>
<li>Manually open Copilot Chat, paste the diff, ask it to review, wait, context-switch back, forget what you were doing.</li>
</ol>
<p>Option 1 leads to PR comments that age poorly. Option 2 is a full flow-killing chore that requires so much ceremony you never actually do it.</p>
<p>What if the AI just... watched?</p>
<h2 id="making-staging-aggressively-judgemental"><a href="#making-staging-aggressively-judgemental">Making Staging Aggressively Judgemental</a></h2>
<p>I built a VS Code extension that hooks into the Git API. Every time you stage a file — <code>git add</code>, the little <code>+</code> button, however you do it — <strong>Auto Copilot Review</strong> wakes up, grabs the diff of everything staged, and fires it straight into GitHub Copilot Chat with a review request.</p>
<p>Zero clicks. Zero context-switching. You stage a file, you glance right, and there's already a breakdown of your logic problems waiting for you like a very patient disappointed parent.</p>
<p>It happens entirely in the background. The extension doesn't ask. It doesn't warn you. It just starts judging.</p>
<h2 id="the-god-slow-down-problem"><a href="#the-god-slow-down-problem">The "God, Slow Down" Problem</a></h2>
<p>My first build was too eager. Embarrassingly eager.</p>
<p>I stage files by rapidly clicking <code>+</code> on five different files in a row, which in V1 meant five simultaneous Copilot API requests firing within one second. The Chat panel had a complete breakdown — five competing reviews flashing at once, each overwriting the last. It looked like a merge conflict with extra steps.</p>
<p>Turns out teaching a machine to review your code also means teaching it patience.</p>
<p>I added a configurable debounce timer — default 5 seconds. Now you can stage as frantically as you want. Once you stop moving for 5 seconds, everything gets bundled into a single review request. One clean response. No chaos.</p>
<p>It's a small thing. It's the thing that made it actually usable.</p>
<h2 id="the-part-where-it-actually-works"><a href="#the-part-where-it-actually-works">The Part Where It Actually Works</a></h2>
<p>Within seconds of staging: a clean breakdown of your logic flaws, typos, forgotten debug statements, and edge cases you were hoping nobody would notice.</p>
<p>It feels like having an extremely pedantic code reviewer permanently installed on your shoulder, waiting for exactly the moment you're about to commit something embarrassing so they can quietly intervene.</p>
<p>The best part is it requires genuinely nothing from you after install. No commands. No shortcuts. No workflow changes. It just exists, watching, reviewing, and silently saving you from the PR comments that would have aged very badly.</p>
<p>Is it humbling? Yes. Does it catch things you'd have shipped? Also yes.</p>
<p>If you have a Copilot subscription and a realistic assessment of your late-night coding judgment, <a href="https://github.com/Mic-360/auto-copilot-review">install the extension</a>. Your future self — the one reading PR comments — will thank you.</p>]]></content:encoded>
    </item>
    <item>
      <title>I Asked Google Gemini to Dress People. The Results Were Educational.</title>
      <link>https://bhaumicsingh.dev/blog/tryon-modal-ai-fitting-room</link>
      <guid isPermaLink="true">https://bhaumicsingh.dev/blog/tryon-modal-ai-fitting-room</guid>
      <description>Online fashion loses $400B a year to returns. I fixed it with PHP, a WooCommerce plugin, and a carefully worded prompt. No research team required.</description>
      <pubDate>Tue, 11 Feb 2025 00:00:00 GMT</pubDate>
      <author>bhaumic@bhaumicsingh.dev (Bhaumic Singh)</author>
      <content:encoded><![CDATA[<h1 id="i-asked-google-gemini-to-dress-people-the-results-were-educational"><a href="#i-asked-google-gemini-to-dress-people-the-results-were-educational">I Asked Google Gemini to Dress People. The Results Were Educational.</a></h1>
<blockquote>
<p>The fashion industry loses $400 billion a year to returns.
Me: I'll fix it. With PHP.
Everyone: ...
Me: And a modal.</p>
</blockquote>
<p>Look, I could have built a custom vision transformer. Trained it on proprietary fashion datasets. Deployed it on a GPU cluster. Applied for Series A funding and put "AI/ML Engineer" in my LinkedIn bio.</p>
<p>Instead I wrote a WordPress plugin.</p>
<p>The fitting room still works. The VC funding was unnecessary.</p>
<p><img src="/tryon-preview.png" alt="Tryon Modal"></p>
<h2 id="the-problem-is-embarrassingly-real"><a href="#the-problem-is-embarrassingly-real">The Problem Is Embarrassingly Real</a></h2>
<p>You can't try clothes on through a screen. So you buy three sizes, keep one, return two. The warehouse restocks it. The planet suffers. Customer service handles the complaints. Everyone loses except the shipping company.</p>
<p>This problem has an obvious solution that somehow costs $99/month if you want the branded SaaS version of it.</p>
<p>So I built the unbranded version. It's called <code>tryon-modal</code>. It's open source. It takes longer to read this post than to get it running.</p>
<h2 id="its-just-a-wordpress-plugin--yes-and"><a href="#its-just-a-wordpress-plugin--yes-and">"It's Just a WordPress Plugin" — Yes. And?</a></h2>
<p>WooCommerce runs on 30% of all e-commerce sites. If you're targeting fashion stores, you're targeting WordPress. That's not a compromise — that's a distribution strategy.</p>
<p>Also, PHP 8 has named arguments, enums, fibers, union types, and a JIT compiler. If you're still dunking on PHP in 2025, that tweet isn't landing the way you think it is.</p>
<h2 id="the-ai-part-which-is-embarrassingly-simple"><a href="#the-ai-part-which-is-embarrassingly-simple">The AI Part (Which Is Embarrassingly Simple)</a></h2>
<p>Everyone imagines "AI virtual try-on" means a six-month research sprint and a team of computer vision PhDs.</p>
<p>What it actually means in 2025: Google Gemini's multimodal API already understands images, garments, and human figures. So instead of training anything, you send it three things:</p>
<ol>
<li>The user's photo</li>
<li>The garment image</li>
<li>A prompt that is genuinely just English sentences</li>
</ol>
<p>And it gives you back a try-on composite. The "AI research" was writing a good prompt and a clean PHP API handler.</p>

<div class="shiki-code-block" data-code="// This is the entire AI integration. Don&#x27;t overthink it.
$response = $this->gemini->generateContent([
  &#x27;parts&#x27; => [
    [&#x27;text&#x27; => $this->buildTryOnPrompt($garmentCategory)],
    [&#x27;inline_data&#x27; => [&#x27;mime_type&#x27; => &#x27;image/jpeg&#x27;, &#x27;data&#x27; => $userPhotoBase64]],
    [&#x27;inline_data&#x27; => [&#x27;mime_type&#x27; => &#x27;image/jpeg&#x27;, &#x27;data&#x27; => $garmentImageBase64]],
  ]
]);">
  <div class="shiki-header">
    <span class="shiki-lang">php</span>
    <button class="shiki-copy-btn" onclick="(function(btn){
      const code = btn.closest(&#x27;.shiki-code-block&#x27;).dataset.code;
      const decoded = code.replace(/&#x22;/g, &#x27;&#x22;&#x27;).replace(/</g, &#x27;<&#x27;).replace(/>/g, &#x27;>&#x27;).replace(/&#x26;/g, &#x27;&#x26;&#x27;);
      navigator.clipboard.writeText(decoded).then(function(){
        btn.textContent = &#x27;Copied!&#x27;;
        setTimeout(function(){ btn.textContent = &#x27;Copy&#x27;; }, 2000);
      });
    })(this)">Copy</button>
  </div>
  <pre class="shiki-fallback"><code>// This is the entire AI integration. Don't overthink it.
$response = $this->gemini->generateContent([
  'parts' => [
    ['text' => $this->buildTryOnPrompt($garmentCategory)],
    ['inline_data' => ['mime_type' => 'image/jpeg', 'data' => $userPhotoBase64]],
    ['inline_data' => ['mime_type' => 'image/jpeg', 'data' => $garmentImageBase64]],
  ]
]);</code></pre>
</div>
<p>That's it. That's the model call. The rest is plumbing.</p>
<h2 id="the-garment-mapper-my-favorite-feature-nobody-will-tweet-about"><a href="#the-garment-mapper-my-favorite-feature-nobody-will-tweet-about">The Garment Mapper: My Favorite Feature Nobody Will Tweet About</a></h2>
<p>Here's the thing about Gemini: the prompt needs context. "Is this a top or a dress?" changes the output quality significantly. Wildly, visibly significantly.</p>
<p>So I built a <strong>Garment Mapper</strong> — an admin UI where store owners bulk-tag their product images as Tops, Bottoms, Dresses, Outerwear, etc. The tagging feeds into <code>buildTryOnPrompt()</code>. The prompt changes. The results get noticeably better.</p>
<p>It's the most impactful feature in the plugin and the least glamorous one to explain. That is the software development experience in a single sentence.</p>
<h2 id="security-the-part-nobody-claps-for"><a href="#security-the-part-nobody-claps-for">Security: The Part Nobody Claps For</a></h2>
<p>A plugin that accepts user-uploaded photos and routes them to an external API has real attack surfaces. So we do the boring-but-correct stuff:</p>
<ul>
<li><strong>Nonces</strong> on every AJAX action (CSRF protection, WordPress-style)</li>
<li><strong><code>esc_html()</code> and <code>sanitize_*</code></strong> everywhere user data touches the DOM</li>
<li><strong><code>$wpdb->prepare()</code></strong> on every database query — no raw SQL, ever</li>
<li><strong>MIME type validation</strong> on uploaded photos before they go anywhere near Gemini</li>
<li><strong>HTTPS enforcement</strong> on all API calls</li>
</ul>
<p>Nobody writes blog posts about not having CVEs. The alternative is worse.</p>
<h2 id="the-modal-invisible-until-needed"><a href="#the-modal-invisible-until-needed">The Modal: Invisible Until Needed</a></h2>
<p>The worst thing a plugin can do is slow down a product page. So the modal loads nothing — zero CSS, zero JS overhead — until the user clicks <strong>Virtual Try-On</strong>. Then it bootstraps the whole UI on demand.</p>
<p>Once open, it supports:</p>
<ul>
<li>Drag-and-drop photo upload with live preview</li>
<li>A results pane that fades in when Gemini responds</li>
<li>A download button for the try-on image</li>
<li>An <strong>Add to Cart</strong> button inside the modal — no page navigation required</li>
</ul>
<p>The whole thing is namespaced under <code>nano-tryon-*</code> CSS classes so theme overrides work without touching plugin files. Because <code>!important</code>-ing your plugin styles into the host theme is a war crime.</p>
<h2 id="session-history-two-sql-queries-actually-useful"><a href="#session-history-two-sql-queries-actually-useful">Session History: Two SQL Queries, Actually Useful</a></h2>
<p>Try-on sessions get stored in <code>wp_tryon_sessions</code> — image paths, garment used, timestamp, user ID. Users can revisit what looked good before buying.</p>
<p>Could I have skipped it? Yes. Did I? No. Users who come back and check what they tried on last week convert better than users who don't. It cost two SQL queries and a loop. The ROI math is straightforward.</p>
<h2 id="does-it-work"><a href="#does-it-work">Does It Work?</a></h2>
<p>Yes. Upload a photo, pick a product, wait a few seconds, see yourself wearing it — without leaving the product page, without an account on a third-party app, without a $99/month subscription.</p>
<p>A WooCommerce store. A Gemini API key. A five-minute install.</p>
<p>Sometimes the unsexy tool for the unsexy platform solves the real problem better than the venture-backed alternative. Sometimes the fitting room is a PHP modal.</p>
<hr>
<p>The code is <a href="https://github.com/Mic-360/tryon-modal">open source on GitHub</a>. Your Gemini API key is free up to a quota that will absolutely cover your first thousand try-ons.</p>]]></content:encoded>
    </item>
  </channel>
</rss>