<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Aman</title>
    <description>The latest articles on DEV Community by Aman (@amandevelops).</description>
    <link>https://dev.to/amandevelops</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3235079%2Ff5ee783c-893e-4d90-afab-6822746100d5.png</url>
      <title>DEV Community: Aman</title>
      <link>https://dev.to/amandevelops</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/amandevelops"/>
    <language>en</language>
    <item>
      <title>I built a chore daemon that refuses to work while I'm using my laptop</title>
      <dc:creator>Aman</dc:creator>
      <pubDate>Wed, 21 Oct 2026 16:42:46 +0000</pubDate>
      <link>https://dev.to/amandevelops/i-built-a-chore-daemon-that-refuses-to-work-while-im-using-my-laptop-3j72</link>
      <guid>https://dev.to/amandevelops/i-built-a-chore-daemon-that-refuses-to-work-while-im-using-my-laptop-3j72</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19"&gt;Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;Every AI tool I have tried this year wants my attention at the exact moment it decides it has something to say. So I built the opposite: a worker that does its job only when nobody is looking, and puts the file down the instant I touch the keyboard.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;slothd&lt;/code&gt; is a chore daemon. You point it at the folder you keep meaning to deal with — mine is a downloads folder full of files called &lt;code&gt;Untitled document (3).txt&lt;/code&gt; — and it reads each file with a local open-weight model, decides what the thing actually is, renames it, and files it under &lt;code&gt;sorted/&amp;lt;category&amp;gt;/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The interesting part is not the filing. It is the pace. It only works when the machine is genuinely idle, and the moment you start using the computer it &lt;strong&gt;drops the file it is in the middle of&lt;/strong&gt; and gets out of the way. You never wait for it, you never watch it, and you never pay for it. You walk away, and the mess quietly shrinks.&lt;/p&gt;

&lt;p&gt;In the morning you get two lines, not a dashboard:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Filed 16 things while you were away (4 code, 4 money, 4 personal). That is ~5.3 minutes you did not spend.&lt;br&gt;
Only 4 want a look from you: money files. Everything else can wait.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the whole interface. The web UI exists mostly so you can confirm it is not lying to you.&lt;/p&gt;

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

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

&lt;p&gt;What to watch for, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;it is filing a file, one core busy, status &lt;code&gt;working while you're away&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;I start a build on both cores — within ~2 seconds the status flips to &lt;code&gt;waiting for you to stop typing&lt;/code&gt;, the in-flight file is abandoned mid-inference, and nothing is left half-filed;&lt;/li&gt;
&lt;li&gt;the build finishes, it resumes the same file from the start, and the ledger keeps growing;&lt;/li&gt;
&lt;li&gt;the digest at the end is two lines.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The dashboard's &lt;code&gt;times it gave the machine back&lt;/code&gt; counter is not a mock: it increments every time the model call is killed mid-stream because someone touched the machine.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://github.com/AmanDevelops/slothd" rel="noopener noreferrer"&gt;AmanDevelops/slothd&lt;/a&gt; — standard library only, no dependencies, no API key. Runs on Linux (and WSL) today; macOS needs the gate ported.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/AmanDevelops/slothd.git
&lt;span class="nb"&gt;cd &lt;/span&gt;slothd
python3 scripts/make_fixtures.py                       &lt;span class="c"&gt;# the 16 messy files from this post&lt;/span&gt;
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; slothd.server &lt;span class="nt"&gt;--inbox&lt;/span&gt; fixtures/inbox &lt;span class="nt"&gt;--var&lt;/span&gt; ./var
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;17 stdlib tests (&lt;code&gt;python3 -m unittest discover -s tests&lt;/code&gt;) cover the parsing, the&lt;br&gt;
category clamping, hash dedupe, undo, and the digest. The gate itself is exercised by&lt;br&gt;
watching the dashboard flip to &lt;code&gt;waiting for you to stop typing&lt;/code&gt; the moment you start a build.&lt;/p&gt;
&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The only prompt in the project
&lt;/h3&gt;

