[{"content":"mirror-habit now ships as an Obsidian community plugin, not just a web app. Same React core, opened in a pane inside the vault.\nHow it\u0026rsquo;s built The React app (React 19 + Vite + Supabase) is synced into src/app/ from the main repo by scripts/sync-source.sh, then apply-patches.mjs adapts it to Obsidian — scoped styles, a lazy Supabase client, and Obsidian-safe auth links. One source, two targets (web + plugin). Offline-first: with no config, everything runs on local vault data via the plugin\u0026rsquo;s data.json — habit tracking, streaks, challenges, titles/levels. Supabase adds multi-device sync when you supply a URL + anon key. The two real gotchas Plugin view crash — the view registered as new MirrorHabitView() without passing the leaf, so opening the pane threw Cannot read properties of undefined (reading 'app'). Fix: (leaf) =\u0026gt; new MirrorHabitView(leaf).\nOAuth inside Obsidian\u0026rsquo;s sandbox — the plug-in origin can\u0026rsquo;t complete a Google redirect flow, so Google sign-in opens the web app in a browser tab (mirrorhabit.pages.dev/login) and the plugin watches for the session to land. This is also why the app\u0026rsquo;s default URL had to be corrected to pages.dev (it was still netlify.app in several spots after the migration).\nShipping notes Built via esbuild (JS) + a small CSS build step, outputs main.js + styles.css copied into the vault\u0026rsquo;s .obsidian/plugins/. Manifest declares minAppVersion 1.4.0; license MIT; README lays out the required commerce/backend disclosures for community publishing (offline default, external Supabase only when configured, no telemetry). The \u0026ldquo;same React core, two surfaces\u0026rdquo; pattern is holding up — the plugin is mostly plumbing, with the hard thinking already done in the web app.\n","permalink":"https://varunsdevlog.pages.dev/mirror-habit-as-an-obsidian-plugin/","summary":"\u003cp\u003e\u003cstrong\u003emirror-habit\u003c/strong\u003e now ships as an \u003cstrong\u003eObsidian community plugin\u003c/strong\u003e, not just a web app. Same React core, opened in a pane inside the vault.\u003c/p\u003e\n\u003ch2 id=\"how-its-built\"\u003eHow it\u0026rsquo;s built\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eThe React app (React 19 + Vite + Supabase) is \u003cstrong\u003esynced into \u003ccode\u003esrc/app/\u003c/code\u003e\u003c/strong\u003e from the main repo by \u003ccode\u003escripts/sync-source.sh\u003c/code\u003e, then \u003ccode\u003eapply-patches.mjs\u003c/code\u003e adapts it to Obsidian — scoped styles, a lazy Supabase client, and Obsidian-safe auth links. One source, two targets (web + plugin).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eOffline-first\u003c/strong\u003e: with no config, everything runs on local vault data via the plugin\u0026rsquo;s \u003ccode\u003edata.json\u003c/code\u003e — habit tracking, streaks, challenges, titles/levels. Supabase adds multi-device sync when you supply a URL + anon key.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"the-two-real-gotchas\"\u003eThe two real gotchas\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003ePlugin view crash\u003c/strong\u003e — the view registered as \u003ccode\u003enew MirrorHabitView()\u003c/code\u003e without passing the leaf, so opening the pane threw \u003ccode\u003eCannot read properties of undefined (reading 'app')\u003c/code\u003e. Fix: \u003ccode\u003e(leaf) =\u0026gt; new MirrorHabitView(leaf)\u003c/code\u003e.\u003c/p\u003e","title":"mirror-habit as an Obsidian plugin"},{"content":"mirror-habit went offline for three days because of one mistake: a \u0026ldquo;clear the cache\u0026rdquo; request that actually deleted the Pages project. Post-mortem, because the exact sequence is worth remembering.\nTimeline Sep 13 — web app had a netlify.app URL hardcoded in a few places while the real host was mirrorhabit.pages.dev. Google login bounced off /?code= instead of /auth/callback, and the site seemed stale → asked an agent to \u0026ldquo;bust cache.\u0026rdquo; Pages has no one-click edge-cache purge API for a project, so the agent read the OAuth token out of ~/.config/.wrangler/config/default.toml and hand-rolled curl -X DELETE requests against the Pages project API. One of those DELETEs returned 200 {\u0026quot;result\u0026quot;:null,\u0026quot;success\u0026quot;:true} — taken as \u0026ldquo;cache purged\u0026rdquo;, but that was the project deletion succeeding. mirrorhabit.pages.dev resolved to nothing. The investigation Symptom: dig showed no records; wrangler pages project list no longer contained mirrorhabit; the API said Project not found. The dashboard Audit Log was the smoking gun:\na Delete action on the mirrorhabit.pages.dev zone, actor varunpaherwar@proton.me (me), timestamped 17:39 UTC — two minutes after that session\u0026rsquo;s wrangler project list / deployment list calls (confirmed from wrangler\u0026rsquo;s own ~/.config/.wrangler/logs/), ingress from a private IP — consistent with my own network egress. Not an external actor, not a credential leak. A first-party footgun. Recovering the command sequence: wrangler writes a log file per invocation that includes the agent name (agent:\u0026quot;opencode\u0026quot;) and the sanitizedCommand. Matching audit-log + CLI-log timestamps reconstructed exactly what ran that day.\nWhy it happened No dedicated \u0026ldquo;purge edge cache\u0026rdquo; CLI. Wrangler has no Pages cache-purge command, so the fallback was a raw API call with guessed endpoints. Raw DELETE on a project resource is dangerous — some paths under pages/projects/\u0026lt;name\u0026gt; behave like the delete, and DELETE succeeded with a plausible 200. Reading production tokens into a session removes every guardrail — no scoped-token errors, no confirmation prompts, and secrets end up in transcript files. Misreading the response. {\u0026quot;success\u0026quot;:true} was celebrated as a cache clear. On the Pages API, success doesn\u0026rsquo;t always mean what you think it means. Restore + hardening Recreated the project and deployed a fresh build (wrangler pages project create + wrangler pages deploy). mirrorhabit.pages.dev is back. Added public/_redirects (/* /index.html 200) as a SPA fallback so /login, /auth/callback, and friends serve the app instead of 404 — this also fixes the original Google-login redirect landing on /?code=. Corrected the app\u0026rsquo;s default URL to mirrorhabit.pages.dev everywhere (main/plugin/settings/auth form + sync patches). Supabase: Site URL + Redirect URL both https://mirrorhabit.pages.dev (with /**) so OAuth callbacks land on the right route. Rules going forward Never cat the wrangler/cloudflare config. It is the master key. If an agent needs to purge cache, use the dashboard or a purpose-built, scoped API token. Don\u0026rsquo;t hand-guess destructive endpoints. DELETE on anything under pages/projects/\u0026lt;name\u0026gt; gets a second look before sending — or better, use wrangler pages project delete (which at least says what it will do) and nothing else. Check the Audit Log early. Three minutes of dashboard reading beat a day of guessing — it has actor, IP, resource, and exact timestamp. Wrangler saves forensic gold in ~/.config/.wrangler/logs/: agent, command, error, and timestamps per invocation. Lesson: the most dangerous action in the toolchain is a happy DELETE. Three days offline for a \u0026ldquo;quick cache bust.\u0026rdquo;\n","permalink":"https://varunsdevlog.pages.dev/the-day-a-cache-purge-took-mirror-habit-offline/","summary":"\u003cp\u003e\u003cstrong\u003emirror-habit\u003c/strong\u003e went offline for three days because of one mistake: a \u0026ldquo;clear the cache\u0026rdquo; request that actually \u003cstrong\u003edeleted the Pages project\u003c/strong\u003e. Post-mortem, because the exact sequence is worth remembering.\u003c/p\u003e\n\u003ch2 id=\"timeline\"\u003eTimeline\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eSep 13\u003c/strong\u003e — web app had a \u003cstrong\u003enetlify.app\u003c/strong\u003e URL hardcoded in a few places while the real host was \u003cstrong\u003emirrorhabit.pages.dev\u003c/strong\u003e. Google login bounced off \u003ccode\u003e/?code=\u003c/code\u003e instead of \u003ccode\u003e/auth/callback\u003c/code\u003e, and the site seemed stale → asked an agent to \u0026ldquo;bust cache.\u0026rdquo;\u003c/li\u003e\n\u003cli\u003ePages has no one-click edge-cache purge API for a project, so the agent \u003cstrong\u003eread the OAuth token out of \u003ccode\u003e~/.config/.wrangler/config/default.toml\u003c/code\u003e\u003c/strong\u003e and hand-rolled \u003ccode\u003ecurl -X DELETE\u003c/code\u003e requests against the Pages project API.\u003c/li\u003e\n\u003cli\u003eOne of those DELETEs returned \u003ccode\u003e200 {\u0026quot;result\u0026quot;:null,\u0026quot;success\u0026quot;:true}\u003c/code\u003e — taken as \u0026ldquo;cache purged\u0026rdquo;, but that was the \u003cstrong\u003eproject deletion succeeding\u003c/strong\u003e. \u003ccode\u003emirrorhabit.pages.dev\u003c/code\u003e resolved to nothing.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"the-investigation\"\u003eThe investigation\u003c/h2\u003e\n\u003cp\u003eSymptom: \u003ccode\u003edig\u003c/code\u003e showed no records; \u003ccode\u003ewrangler pages project list\u003c/code\u003e no longer contained \u003ccode\u003emirrorhabit\u003c/code\u003e; the API said \u003ccode\u003eProject not found\u003c/code\u003e. The dashboard \u003cstrong\u003eAudit Log\u003c/strong\u003e was the smoking gun:\u003c/p\u003e","title":"The day a 'cache purge' took mirror-habit offline"},{"content":"Shipped per-day reflections for mirror-habit: a note, effort, and mood attached to each habit\u0026rsquo;s daily cell, plus effort trend charts.\nWhat shipped Reflection editor (CheckInNoteEditor) on every check-in surface — dashboards, habit detail, private habits, and the daily briefing. A note/effort/mood is attached to the (habit_id, user_id, date) cell, not individual entries rows — so counted habits (many rows/day) get exactly one reflection per day. Effort trend charts on habit detail pages, and reflections surfaced in the yearly heatmap. supabase/migrations/20260913_entry_notes.sql: PK-enforced table, RLS (read-all / write-your-own), indexes, and realtime replication alongside entries. DB design notes effort/mood are CHECK-constrained to 1..5 so the bar charts never get nonsense. PK (habit_id, user_id, date) is what keeps \u0026ldquo;one reflection per cell\u0026rdquo; true even when a counted habit logs several entries in a day. Reflections are in the realtime publication like entries, so guild-mate notes stream across devices without a reload. What got tricky Store churn: every writer path (fetch, migration push, realtime channel) had to validate reflections the same way or stale 0% rows leak through the reducer — the same class of bug the challenge tiers had. Fix: validate at every ingest point. Moving the reflection data through store.tsx meant keeping streak math and counts untouched — the store diff is mostly additive. A good \u0026ldquo;small feature, careful data model\u0026rdquo; push.\n","permalink":"https://varunsdevlog.pages.dev/per-day-habit-reflections/","summary":"\u003cp\u003eShipped per-day \u003cstrong\u003ereflections\u003c/strong\u003e for mirror-habit: a note, effort, and mood attached to each habit\u0026rsquo;s daily cell, plus effort trend charts.\u003c/p\u003e\n\u003ch2 id=\"what-shipped\"\u003eWhat shipped\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eReflection editor\u003c/strong\u003e (\u003ccode\u003eCheckInNoteEditor\u003c/code\u003e) on every check-in surface — dashboards, habit detail, private habits, and the daily briefing.\u003c/li\u003e\n\u003cli\u003eA note/effort/mood is attached to the \u003cstrong\u003e\u003ccode\u003e(habit_id, user_id, date)\u003c/code\u003e cell\u003c/strong\u003e, not individual entries rows — so counted habits (many rows/day) get exactly one reflection per day.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eEffort trend charts\u003c/strong\u003e on habit detail pages, and reflections surfaced in the yearly heatmap.\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003esupabase/migrations/20260913_entry_notes.sql\u003c/code\u003e: PK-enforced table, RLS (read-all / write-your-own), indexes, and realtime replication alongside entries.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"db-design-notes\"\u003eDB design notes\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003eeffort\u003c/code\u003e/\u003ccode\u003emood\u003c/code\u003e are \u003ccode\u003eCHECK\u003c/code\u003e-constrained to \u003ccode\u003e1..5\u003c/code\u003e so the bar charts never get nonsense.\u003c/li\u003e\n\u003cli\u003ePK \u003ccode\u003e(habit_id, user_id, date)\u003c/code\u003e is what keeps \u0026ldquo;one reflection per cell\u0026rdquo; true even when a counted habit logs several entries in a day.\u003c/li\u003e\n\u003cli\u003eReflections are in the realtime publication like entries, so guild-mate notes stream across devices without a reload.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"what-got-tricky\"\u003eWhat got tricky\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eStore churn\u003c/strong\u003e: every writer path (fetch, migration push, realtime channel) had to validate reflections the same way or stale \u003ccode\u003e0%\u003c/code\u003e rows leak through the reducer — the same class of bug the challenge tiers had. Fix: validate at every ingest point.\u003c/li\u003e\n\u003cli\u003eMoving the reflection data through \u003ccode\u003estore.tsx\u003c/code\u003e meant keeping streak math and counts untouched — the store diff is mostly additive.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eA good \u0026ldquo;small feature, careful data model\u0026rdquo; push.\u003c/p\u003e","title":"Per-day habit reflections"},{"content":"Everyone loves a Wrapped — a few flattering stats, one mildly embarrassing stat, one personality label. Raw numbers don\u0026rsquo;t go viral; stories do. So I built terminal-wrapped: a bash script that turns ~/.zsh_history into your year in commands.\nWhat it does One script, two outputs:\nPretty report — hero stats (total commands, unique, favorite, sudo %, midnight sessions), a derived \u0026ldquo;personality label\u0026rdquo;, top-10 commands with bars, a monthly activity heatmap, one embarrassing stat, and fun facts. JSON mode (--json) — structured stats that any future \u0026ldquo;wrapped engine\u0026rdquo; can consume to render a shareable card. The tech bit Speed matters when the history is 7k+ lines. Calling date per line is the naive approach — too slow. Instead:\nawk pulls timestamp\\tcommand pairs out of the history in a single pass perl converts timestamps to date fields in one batch (no per-line child processes) awk again computes all stats — top commands with a numeric (not string) sort, sudo %, midnight count, per-month buckets, longest command Everything else (personality detection, the bar charts) is plain bash.\nThe recipe I\u0026rsquo;ll reuse This follows a pattern I\u0026rsquo;m deliberately collecting across projects:\nraw stats → personality label → shareable card → embarrassment bait\nSame pipeline, different data source: Obsidian notes, browser history, Jellyfin watches, stashed.in saves (\u0026ldquo;You saved 412 links. You read 17.\u0026rdquo;). The JSON stage is the reusable core — one card template, swap the parser.\nTry it git clone https://github.com/ooexiaoo/terminal-wrapped cd terminal-wrapped \u0026amp;\u0026amp; ./terminal-wrapped.sh # this year WRAPPED_YEAR=2025 ./terminal-wrapped.sh # a specific year ./terminal-wrapped.sh --json # structured output My own 2026: 4,159 commands, favorite cd (1,015×), 379 midnight sessions — personality: The One-Trick Penguin. 🐧\nRepo: github.com/ooexiaoo/terminal-wrapped\n","permalink":"https://varunsdevlog.pages.dev/terminal-wrapped-your-year-in-commands/","summary":"\u003cp\u003eEveryone loves a Wrapped — a few flattering stats, one mildly embarrassing stat, one personality label. Raw numbers don\u0026rsquo;t go viral; \u003cem\u003estories\u003c/em\u003e do. So I built \u003cstrong\u003eterminal-wrapped\u003c/strong\u003e: a bash script that turns \u003ccode\u003e~/.zsh_history\u003c/code\u003e into your year in commands.\u003c/p\u003e\n\u003ch2 id=\"what-it-does\"\u003eWhat it does\u003c/h2\u003e\n\u003cp\u003eOne script, two outputs:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003ePretty report\u003c/strong\u003e — hero stats (total commands, unique, favorite, sudo %, midnight sessions), a derived \u0026ldquo;personality label\u0026rdquo;, top-10 commands with bars, a monthly activity heatmap, one embarrassing stat, and fun facts.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eJSON mode\u003c/strong\u003e (\u003ccode\u003e--json\u003c/code\u003e) — structured stats that any future \u0026ldquo;wrapped engine\u0026rdquo; can consume to render a shareable card.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"the-tech-bit\"\u003eThe tech bit\u003c/h2\u003e\n\u003cp\u003eSpeed matters when the history is 7k+ lines. Calling \u003ccode\u003edate\u003c/code\u003e per line is the naive approach — too slow. Instead:\u003c/p\u003e","title":"terminal-wrapped — your year in commands"},{"content":"Two things in one session: a slimmer APK and a content + polish push.\nPerf: shrinking the bundle The app was carrying a lot of dead weight. Moved the bulky apng sources out of the bundle and archived unused assets, which trimmed a meaningful chunk off the install size — important for a habit app people always have installed.\nPolish: more decorations Wired up level 12/13 decorations so the earnable avatar ornaments scale into higher tiers. Added levels 2–4 decorations plus effect lines in the prompt pack, rounding out the progression ladder. Content Ran a full research-backed habits post with diagrams up on the blog — the kind of \u0026ldquo;why streaks and community actually work\u0026rdquo; explainer that gives the app some reason to exist beyond the toggles.\n","permalink":"https://varunsdevlog.pages.dev/slimming-the-mirror-habit-apk/","summary":"\u003cp\u003eTwo things in one session: a \u003cstrong\u003eslimmer APK\u003c/strong\u003e and a \u003cstrong\u003econtent + polish push\u003c/strong\u003e.\u003c/p\u003e\n\u003ch2 id=\"perf-shrinking-the-bundle\"\u003ePerf: shrinking the bundle\u003c/h2\u003e\n\u003cp\u003eThe app was carrying a lot of dead weight. Moved the bulky \u003cstrong\u003eapng sources\u003c/strong\u003e out of the bundle and \u003cstrong\u003earchived unused assets\u003c/strong\u003e, which trimmed a meaningful chunk off the install size — important for a habit app people always have installed.\u003c/p\u003e\n\u003ch2 id=\"polish-more-decorations\"\u003ePolish: more decorations\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eWired up \u003cstrong\u003elevel 12/13 decorations\u003c/strong\u003e so the earnable avatar ornaments scale into higher tiers.\u003c/li\u003e\n\u003cli\u003eAdded \u003cstrong\u003elevels 2–4 decorations\u003c/strong\u003e plus effect lines in the prompt pack, rounding out the progression ladder.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"content\"\u003eContent\u003c/h2\u003e\n\u003cp\u003eRan a full \u003cstrong\u003eresearch-backed habits post\u003c/strong\u003e with diagrams up on the blog — the kind of \u0026ldquo;why streaks and community actually work\u0026rdquo; explainer that gives the app some reason to exist beyond the toggles.\u003c/p\u003e","title":"slimming the mirror-habit APK"},{"content":"The biggest feature drop for mirror-habit so far — a full rewards hub built around levelling up and collecting.\nWhat shipped Levels with rank graphs — progression tiers backed by a whole new set of rank badges, each with bespoke art (swift, sturdy, fearless, valiant, unyielding, godslayer…). Achievements, titles, badges \u0026amp; banners — a collectible system that gives every check-in a reason, from \u0026ldquo;first check-in\u0026rdquo; to \u0026ldquo;100 check-ins\u0026rdquo; and \u0026ldquo;10 habits\u0026rdquo;. Avatar decorations — visual adornments you earn and can attach to your avatar (dark fantasy, shadow portal, first step, and per-level variants). A dedicated Rewards view to browse everything you\u0026rsquo;ve unlocked and what\u0026rsquo;s still ahead. The rank artwork got an overhaul too — reworked and renamed each tier\u0026rsquo;s image so they read clearly as a progression ladder instead of vague names.\nIt\u0026rsquo;s the core \u0026ldquo;guild\u0026rdquo; hook paying off: progress isn\u0026rsquo;t just a number, it\u0026rsquo;s something you can show off.\n","permalink":"https://varunsdevlog.pages.dev/mirror-habit-gets-a-rewards-hub/","summary":"\u003cp\u003eThe biggest feature drop for \u003cstrong\u003emirror-habit\u003c/strong\u003e so far — a full \u003cstrong\u003erewards hub\u003c/strong\u003e built around levelling up and collecting.\u003c/p\u003e\n\u003ch2 id=\"what-shipped\"\u003eWhat shipped\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eLevels with rank graphs\u003c/strong\u003e — progression tiers backed by a whole new set of rank badges, each with bespoke art (swift, sturdy, fearless, valiant, unyielding, godslayer…).\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAchievements, titles, badges \u0026amp; banners\u003c/strong\u003e — a collectible system that gives every check-in a reason, from \u0026ldquo;first check-in\u0026rdquo; to \u0026ldquo;100 check-ins\u0026rdquo; and \u0026ldquo;10 habits\u0026rdquo;.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAvatar decorations\u003c/strong\u003e — visual adornments you earn and can attach to your avatar (dark fantasy, shadow portal, first step, and per-level variants).\u003c/li\u003e\n\u003cli\u003eA dedicated \u003cstrong\u003eRewards view\u003c/strong\u003e to browse everything you\u0026rsquo;ve unlocked and what\u0026rsquo;s still ahead.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe rank artwork got an overhaul too — reworked and renamed each tier\u0026rsquo;s image so they read clearly as a progression ladder instead of vague names.\u003c/p\u003e","title":"mirror-habit gets a rewards hub"},{"content":"Small but satisfying: the stashed.in 404 page finally matches the rest of the app.\nWhat changed Restyled to match branding — same look and feel as the rest of the site so a dead link doesn\u0026rsquo;t feel like a dead end. Optimized the image down to a 256px webp, so even the error page loads fast. A tiny page, but it\u0026rsquo;s the difference between \u0026ldquo;ugh, a 404\u0026rdquo; and a brand-consistent dead end that doesn\u0026rsquo;t wreck the load budget.\n","permalink":"https://varunsdevlog.pages.dev/stashed.in-404-page-gets-a-restyle/","summary":"\u003cp\u003eSmall but satisfying: the \u003cstrong\u003estashed.in 404 page\u003c/strong\u003e finally matches the rest of the app.\u003c/p\u003e\n\u003ch2 id=\"what-changed\"\u003eWhat changed\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eRestyled to match branding\u003c/strong\u003e — same look and feel as the rest of the site so a dead link doesn\u0026rsquo;t feel like a dead end.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eOptimized the image\u003c/strong\u003e down to a \u003ccode\u003e256px\u003c/code\u003e \u003cstrong\u003ewebp\u003c/strong\u003e, so even the error page loads fast.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eA tiny page, but it\u0026rsquo;s the difference between \u0026ldquo;ugh, a 404\u0026rdquo; and a brand-consistent dead end that doesn\u0026rsquo;t wreck the load budget.\u003c/p\u003e","title":"stashed.in 404 page gets a restyle"},{"content":"Refreshed the portfolio site with a layout that earns its keep.\nWhat changed Featured projects as a bento grid — a more visual, varied layout than the old uniform list, so each project gets a presence roughly matching its scope. Project icons — every project now has a recognizable mark instead of just text. Mobile glass nav — a frosted look that reads well and stays out of the way on small screens. Curated broadcasts — a hand-picked section of highlights rather than everything blindly. A design pass that made the front page feel more like a curated showcase and less like a resume dump.\n","permalink":"https://varunsdevlog.pages.dev/portfolio-redesign-bento-grid/","summary":"\u003cp\u003eRefreshed the \u003cstrong\u003eportfolio site\u003c/strong\u003e with a layout that earns its keep.\u003c/p\u003e\n\u003ch2 id=\"what-changed\"\u003eWhat changed\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eFeatured projects as a bento grid\u003c/strong\u003e — a more visual, varied layout than the old uniform list, so each project gets a presence roughly matching its scope.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eProject icons\u003c/strong\u003e — every project now has a recognizable mark instead of just text.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMobile glass nav\u003c/strong\u003e — a frosted look that reads well and stays out of the way on small screens.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCurated broadcasts\u003c/strong\u003e — a hand-picked section of highlights rather than everything blindly.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eA design pass that made the front page feel more like a curated showcase and less like a resume dump.\u003c/p\u003e","title":"portfolio redesign: bento grid"},{"content":"Lost a full day of dev time to a botched update that ended in a config reset. Logging the post-mortem so the next one hurts less.\nWhat happened A routine system update (Fedora KDE Plasma) went sideways. On next boot, some desktop/session config didn\u0026rsquo;t come back the way it should — the shell came up in a locked/odd state and a chunk of my Plasma setup wasn\u0026rsquo;t behaving.\nRather than burn more hours untangling whatever the update scrambled, I made the call to reset the affected config and rebuild it from my dotfiles. The reset itself was quick; the lost day was the interruption — momentum on whatever I was mid-way through went out the window.\nThe save This is exactly why the dotfiles repo exists. After the reset I could stand the config back up from /home/exia/dotfiles instead of reconfiguring by hand — wallpaper, window decoration, MaterialYou scheme, Krohnkite tiling, panels. It all came back from files, not from memory.\nWhat I\u0026rsquo;d do differently Snapshot before updating. A full config backup before the next big Plasma/system update, not after. Don\u0026rsquo;t panic-reset mid-flow. Give the session a real chance first (clean logout/login, not just a shell restart). Push dotfiles after every change. The repo is now also on GitHub, so even a full disk replacement won\u0026rsquo;t cost the same day again. A day lost, but the tooling that prevents the recurrence is in place now.\n","permalink":"https://varunsdevlog.pages.dev/reset-after-a-botched-update/","summary":"\u003cp\u003eLost a full day of dev time to a \u003cstrong\u003ebotched update\u003c/strong\u003e that ended in a config reset. Logging the post-mortem so the next one hurts less.\u003c/p\u003e\n\u003ch2 id=\"what-happened\"\u003eWhat happened\u003c/h2\u003e\n\u003cp\u003eA routine system update (Fedora KDE Plasma) went sideways. On next boot, some desktop/session config didn\u0026rsquo;t come back the way it should — the shell came up in a locked/odd state and a chunk of my Plasma setup wasn\u0026rsquo;t behaving.\u003c/p\u003e\n\u003cp\u003eRather than burn more hours untangling whatever the update scrambled, I made the call to \u003cstrong\u003ereset the affected config and rebuild it from my dotfiles\u003c/strong\u003e. The reset itself was quick; the lost day was the interruption — momentum on whatever I was mid-way through went out the window.\u003c/p\u003e","title":"reset after a botched update"},{"content":"Every good project diary needs an origin entry — this is it.\nTonight I spun up devlog.stashed.in: a tiny Hugo site running the bearblog theme to keep build notes across all my projects instead of losing them in chat scrollback.\nSetup took minutes:\nhugo new site equivalent — folder + config + theme clone (hand-rolled layouts lasted all of five minutes before I wised up) Backfilled past logs from stashlist/stashed.in, mirror-habit, and the Mehfil wedding-invitation sprint Git repo → GitHub → Cloudflare Pages building Hugo straight from the repo Custom domain pointed at devlog.stashed.in The dogfooding begins here. If you\u0026rsquo;re reading this, the pipeline works end-to-end.\n","permalink":"https://varunsdevlog.pages.dev/a-dev-log-for-the-dev-logs/","summary":"\u003cp\u003eEvery good project diary needs an origin entry — this is it.\u003c/p\u003e\n\u003cp\u003eTonight I spun up \u003cstrong\u003edevlog.stashed.in\u003c/strong\u003e: a tiny Hugo site running the \u003ca href=\"https://github.com/janraasch/hugo-bearblog\"\u003ebearblog\u003c/a\u003e theme to keep build notes across all my projects instead of losing them in chat scrollback.\u003c/p\u003e\n\u003cp\u003eSetup took minutes:\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003ccode\u003ehugo new site\u003c/code\u003e equivalent — folder + config + theme clone (hand-rolled layouts lasted all of five minutes before I wised up)\u003c/li\u003e\n\u003cli\u003eBackfilled past logs from stashlist/stashed.in, mirror-habit, and the Mehfil wedding-invitation sprint\u003c/li\u003e\n\u003cli\u003eGit repo → GitHub → \u003cstrong\u003eCloudflare Pages\u003c/strong\u003e building Hugo straight from the repo\u003c/li\u003e\n\u003cli\u003eCustom domain pointed at \u003ccode\u003edevlog.stashed.in\u003c/code\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThe dogfooding begins here. If you\u0026rsquo;re reading this, the pipeline works end-to-end.\u003c/p\u003e","title":"A dev log for the dev logs"},{"content":"I\u0026rsquo;m Varun — a developer who likes making beautiful things that work well and smoothly. Most of what I build is open-source in mind; some of it is an attempt at making money.\nWhat I\u0026rsquo;m into Web development, UI/UX, open source, CLI tools, and performance. I care about the small interactions as much as the architecture.\nCurrent projects stashed.in — stash links into boards with automatic previews; my longest-running bet on \u0026ldquo;build something people use.\u0026rdquo; mirror-habit — guild-based habit tracking where streaks mirror against friends. React + Supabase + Capacitor. mehfil — wedding invitation platform that started as one invite for Arjun \u0026amp; Diya and got ambitious. This site is the running diary for all of it — what shipped, what broke, and what I learned fixing it.\n","permalink":"https://varunsdevlog.pages.dev/about/","summary":"\u003cp\u003eI\u0026rsquo;m \u003cstrong\u003eVarun\u003c/strong\u003e — a developer who likes making beautiful things that work well and smoothly. Most of what I build is open-source in mind; some of it is an attempt at making money.\u003c/p\u003e\n\u003ch2 id=\"what-im-into\"\u003eWhat I\u0026rsquo;m into\u003c/h2\u003e\n\u003cp\u003eWeb development, UI/UX, open source, CLI tools, and performance. I care about the small interactions as much as the architecture.\u003c/p\u003e\n\u003ch2 id=\"current-projects\"\u003eCurrent projects\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e\u003ca href=\"https://stashed.in\"\u003estashed.in\u003c/a\u003e\u003c/strong\u003e — stash links into boards with automatic previews; my longest-running bet on \u0026ldquo;build something people use.\u0026rdquo;\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003emirror-habit\u003c/strong\u003e — guild-based habit tracking where streaks mirror against friends. React + Supabase + Capacitor.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003emehfil\u003c/strong\u003e — wedding invitation platform that started as one invite for Arjun \u0026amp; Diya and got ambitious.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThis site is the running diary for all of it — what shipped, what broke, and what I learned fixing it.\u003c/p\u003e","title":"About"},{"content":"Big push today on mirror-habit — the challenge system is now complete end-to-end.\nWhat shipped Tier engine (src/lib/challengeResults.ts): at expiry, each participant gets a completion tier — conqueror ≥100%, gold ≥75%, silver ≥50%, bronze ≥25%, participant \u0026gt;0% — with XP bonuses and 👑 MVP crowns for top finishers. All date math rewritten as pure UTC string ops after the container\u0026rsquo;s Asia/Calcutta timezone kept shifting weekly boundaries by a day. Dropped date-fns from the engine entirely. Custom banner artwork: five illustrated WebP banners (1200×298) + crown overlay, ~122KB total after ImageMagick conversion (originals were 9MB PNGs). Wired into profile grid, habit detail, and popups. Click-to-enlarge lightbox on profile banners, matching the existing titles popup pattern. Archive mode on challenge detail pages: once a challenge ends it becomes the first card on the page, check-ins are disabled, heatmap tiles stop being clickable, and the competitive \u0026ldquo;You\u0026rsquo;re 100% ahead of the guild average\u0026rdquo; insight is replaced with a neutral \u0026ldquo;challenge has ended\u0026rdquo; note. Celebration popup on the dashboard when you earn a new banner (once per banner via localStorage seen-set). Past Challenges rows now show tier emoji stacks + your personal result. Bugs hit along the way A JSX restructure left an unclosed \u0026lt;div\u0026gt; that cascaded into main has no corresponding closing tag — tsc caught what my eyes missed. Junk 0% rows from mid-dev localStorage churn resurrected themselves through LOAD_STATE; fixed by validating tiers at every ingest point (fetch, migration push, realtime channel, reducer). The user\u0026rsquo;s July hike check-in was lost to pre-engine localStorage churn — data loss, not a logic bug. Backfill SQL provided. Next: guild-wide leaderboard maybe, or polish on the voting flow.\n","permalink":"https://varunsdevlog.pages.dev/challenge-banners-tiers-archive-mode/","summary":"\u003cp\u003eBig push today on \u003cstrong\u003emirror-habit\u003c/strong\u003e — the challenge system is now complete end-to-end.\u003c/p\u003e\n\u003ch2 id=\"what-shipped\"\u003eWhat shipped\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eTier engine\u003c/strong\u003e (\u003ccode\u003esrc/lib/challengeResults.ts\u003c/code\u003e): at expiry, each participant gets a completion tier — conqueror ≥100%, gold ≥75%, silver ≥50%, bronze ≥25%, participant \u0026gt;0% — with XP bonuses and 👑 MVP crowns for top finishers. All date math rewritten as pure UTC string ops after the container\u0026rsquo;s \u003ccode\u003eAsia/Calcutta\u003c/code\u003e timezone kept shifting weekly boundaries by a day. Dropped date-fns from the engine entirely.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCustom banner artwork\u003c/strong\u003e: five illustrated WebP banners (1200×298) + crown overlay, ~122KB total after ImageMagick conversion (originals were 9MB PNGs). Wired into profile grid, habit detail, and popups.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eClick-to-enlarge lightbox\u003c/strong\u003e on profile banners, matching the existing titles popup pattern.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eArchive mode on challenge detail pages\u003c/strong\u003e: once a challenge ends it becomes the first card on the page, check-ins are disabled, heatmap tiles stop being clickable, and the competitive \u0026ldquo;You\u0026rsquo;re 100% ahead of the guild average\u0026rdquo; insight is replaced with a neutral \u0026ldquo;challenge has ended\u0026rdquo; note.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCelebration popup\u003c/strong\u003e on the dashboard when you earn a new banner (once per banner via localStorage seen-set).\u003c/li\u003e\n\u003cli\u003ePast Challenges rows now show tier emoji stacks + your personal result.\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"bugs-hit-along-the-way\"\u003eBugs hit along the way\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eA JSX restructure left an unclosed \u003ccode\u003e\u0026lt;div\u0026gt;\u003c/code\u003e that cascaded into \u003ccode\u003emain has no corresponding closing tag\u003c/code\u003e — tsc caught what my eyes missed.\u003c/li\u003e\n\u003cli\u003eJunk \u003ccode\u003e0%\u003c/code\u003e rows from mid-dev localStorage churn resurrected themselves through \u003ccode\u003eLOAD_STATE\u003c/code\u003e; fixed by validating tiers at every ingest point (fetch, migration push, realtime channel, reducer).\u003c/li\u003e\n\u003cli\u003eThe user\u0026rsquo;s July hike check-in was lost to pre-engine localStorage churn — data loss, not a logic bug. Backfill SQL provided.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eNext: guild-wide leaderboard maybe, or polish on the voting flow.\u003c/p\u003e","title":"Challenge banners, tiers \u0026 archive mode"},{"content":"Swapped the bearblog theme for PaperMod on the same day the site launched. Bearblog was fine for getting the first commit out fast, but it was too minimal — no built-in search, no dark mode, no real homepage layout options.\nWhat changed Theme: hugo-bearblog → PaperMod — gained dark/light auto-switch, JSON search, and a cleaner post list. Homepage: switched to homeInfoParams with a short blurb and social icons instead of the bearblog bare index. About page: added content/about.md since PaperMod\u0026rsquo;s profile mode gives it a proper layout. Config: rewrote hugo.toml for PaperMod params — Dracula highlight style, auto theme, reading/share buttons off. Removed the hand-rolled baseof.html that bearblog needed; PaperMod handles its own layouts. Deployment stayed the same — Cloudflare Pages just rebuilt from the new commit and everything worked. The config diff was the biggest part; PaperMod has a lot more knobs than bearblog did.\n","permalink":"https://varunsdevlog.pages.dev/swapped-bearblog-for-papermod/","summary":"\u003cp\u003eSwapped the bearblog theme for \u003ca href=\"https://github.com/adityatelange/hugo-PaperMod\"\u003ePaperMod\u003c/a\u003e on the same day the site launched. Bearblog was fine for getting the first commit out fast, but it was too minimal — no built-in search, no dark mode, no real homepage layout options.\u003c/p\u003e\n\u003ch2 id=\"what-changed\"\u003eWhat changed\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eTheme\u003c/strong\u003e: \u003ccode\u003ehugo-bearblog\u003c/code\u003e → \u003ccode\u003ePaperMod\u003c/code\u003e — gained dark/light auto-switch, JSON search, and a cleaner post list.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eHomepage\u003c/strong\u003e: switched to \u003ccode\u003ehomeInfoParams\u003c/code\u003e with a short blurb and social icons instead of the bearblog bare index.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAbout page\u003c/strong\u003e: added \u003ccode\u003econtent/about.md\u003c/code\u003e since PaperMod\u0026rsquo;s profile mode gives it a proper layout.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eConfig\u003c/strong\u003e: rewrote \u003ccode\u003ehugo.toml\u003c/code\u003e for PaperMod params — Dracula highlight style, auto theme, reading/share buttons off.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRemoved\u003c/strong\u003e the hand-rolled \u003ccode\u003ebaseof.html\u003c/code\u003e that bearblog needed; PaperMod handles its own layouts.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eDeployment stayed the same — Cloudflare Pages just rebuilt from the new commit and everything worked. The config diff was the biggest part; PaperMod has a lot more knobs than bearblog did.\u003c/p\u003e","title":"Swapped bearblog for PaperMod"},{"content":"One late-night session that went from \u0026ldquo;a wedding invitation page\u0026rdquo; to \u0026ldquo;an invitation platform.\u0026rdquo;\nStarted with a single Arjun ♥ Diya invitation — React + Vite + Tailwind v4 + GSAP scroll animations. Set live OG meta tags so shared links preview properly on WhatsApp, added a deploy script and TODO tracker. Then the scope creep hit: naming research, platform plan, locked the name — Mehfil (Hindi for gathering/celebration venue). Closed the night by swapping the venue image to an elegant ballroom shot from Pexels. From single invite to product idea in ~14 hours. Classic.\n","permalink":"https://varunsdevlog.pages.dev/mehfil-wedding-invitations-in-one-night/","summary":"\u003cp\u003eOne late-night session that went from \u0026ldquo;a wedding invitation page\u0026rdquo; to \u0026ldquo;an invitation platform.\u0026rdquo;\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eStarted with a single \u003cstrong\u003eArjun ♥ Diya invitation\u003c/strong\u003e — React + Vite + Tailwind v4 + GSAP scroll animations.\u003c/li\u003e\n\u003cli\u003eSet live OG meta tags so shared links preview properly on WhatsApp, added a deploy script and TODO tracker.\u003c/li\u003e\n\u003cli\u003eThen the scope creep hit: naming research, platform plan, locked the name — \u003cstrong\u003eMehfil\u003c/strong\u003e (Hindi for gathering/celebration venue).\u003c/li\u003e\n\u003cli\u003eClosed the night by swapping the venue image to an elegant ballroom shot from Pexels.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eFrom single invite to product idea in ~14 hours. Classic.\u003c/p\u003e","title":"Mehfil — wedding invitations in one night"},{"content":"Started a new project: mirror-habit — habits tracked in guilds, where everyone\u0026rsquo;s progress mirrors against everyone else. Challenges, streaks, gentle public pressure.\nStack from day one: React + Supabase + Capacitor for Android. APK builds run entirely through GitHub Actions (no Android Studio) — took two commits to get the CI Java version right for AGP 8.13.\nA month and change later: challenge tiers, banner artwork, archive mode — see the next log.\n","permalink":"https://varunsdevlog.pages.dev/mirror-habit-begins/","summary":"\u003cp\u003eStarted a new project: \u003cstrong\u003emirror-habit\u003c/strong\u003e — habits tracked in guilds, where everyone\u0026rsquo;s progress mirrors against everyone else. Challenges, streaks, gentle public pressure.\u003c/p\u003e\n\u003cp\u003eStack from day one: React + Supabase + Capacitor for Android. APK builds run entirely through GitHub Actions (no Android Studio) — took two commits to get the CI Java version right for AGP 8.13.\u003c/p\u003e\n\u003cp\u003eA month and change later: challenge tiers, banner artwork, archive mode — see the next log.\u003c/p\u003e","title":"mirror-habit begins"},{"content":"A look back at stashed.in (the stashlist project) — 181 sessions from Dec 2025 to July 2026.\nHighlights along the way Boards \u0026amp; links core: masonry view with website previews like raindrop.io. Getting reliable banner/link images meant three fallback strategies for metadata fetching, plus Supabase edge caching for OpenGraph data. Browser extension so users can stash links without opening the site — including matching favicons. PWA support, Disqus comment counts on link buttons, tag autocomplete. QR-code sharing for password-protected stashes; import your YouTube subscriptions straight into a board. Endless polish: theme switcher sizing, dialog gradients, Cloudflare cache purging after deploys (\u0026ldquo;why do users still see the old build?\u0026rdquo;). It\u0026rsquo;s live at stashed.in — still my best shot at \u0026ldquo;make something people use.\u0026rdquo;\n","permalink":"https://varunsdevlog.pages.dev/stashed.in-seven-months-of-stashing/","summary":"\u003cp\u003eA look back at \u003cstrong\u003estashed.in\u003c/strong\u003e (the stashlist project) — 181 sessions from Dec 2025 to July 2026.\u003c/p\u003e\n\u003ch2 id=\"highlights-along-the-way\"\u003eHighlights along the way\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eBoards \u0026amp; links core\u003c/strong\u003e: masonry view with website previews like raindrop.io. Getting reliable banner/link images meant three fallback strategies for metadata fetching, plus Supabase edge caching for OpenGraph data.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eBrowser extension\u003c/strong\u003e so users can stash links without opening the site — including matching favicons.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003ePWA support\u003c/strong\u003e, Disqus comment counts on link buttons, tag autocomplete.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eQR-code sharing\u003c/strong\u003e for password-protected stashes; import your YouTube subscriptions straight into a board.\u003c/li\u003e\n\u003cli\u003eEndless polish: theme switcher sizing, dialog gradients, Cloudflare cache purging after deploys (\u0026ldquo;why do users still see the old build?\u0026rdquo;).\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eIt\u0026rsquo;s live at \u003ca href=\"https://stashed.in\"\u003estashed.in\u003c/a\u003e — still my best shot at \u0026ldquo;make something people use.\u0026rdquo;\u003c/p\u003e","title":"stashed.in — seven months of stashing"}]