&lt;p&gt;One question per file, four fields out, temperature 0, JSON-constrained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are a filing clerk with no sense of urgency. Read the file snippet and reply with
JSON only. Keys: title (max 6 words), category (exactly one of money, work, code,
personal, reference, junk), summary (max 12 words), confidence (0-1).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is it. No agent loop, no tool calling, no planner. The job is narrow enough that a 4B model does it well, and every extra token is time I am spending on someone else's machine.&lt;/p&gt;

&lt;h3&gt;
  
  
  The gate is the hard part
&lt;/h3&gt;

&lt;p&gt;"It runs when you are idle" is easy to write and hard to get right, because &lt;strong&gt;the worker is itself load&lt;/strong&gt;. My first version read &lt;code&gt;/proc/stat&lt;/code&gt;, saw the CPU at 100% (from its own inference), concluded the human was busy, and stopped forever. A worker that watches its own footprint is a worker that deadlocks.&lt;/p&gt;

&lt;p&gt;The fix is to measure the machine &lt;strong&gt;minus itself&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;d_own&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;our_cpu_seconds&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;model_server_cpu_seconds&lt;/span&gt;   &lt;span class="c1"&gt;# from /proc/&amp;lt;pid&amp;gt;/stat
&lt;/span&gt;&lt;span class="n"&gt;d_total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cgroup_cpu_seconds_delta&lt;/span&gt;                     &lt;span class="c1"&gt;# whole box, this session
&lt;/span&gt;&lt;span class="n"&gt;human&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d_total&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;d_own&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;idle&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;human&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;cores&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So when Ollama pins one core for twenty seconds, the gate reads &lt;code&gt;human uses 0.0 cores&lt;/code&gt; and keeps going, and when &lt;em&gt;I&lt;/em&gt; pin both cores it reads &lt;code&gt;human uses 2.0 cores&lt;/code&gt; and stops. The dashboard shows both numbers side by side (&lt;code&gt;your cpu, this window&lt;/code&gt; / &lt;code&gt;its cpu, this window&lt;/code&gt;) so the arithmetic is never a mystery.&lt;/p&gt;

&lt;p&gt;Two things I got wrong and fixed, in case you are building something similar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Load average is the wrong signal.&lt;/strong&gt; It is a one-minute EWMA that counts your own past work, so the worker throttles itself for a minute after every file. I switched to cgroup CPU usage and dropped the load check entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/proc/stat&lt;/code&gt; lies inside containers.&lt;/strong&gt; It reports the host's CPUs, not your slice. On a shared VM the machine can read as 99% busy while your container is idle. cgroup v2 &lt;code&gt;cpu.stat&lt;/code&gt; is per-slice and correct in both places; &lt;code&gt;/proc/stat&lt;/code&gt; is the fallback for older systems.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Preemption, because "idle" changes while you are typing
&lt;/h3&gt;

&lt;p&gt;Sampling the gate between files is not enough: one file takes ~20 seconds of inference, so a build that starts in the middle of a file waits 20 seconds for a courtesy.&lt;/p&gt;

&lt;p&gt;The model call is streamed over a raw &lt;code&gt;http.client&lt;/code&gt; connection, and every chunk is an opportunity to change my mind. A watchdog thread samples the gate once a second while a file is in flight; if the human comes back, it sets a cancel flag, the reader closes the socket, and the generation dies mid-token. The file has not moved — the move happens only after a complete verdict — so nothing is half-filed, and the ledger records the yield instead of the file.&lt;/p&gt;

&lt;p&gt;That is also why the answer to "what happens if I shut the laptop lid?" is boring: files move with &lt;code&gt;os.replace&lt;/code&gt; and then one line is appended to &lt;code&gt;var/ledger.jsonl&lt;/code&gt;. Kill -9 mid-run and the in-flight file is simply still in the inbox, to be retried on the next pass.&lt;/p&gt;

&lt;h3&gt;
  
  
  The unglamorous parts that make it safe to leave running
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dedupe by content hash.&lt;/strong&gt; Restarting the daemon cannot file the same receipt twice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Undo.&lt;/strong&gt; &lt;code&gt;POST /api/undo&lt;/code&gt; walks the last run backwards and puts every file back where it came from. &lt;code&gt;--undo-all&lt;/code&gt; does the ledger.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It cannot wedge.&lt;/strong&gt; Model down, model returns prose, model returns a category I did not ask for — it clamps the category, or falls back to a keyword classifier, and marks the ledger entry &lt;code&gt;source: fallback&lt;/code&gt;. Two failed attempts on one file and it gives up and leaves the file alone, rather than looping forever (which my first version cheerfully did).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It yields rather than shares.&lt;/strong&gt; &lt;code&gt;os.nice(19)&lt;/code&gt;. If it is ever running while you need the machine, it is the least important process on it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;Three things, all of them things a hosted API structurally cannot give me:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It runs on the pile of personal files you would never upload.&lt;/strong&gt; A folder like this one has bank statements, invoices with your address on them, and a to-do list with your mother's birthday in it. A local 4B model means the answer to "who can read this" is "whoever can read my disk", full stop. The process opens exactly two sockets: &lt;code&gt;127.0.0.1:8010&lt;/code&gt; for the UI and &lt;code&gt;127.0.0.1:11434&lt;/code&gt; for the model. The dashboard's &lt;code&gt;bytes uploaded&lt;/code&gt; counter is &lt;code&gt;0 B&lt;/code&gt; because there is no code path that could make it anything else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cost of a chore is zero, which changes what counts as worth doing.&lt;/strong&gt; Filing a file by hand costs me 20 seconds of attention. Filing it with a paid API costs a fraction of a cent plus a key to manage plus a service that can deprecate the model next quarter. When inference is free and local, "automate this boring thing" stops being a budget decision. I left it running for hours because there was nothing to meter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I can swap the brain.&lt;/strong&gt; The model is one string in &lt;code&gt;--model&lt;/code&gt;. &lt;code&gt;gemma3:4b&lt;/code&gt; gets the subtle ones right; &lt;code&gt;gemma3:1b&lt;/code&gt; filed the same 16 files in 162 seconds against the 4B's 307 — &lt;strong&gt;1.9x faster&lt;/strong&gt; on this 2-core box — and disagreed on the five vaguest ones, nearly all of them by falling back to &lt;code&gt;reference&lt;/code&gt;. On a slow machine that is the difference between "overnight" and "over lunch", and it is my call to make, per machine, per afternoon. A closed endpoint lets me pick from whatever tier the vendor keeps in stock; an open-weight model lets me pick quantisation, size, and hardware — and lets me fine-tune it later on my own categorisation without asking permission.&lt;/p&gt;

&lt;h3&gt;
  
  
  The numbers, since claims deserve them
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;files filed&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;total model time&lt;/td&gt;
&lt;td&gt;307 s (~5.1 min), spent while I was not using the machine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;average per file&lt;/td&gt;
&lt;td&gt;19 s (one local 4B call, temperature 0)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tokens generated&lt;/td&gt;
&lt;td&gt;823&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bytes uploaded&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dollars spent&lt;/td&gt;
&lt;td&gt;$0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;human clicks&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mis-filed on review&lt;/td&gt;
&lt;td&gt;0 flatly wrong, 2 arguable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The reproduction is two commands, and the second one re-derives the model comparison:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 scripts/make_fixtures.py          &lt;span class="c"&gt;# writes the 16 messy files&lt;/span&gt;
python3 scripts/compare_models.py gemma3:1b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The run above is 16 files, so treat the accuracy as a spot check rather than a benchmark; it is a holdout the size of one evening's clutter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lazy Payoff
&lt;/h2&gt;

&lt;p&gt;The person who gets to be lazier is me, at 11pm, with a folder of 16 files I keep avoiding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it did, measured:&lt;/strong&gt; 16 files, unattended, on a 2-core ARM VM, with &lt;code&gt;gemma3:4b&lt;/code&gt; Q4_K_M running locally through Ollama. Total model time ~5.1 minutes, spread across the evening while I was not using the machine. Human clicks: 0. Files uploaded: 0. Dollars: $0.00.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it saved:&lt;/strong&gt; the dashboard uses a deliberately conservative 20 seconds per file — open it, read enough to know what it is, decide, rename, move. 16 files × 20s ≈ &lt;strong&gt;5.3 minutes&lt;/strong&gt; of the specific kind of attention that is hardest to schedule: small, boring, interruptible, and always deferred to tomorrow. The estimate is probably low, and I would rather understate it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it got wrong:&lt;/strong&gt; a grocery list went to &lt;code&gt;personal&lt;/code&gt; instead of &lt;code&gt;reference&lt;/code&gt;, which is arguable, and a page of project musings in a file called &lt;code&gt;Untitled document (3).txt&lt;/code&gt; went to &lt;code&gt;code&lt;/code&gt; because the contents mention a deploy and a cache. No file landed somewhere I would call flatly wrong, nothing was deleted, and everything is reversible in one call. That is the bar I wanted: a tired intern, not an oracle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest bit:&lt;/strong&gt; the folder in this post is a synthetic one I put together to make the run reproducible (&lt;code&gt;scripts/make_fixtures.py&lt;/code&gt;), because my real downloads folder is full of invoices with my address on them and I am not publishing those. The code does not care which folder it points at; &lt;code&gt;--inbox&lt;/code&gt; is the only thing that changes.&lt;/p&gt;

&lt;p&gt;When it finished I opened the folder once, saw &lt;code&gt;money/&lt;/code&gt;, &lt;code&gt;work/&lt;/code&gt;, &lt;code&gt;code/&lt;/code&gt;, &lt;code&gt;personal/&lt;/code&gt;, &lt;code&gt;reference/&lt;/code&gt;, &lt;code&gt;junk/&lt;/code&gt;, and closed it again. That is the entire product: it removes a decision I did not want to make, at a time I was not using the computer, for free.&lt;/p&gt;

&lt;p&gt;Slow and steady. Mostly slow.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Best Use of Gemma&lt;/strong&gt; — the whole project is built on Gemma running locally: &lt;code&gt;gemma3:4b&lt;/code&gt; (Q4_K_M, via Ollama) does every classification, with &lt;code&gt;gemma3:1b&lt;/code&gt; as the speed option in the same code path, and a deterministic keyword fallback when the model is unavailable. There is no other model provider in the project.&lt;/p&gt;

&lt;p&gt;I am not entering any other category, because the build does not use any other partner technology — no hosting, no cloud inference, no auth, no database. The point of this one is that it runs on the machine it is sitting on.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>Frostline: a planting planner that decides on your own machine</title>
      <dc:creator>Aman</dc:creator>
      <pubDate>Mon, 12 Oct 2026 06:42:50 +0000</pubDate>
      <link>https://dev.to/amandevelops/frostline-a-planting-planner-that-decides-on-your-own-machine-4a9m</link>
      <guid>https://dev.to/amandevelops/frostline-a-planting-planner-that-decides-on-your-own-machine-4a9m</guid>
      <description>&lt;h1&gt;
  
  
  Frostline: a planting planner that decides on your own machine
&lt;/h1&gt;

&lt;p&gt;This is a submission for &lt;a href="https://dev.to/challenges/hacktoberfest-week1-2026-10-05"&gt;Hacktoberfest Open-Source AI Challenge: Week 1&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Theme: Touch Grass.&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;I have a small balcony in Delhi NCR. Every October I do the same thing: buy coriander seeds, sow them, and watch them bolt in the heat — because October in Delhi is not autumn, it is a second summer with shorter days. The advice I was following came from gardening sites written for England.&lt;/p&gt;

&lt;p&gt;Frostline answers one question — &lt;strong&gt;what should I sow this week, where I actually live&lt;/strong&gt; — and it answers it from my own weather history, on my own laptop, with no account and no API keys.&lt;/p&gt;

&lt;p&gt;You open it, pick your town, press one button. It tells you the next seven days of temperature for your coordinates and which of 30 crops fit that window. This week for Delhi it says: bush beans, cucumber, okra, marigold, basil, mint, bottle gourd and sunflower all fit at 100%. Radish, fenugreek, spinach, coriander and mustard greens score zero, and the app says so out loud instead of hedging.&lt;/p&gt;

&lt;p&gt;Then you close the laptop and go plant.&lt;/p&gt;

&lt;h2&gt;
  
  
  The demo
&lt;/h2&gt;

&lt;p&gt;Here is the app running — 29 seconds, no mockups:&lt;/p&gt;

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

&lt;p&gt;One honest note about the clip: TabPFN refits on every run, and on this two-core box a full fit takes minutes, so the recording shows the button press and then cuts the wait. What appears after the cut is the output of a completed run — the same run whose numbers are below, rendered by the app itself. Nothing in the clip is staged, but the waiting is not real-time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why open innovation matters for this
&lt;/h2&gt;

&lt;p&gt;A planting calendar is a small, personal thing. The closed version of it is worse in five specific ways, and every one of them is why this is built the way it is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The model runs on the machine.&lt;/strong&gt; TabPFN does its inference on CPU, in the same process as the web server. My latitude, my garden, my plans are never sent anywhere to be scored. I do not have to trust a vendor's retention policy to ask what to plant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It works with the network unplugged.&lt;/strong&gt; Weather history is cached in one SQLite file — 5,761 days of daily min/max for Delhi, back to 2011. After the first fetch, the whole planner runs offline. I tested this properly: I stubbed the archive fetcher so it raised on every call, and the app served the cached history and produced the full plan anyway. That matters for a tool you would actually take outside.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The advice is inspectable.&lt;/strong&gt; The crop table is a plain Python list of temperature envelopes, not a black box. If okra is mis-scored for your microclimate, you open &lt;code&gt;frostline/crops.py&lt;/code&gt; and change a number. No retraining, no waiting on a vendor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The model is swappable, and optional.&lt;/strong&gt; Gemma sits behind a single function. If Ollama is not running, Frostline falls back to a deterministic template-written plan and keeps working. Nothing 500s because a model is missing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It costs nothing to run.&lt;/strong&gt; No per-request price for asking what to plant, and no ceiling on how often you ask.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Open-Meteo archive (free, no key)  →  SQLite cache  →  20 engineered features
   →  TabPFN regressor (CPU, local)  →  7-day min/max forecast
   →  crop scoring against temperature envelopes
   →  Gemma 3 4B via Ollama writes the plan in plain language
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things I care about in the design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The forecast is recursive.&lt;/strong&gt; TabPFN predicts day one, that prediction is fed back in as a lag feature, then day two, and so on for seven days. The 20 features are the day-of-year (raw, plus sine/cosine so December and January are neighbours), year, lags at 1/2/3/7 days, and rolling windows up to 30 days.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The model has to justify itself.&lt;/strong&gt; Frostline holds out the last seven days of history, predicts them, and scores the mean absolute error against a day-of-year climatology baseline. The UI has a panel literally titled &lt;em&gt;"Did the model earn its place?"&lt;/em&gt; — if the model cannot beat "the average for this date over 15 years", that is what it says.&lt;/p&gt;

&lt;h2&gt;
  
  
  Did the model earn its place?
&lt;/h2&gt;

&lt;p&gt;Yes — and here is the number that decides it. Frostline holds out the last seven days of real history, predicts them, and scores itself against the simplest thing that could work: the average for that same date over 15 years.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Held-out week: 3–9 October 2026. 5,724 days of training history, fitted on the most recent 2,500.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Min temperature: &lt;strong&gt;1.024 °C MAE&lt;/strong&gt;, against 2.797 °C for the climatology baseline — &lt;strong&gt;63% better&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Max temperature: &lt;strong&gt;1.152 °C MAE&lt;/strong&gt;, against 1.808 °C — &lt;strong&gt;36% better&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model also earns its place in shape, not just in error. A day-of-year average cannot see a trend by construction — it is the same number every year for that date. This week Delhi cools from 32.7 °C down to 28.2 °C, and TabPFN called the slide; the baseline sat flat at 32 °C for all seven days. That is the difference between a calendar and a forecast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The caveat, stated plainly.&lt;/strong&gt; A seven-day holdout is a small sample, and the two targets disagree about how much the training cap matters. Re-running the same holdout with a 200-row cap gives 57% on min temperature but 76% on max — so read "36% better on max temperature" as a point estimate from one week, not a settled figure. A walk-forward evaluation over many holdout windows is the honest next step; at roughly six minutes per fit on this CPU it is an overnight job, not a footnote.&lt;/p&gt;

&lt;p&gt;Both numbers are reproducible on your own machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;.venv/bin/python scripts/evaluate.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What broke
&lt;/h2&gt;

&lt;p&gt;Five things, all worth writing down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gemma invented weather.&lt;/strong&gt; My first prompt got back a plan that warned about frost protection. There were zero frost-risk nights in the window — the coldest night was 19.9 °C. A small model will happily describe a hazard the data says is absent, and that contradiction is the first thing a reader notices. The fix was to put every fact in as a list, state the exact date range and that there is no data beyond it, and forbid the specific invention: &lt;em&gt;"Frost-risk nights: NONE. Do not mention frost, frost cloth, or frost protection."&lt;/em&gt; It now writes &lt;em&gt;"no risk of anything needing protection from the cold."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The plan was a week stale.&lt;/strong&gt; The cache top-up had an early return: if more than five years of history was already cached, it skipped fetching entirely. So the app kept serving whatever week the cache was first filled in. On 11 October it produced a forecast for 4–10 October, and Gemma faithfully wrote &lt;em&gt;"start planting your bush beans today, October 4th."&lt;/em&gt; The model was not wrong — my pipeline was handing it last week's data. Now every run requests only the missing tail, which also makes the offline path explicit instead of accidental.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A 4B model needs 4.1 GB of RAM and does not negotiate.&lt;/strong&gt; The Ollama runner kept dying mid-load. I initially blamed the CPU repack path, and I was wrong — the log line still read &lt;code&gt;REPACK = 1&lt;/code&gt; with the flag set, and the real cause was the OOM killer: the process was inside a 4 GB memory cgroup. It now runs detached with a 6 GB budget. I mention this because "run a model locally" has a resource floor, and the honest number is more useful than a mystery crash.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The bug that only existed once the model worked.&lt;/strong&gt; &lt;code&gt;_run()&lt;/code&gt; in &lt;code&gt;forecast.py&lt;/code&gt; built its recursive walk from a variable it had never been passed. Every code path that could reach it was gated behind a working TabPFN licence, so it was unreachable — and therefore invisible — until the day the licence cleared. The first real run fitted the model, then died with &lt;code&gt;NameError: name 'df' is not defined&lt;/code&gt;, and the broad exception handler I wrote &lt;em&gt;on purpose&lt;/em&gt; ("the model is not usable here, never a broken request") swallowed it and quietly served the climatology fallback. The fallback design that makes the app robust is the same design that hid the bug. I only caught it because I read the run log instead of the UI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TabPFN on a 2-core ARM box is not fast.&lt;/strong&gt; The full run — two evaluation fits, two product fits, and seven recursive one-day-ahead predictions — took &lt;strong&gt;68 minutes&lt;/strong&gt; on two CPU cores. TabPFN is a transformer that does its learning in-context, so every prediction re-reads the whole training set: the cost is per-prediction, not per-training-run. Lowering the row cap to a few hundred cuts that dramatically, and on this holdout it did not hurt accuracy. I shipped the 2,500-row default because it is the stronger claim, and I would rather state the honest runtime than pretend a 7-day forecast is free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest gaps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The TabPFN licence needs a one-time interactive acceptance per model version, and the weights are 876 MB. That is a real gate on anyone cloning this — on a fresh machine the accuracy panel honestly reads &lt;em&gt;"TabPFN is not active, so there is no model to score"&lt;/em&gt; until that is done. The app never hard-fails either way: no TabPFN token falls back to climatology, no Ollama falls back to a template plan, and both fallbacks are labelled in the UI rather than hidden.&lt;/li&gt;
&lt;li&gt;Gemma 3 4B is the largest model this machine can serve.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The code is here: &lt;strong&gt;&lt;a href="https://github.com/amandevelops/frostline" rel="noopener noreferrer"&gt;https://github.com/amandevelops/frostline&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/amandevelops/frostline.git &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;frostline
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; .venv/bin/pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'TABPFN_TOKEN=%s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;your key&amp;gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .env    &lt;span class="c"&gt;# free: ux.priorlabs.ai -&amp;gt; Licenses tab&lt;/span&gt;
.venv/bin/python scripts/evaluate.py               &lt;span class="c"&gt;# prints the accuracy table above&lt;/span&gt;
.venv/bin/python server.py                         &lt;span class="c"&gt;# then open http://127.0.0.1:8000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server is one standard-library file — no framework, no build step, no container. On a slow machine set &lt;code&gt;FROSTLINE_MAX_ROWS=400&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt; and a full run drops from about an hour to a couple of minutes.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with &lt;a href="https://priorlabs.ai" rel="noopener noreferrer"&gt;TabPFN&lt;/a&gt; (Prior Labs, CPU inference), &lt;a href="https://ai.google.dev/gemma" rel="noopener noreferrer"&gt;Gemma 3&lt;/a&gt; via &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt;, and &lt;a href="https://open-meteo.com" rel="noopener noreferrer"&gt;Open-Meteo&lt;/a&gt;'s free archive.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>SplitBasket: a local-first grocery ledger I built for my flatmate</title>
      <dc:creator>Aman</dc:creator>
      <pubDate>Mon, 05 Oct 2026 06:19:30 +0000</pubDate>
      <link>https://dev.to/amandevelops/splitbasket-a-local-first-grocery-ledger-i-built-for-my-flatmate-3ji3</link>
      <guid>https://dev.to/amandevelops/splitbasket-a-local-first-grocery-ledger-i-built-for-my-flatmate-3ji3</guid>
      <description>&lt;p&gt;This is a submission for the Hacktoberfest Weekend Challenge: Build for a Friend&lt;/p&gt;

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

&lt;p&gt;My flatmate Akshay and I split groceries constantly. One of us pays for the dal, the other for the gas cylinder, and by the end of the month neither of us remembers who owes what. We tried a shared spreadsheet twice and both times it was dead within two weeks — logging a purchase meant opening a file, finding the right row, and doing the arithmetic yourself, which is exactly the friction that makes people stop bothering.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;SplitBasket&lt;/strong&gt; for him. You log a purchase once — item, amount, who paid, the date — tick which flatmates shared it, and that's the whole job. It categorises the item on its own, computes the equal split, and keeps a running balance of who owes whom.&lt;/p&gt;

&lt;p&gt;What it actually does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Log a purchase&lt;/strong&gt; in one small form: item, amount, payer, date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tick who shared it&lt;/strong&gt; — the equal split is worked out for you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-categorise&lt;/strong&gt; into staples, cooking, fresh produce, dairy &amp;amp; eggs, gas &amp;amp; utilities, snacks &amp;amp; drinks, household, personal care, or other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;See the month at a glance&lt;/strong&gt; — total spend, your share, your live balance, and a spend-by-category breakdown.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Settle up&lt;/strong&gt; — a plain snapshot of who owes whom, no interpretation needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Export to CSV&lt;/strong&gt; whenever you want the raw numbers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The part that matters most to us: it all runs on the laptop. The entire ledger lives in a single SQLite file on the machine, and the app works with no internet connection at all.&lt;/p&gt;

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

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

&lt;p&gt;The clip walks through logging a grocery run: I type the item, the category appears as I type, the split is computed across all three flatmates, and the new entry lands in the settle-up view.&lt;/p&gt;

&lt;p&gt;I handed it over last week — he stopped asking me what he owes, which was the entire point.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://github.com/AmanDevelops/SplitBasket" rel="noopener noreferrer"&gt;https://github.com/AmanDevelops/SplitBasket&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;The whole thing is deliberately small. It's a &lt;strong&gt;Node.js&lt;/strong&gt; app (Node ≥ 22.5) with &lt;strong&gt;zero runtime dependencies&lt;/strong&gt;, and the open-source AI is the core of how it thinks, not a decorative add-on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storage — &lt;code&gt;node:sqlite&lt;/code&gt;.&lt;/strong&gt; Node now ships SQLite in the standard library, so the database is the built-in driver: no ORM, no install step, no external service. Three tables — &lt;code&gt;members&lt;/code&gt;, &lt;code&gt;transactions&lt;/code&gt;, and &lt;code&gt;transaction_shares&lt;/code&gt; — with foreign keys on and the split stored as rows, so a purchase and its shares are always consistent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Categorisation — local rules first.&lt;/strong&gt; A grocery-aware rule set maps keywords to categories. It's tuned for how people in an Indian flat actually write their shopping lists, so it recognises Hinglish alongside English: &lt;code&gt;dal&lt;/code&gt;, &lt;code&gt;atta&lt;/code&gt;, &lt;code&gt;chawal&lt;/code&gt;, &lt;code&gt;sabzi&lt;/code&gt;, &lt;code&gt;pyaz&lt;/code&gt;, &lt;code&gt;doodh&lt;/code&gt;, &lt;code&gt;paneer&lt;/code&gt;, &lt;code&gt;tel&lt;/code&gt;, &lt;code&gt;haldi&lt;/code&gt;, &lt;code&gt;namak&lt;/code&gt;, &lt;code&gt;chai&lt;/code&gt;, &lt;code&gt;gas cylinder&lt;/code&gt;. Type "Toor dal 1kg" and it lands in Staples without asking anyone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Categorisation — an open-weight model when there's one to use.&lt;/strong&gt; If Ollama is running locally, the server first asks &lt;strong&gt;Gemma 3 (1B)&lt;/strong&gt; — Google's open-weight model, pulled with &lt;code&gt;ollama pull gemma3:1b&lt;/code&gt; — to classify the item. The request asks for a strict JSON shape (&lt;code&gt;category&lt;/code&gt;, &lt;code&gt;confidence&lt;/code&gt;, &lt;code&gt;reason&lt;/code&gt;), runs with a 3.5-second timeout, and is parsed defensively. If the model is missing, slow, or returns anything unusable, the code &lt;strong&gt;falls back to the local rules automatically&lt;/strong&gt; and the app keeps working exactly as before.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm start                     &lt;span class="c"&gt;# then open http://localhost:3000&lt;/span&gt;
ollama pull gemma3:1b         &lt;span class="c"&gt;# optional: richer categorisation&lt;/span&gt;
&lt;span class="nv"&gt;OLLAMA_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gemma3:1b npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The front end is plain HTML, CSS, and JavaScript served by the same Node process — no build step, no framework, nothing to install.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;This is the part where open wasn't a preference — it was the right engineering call for this particular project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The data never leaves the device.&lt;/strong&gt; A grocery ledger looks harmless, but it's a detailed record of what three people eat, when they cook, and what they spend. With local inference, my flatmate's habits stay on our laptop instead of sitting on a server neither of us controls. For something this personal and this small, "we promise we won't look" isn't a good enough answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The behaviour is inspectable and changeable.&lt;/strong&gt; The categoriser is plain rules in a plain file. When my flatmate told me "that's personal care, not household," I changed one line and it was fixed — no retraining, no prompt-tuning against an opaque model, no waiting on a vendor to improve a black box. With a closed API I'd have had a category I couldn't argue with.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The model is swappable, and its absence is survivable.&lt;/strong&gt; It runs Gemma 3 (1B) today. If we want a larger model, a different one, or a fine-tune, we change a model name — the product doesn't care, because the model sits behind one function with a working fallback. That fallback is the real win: the app is fully functional with the model switched off entirely. A closed API can't offer that. If it's down, rate-limited, or repriced, your app is down with it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It costs nothing to run.&lt;/strong&gt; A flat's grocery ledger should not have a per-request cost. There's no key to rotate, no quota to watch, no bill that grows as the flatmate count does. The only running cost is the laptop we already had.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It works offline.&lt;/strong&gt; No internet, no problem — which matters more than it sounds, because the moment the ledger stops working is the moment it stops being used.&lt;/p&gt;

&lt;p&gt;None of this would have been possible with a closed hosted API without giving something up: the privacy, the inspectability, the offline behaviour, or the cost. Open-weight models and local inference let me hand a friend a tool that is private, honest about how it works, and genuinely his.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Best Use of Gemma&lt;/strong&gt; — SplitBasket uses Google's open-weight &lt;strong&gt;Gemma 3 (1B)&lt;/strong&gt; via Ollama as its primary categoriser, running locally on the machine, with a rule-based fallback so the app stays fully functional when no model is available.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>hf26challenge</category>
    </item>
  </channel>
</rss>
