<?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: forkcount</title>
    <description>The latest articles on DEV Community by forkcount (@forkcount).</description>
    <link>https://dev.to/forkcount</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%2F4173625%2Ff1241393-48ee-4f8e-b1c8-fb110167f0fb.png</url>
      <title>DEV Community: forkcount</title>
      <link>https://dev.to/forkcount</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/forkcount"/>
    <language>en</language>
    <item>
      <title>There's no more labagent.tech spam or phishing on dev.to. Here's how it got removed.</title>
      <dc:creator>forkcount</dc:creator>
      <pubDate>Wed, 28 Oct 2026 10:06:15 +0000</pubDate>
      <link>https://dev.to/forkcount/theres-no-more-labagenttech-spam-or-phishing-on-devto-heres-how-it-got-removed-l15</link>
      <guid>https://dev.to/forkcount/theres-no-more-labagenttech-spam-or-phishing-on-devto-heres-how-it-got-removed-l15</guid>
      <description>&lt;p&gt;Two weeks ago, every post I read had the same comment underneath it. Vietnamese text, on-topic, ending with a link to &lt;code&gt;labagent.tech&lt;/code&gt;. Click it and you got a fake dev.to login page asking for your credentials.&lt;/p&gt;

&lt;p&gt;That's phishing. Not spam, not self-promotion — a fake login page designed to steal accounts.&lt;/p&gt;

&lt;p&gt;Today, there's none. I checked. The scanner I run every night came back with zero reportable findings for the third day in a row. No &lt;code&gt;labagent.tech&lt;/code&gt;, no &lt;code&gt;tr.ee&lt;/code&gt; impersonation accounts, no phishing campaign against dev.to users.&lt;/p&gt;

&lt;p&gt;Here's what happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Tiger Sentinel
&lt;/h2&gt;

&lt;p&gt;Tiger Sentinel is a Python script that scans dev.to's public API for coordinated spam and phishing comments.&lt;/p&gt;

&lt;p&gt;The concept came from Harun (&lt;a class="mentioned-user" href="https://dev.to/koda2026"&gt;@koda2026&lt;/a&gt;), a 12-year-old developer who built the original version to protect his own posts. I expanded it into a full daily scan that covers thousands of articles per run.&lt;/p&gt;

&lt;p&gt;The name comes from Harun. The expansion is mine. The bot runs on GitHub Actions at 3 AM UTC every day.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;For each article on dev.to, it fetches the comment thread and scores every comment against two tiers of signals.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strong signals&lt;/strong&gt; — two or more means "report this":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Known phishing domains (&lt;code&gt;labagent.tech&lt;/code&gt;, &lt;code&gt;suppdev.io&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Two or more impersonation phrases ("official platform update", "verify your account", "you have 24 hours")&lt;/li&gt;
&lt;li&gt;Shortlink domain + Vietnamese text&lt;/li&gt;
&lt;li&gt;Shortlink domain + near-empty comment&lt;/li&gt;
&lt;li&gt;Shortlink domain + bot-style username (&lt;code&gt;name_name_hexid&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Weak signals&lt;/strong&gt; — flags for review, not report:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Single shortlink&lt;/li&gt;
&lt;li&gt;Single impersonation phrase&lt;/li&gt;
&lt;li&gt;Boilerplate phrases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A comment needs two strong signals to be classified as reportable. One strong signal plus weak signals lands in the review queue. Zero strong signals means ignore.&lt;/p&gt;

&lt;p&gt;That's the whole design. The first version flagged every comment with a link at the end — 313 findings, most of them legitimate. The rewrite requiring two strong signals dropped the noise to near zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it found
&lt;/h2&gt;

&lt;p&gt;First full scan: &lt;strong&gt;500 articles, 313 flags, 4 confirmed phishing accounts.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@anh_nguynvn_0478e614ba&lt;/code&gt; — 16 comments, &lt;code&gt;labagent.tech&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@shieldxbot&lt;/code&gt; — 12 comments, &lt;code&gt;labagent.tech&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@suppdevbot&lt;/code&gt; — 8 comments, &lt;code&gt;tr.ee&lt;/code&gt; + "official platform update"&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@devantibot&lt;/code&gt; — 9 comments, &lt;code&gt;tr.ee&lt;/code&gt; + "verify your account"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All four created on the same day. All four posting the same pattern. That's not four spammers who found the same domain. That's a coordinated phishing campaign.&lt;/p&gt;

&lt;p&gt;The bot also groups findings by shortlink domain, which turned 4 accounts into a larger picture:&lt;br&gt;
tr.ee — 2 accounts (phishing)&lt;br&gt;
labagent.tech — 2 accounts (phishing)&lt;br&gt;
tinyurl.com — 25 accounts (coordinated?)&lt;br&gt;
shorturl.at — 23 accounts (coordinated?)&lt;br&gt;
t.co — 20 accounts (likely legitimate)&lt;/p&gt;

&lt;p&gt;text&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;tr.ee&lt;/code&gt; and &lt;code&gt;labagent.tech&lt;/code&gt; pairs were the actual phishing campaign. The rest were pattern matches that needed human judgment.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it got reported
&lt;/h2&gt;

&lt;p&gt;dev.to has a report abuse form at &lt;code&gt;/report-abuse&lt;/code&gt; that accepts bulk reports. I sent one submission:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The 4 confirmed phishing accounts&lt;/li&gt;
&lt;li&gt;The 3 domain clusters&lt;/li&gt;
&lt;li&gt;A note explaining which were confirmed phishing and which were pattern matches&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not 199 individual reports. One form, five groups. That's the difference between a report a moderation team can act on and a report that gets filed as noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened
&lt;/h2&gt;

&lt;p&gt;A few days later, all four phishing accounts were gone. dev.to banned them. The &lt;code&gt;tr.ee&lt;/code&gt; cluster dropped from 5 accounts to 3. &lt;code&gt;labagent.tech&lt;/code&gt; disappeared entirely.&lt;/p&gt;

&lt;p&gt;Two weeks later, my account was flagged as &lt;strong&gt;Trusted User&lt;/strong&gt;. The system recognized that my report got a real phishing campaign removed. Trusted User status means future reports go to the front of the moderation queue.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the scanner looks like now
&lt;/h2&gt;

&lt;p&gt;Three full scans in a row have found zero reportable findings. The bot runs for five hours, covers roughly 5,000 articles per run, and comes back with nothing.&lt;/p&gt;

&lt;p&gt;That's the success case. Not "the bot found more phishing." The bot finding nothing means the phishing campaign is gone and no new one has appeared.&lt;/p&gt;

&lt;p&gt;The review findings — 900 to 1,000 per scan — are legitimate comments with shortlinks. People sharing tweets, blog posts, YouTube videos. Not phishing.&lt;/p&gt;

&lt;p&gt;The cluster counts still fluctuate: &lt;code&gt;t.co&lt;/code&gt; has 137 accounts, &lt;code&gt;shorturl.at&lt;/code&gt; has 45. Those are likely legitimate. Twitter links and URL shorteners used by real users.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lessons
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The first version was wrong.&lt;/strong&gt; 313 flags, mostly legitimate comments. The fix wasn't removing weak signals — it was requiring strong ones to co-occur.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The scanner is not a judge.&lt;/strong&gt; It produces candidates. A human decides. The 313 → 4 funnel is the point. If a bot reports everything, it reports nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bulk beats individual.&lt;/strong&gt; A moderation team processes "4 accounts using the same phishing domain" faster than 199 separate flags.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Continuous beats one-time.&lt;/strong&gt; Daily scanning means the next phishing campaign gets caught on day one, not week three.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The bot runs every night. If a new phishing campaign appears, &lt;code&gt;findings_report.json&lt;/code&gt; will have it in the morning. If a new cluster forms with a known phishing domain, the report goes to dev.to within a day.&lt;/p&gt;

&lt;p&gt;The concept came from Harun. The daily scan is mine. The result is a platform with no active phishing campaign against it.&lt;/p&gt;

&lt;p&gt;That's a real win. Not "I built a tool" — "I built a tool, used it, and the phishing campaign it was built to fix is gone."&lt;/p&gt;

&lt;p&gt;The scanner is Python on GitHub Actions. I wrote it by describing what I wanted to Google AI Studio and fixing what broke. It's not production-grade — no tests, no retries on transient failures, no alerting. But it caught a real phishing campaign and got it removed.&lt;/p&gt;

&lt;p&gt;Credit to Harun (&lt;a class="mentioned-user" href="https://dev.to/koda2026"&gt;@koda2026&lt;/a&gt;) for the original Tiger Sentinel concept. He built it to protect his own posts. I turned it into a daily scan.&lt;/p&gt;

&lt;p&gt;The report abuse form at dev.to is &lt;code&gt;/report-abuse&lt;/code&gt;. Bulk submission made the difference.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>security</category>
    </item>
    <item>
      <title>I built a bot that scans dev.to for phishing comments. It found a coordinated campaign. dev.to banned 70+ accounts.</title>
      <dc:creator>forkcount</dc:creator>
      <pubDate>Wed, 28 Oct 2026 09:13:15 +0000</pubDate>
      <link>https://dev.to/forkcount/i-built-a-bot-that-scans-devto-for-phishing-comments-it-found-a-coordinated-campaign-devto-3mi6</link>
      <guid>https://dev.to/forkcount/i-built-a-bot-that-scans-devto-for-phishing-comments-it-found-a-coordinated-campaign-devto-3mi6</guid>
      <description>&lt;p&gt;Two weeks ago, a phishing bot commented on my post. Vietnamese text, on-topic, ending with a link to &lt;code&gt;labagent.tech&lt;/code&gt;. Click it and you got a fake dev.to login page.&lt;/p&gt;

&lt;p&gt;I reported it. Another appeared. Then another. Same pattern, different accounts, all created the same day.&lt;/p&gt;

&lt;p&gt;So I built a scanner. The idea came from Harun (&lt;a class="mentioned-user" href="https://dev.to/koda2026"&gt;@koda2026&lt;/a&gt;), who built the original Tiger Sentinel to protect his own posts. I expanded it into a daily scan.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;Scans dev.to's public API. For each article, fetches comments and scores them against two tiers of signals.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strong&lt;/strong&gt; (2+ needed to flag as reportable):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Known phishing domains&lt;/li&gt;
&lt;li&gt;Two or more impersonation phrases ("verify your account", "you have 24 hours")&lt;/li&gt;
&lt;li&gt;Shortlink + Vietnamese text&lt;/li&gt;
&lt;li&gt;Shortlink + near-empty comment&lt;/li&gt;
&lt;li&gt;Shortlink + bot-style username&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Weak&lt;/strong&gt; (review only):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Single shortlink&lt;/li&gt;
&lt;li&gt;Single impersonation phrase&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The two-tier scoring is the whole trick. The first version flagged every comment with a link at the end — 313 findings, most legitimate. Requiring two strong signals dropped the noise to near zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it found
&lt;/h2&gt;

&lt;p&gt;First full scan: &lt;strong&gt;500 articles, 313 flags, 4 confirmed phishing accounts.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@anh_nguynvn_0478e614ba&lt;/code&gt; — 16 comments, &lt;code&gt;labagent.tech&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@shieldxbot&lt;/code&gt; — 12 comments, &lt;code&gt;labagent.tech&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@suppdevbot&lt;/code&gt; — 8 comments, &lt;code&gt;tr.ee&lt;/code&gt; + "official platform update"&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@devantibot&lt;/code&gt; — 9 comments, &lt;code&gt;tr.ee&lt;/code&gt; + "verify your account"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All four created the same day. All four posting the same pattern. That's a campaign.&lt;/p&gt;

&lt;p&gt;The bot groups findings by shortlink domain. That turned 4 accounts into a report:&lt;br&gt;
tr.ee — 2 accounts (phishing)&lt;br&gt;
labagent.tech — 2 accounts (phishing)&lt;br&gt;
tinyurl.com — 25 accounts (coordinated?)&lt;br&gt;
shorturl.at — 23 accounts (coordinated?)&lt;br&gt;
t.co — 20 accounts (likely legitimate)&lt;/p&gt;

&lt;p&gt;text&lt;/p&gt;

&lt;h2&gt;
  
  
  How I reported it
&lt;/h2&gt;

&lt;p&gt;dev.to has a report abuse form at &lt;code&gt;/report-abuse&lt;/code&gt; that accepts bulk reports. I sent one submission:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The 4 confirmed phishing accounts&lt;/li&gt;
&lt;li&gt;The 3 domain clusters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not 199 individual reports. One form, five groups.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened
&lt;/h2&gt;

&lt;p&gt;A few days later, all four accounts were gone. dev.to banned them. The &lt;code&gt;tr.ee&lt;/code&gt; cluster dropped from 5 to 3. &lt;code&gt;labagent.tech&lt;/code&gt; disappeared.&lt;/p&gt;

&lt;p&gt;Two weeks later, my account was flagged as &lt;strong&gt;Trusted User&lt;/strong&gt;. The system recognized that my report got a real campaign removed. Future reports now go to the front of the moderation queue.&lt;/p&gt;

&lt;p&gt;That's the whole pipeline: Harun's idea → my expanded scan → report → platform acts → trust built.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lessons
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The first version was wrong.&lt;/strong&gt; 313 flags, mostly legitimate comments. The fix wasn't removing weak signals — it was requiring strong ones to co-occur.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The scanner is not a judge.&lt;/strong&gt; It produces candidates. A human decides. The 313 → 4 funnel is the point. If a bot reports everything, it reports nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bulk beats individual.&lt;/strong&gt; A moderation team processes "4 accounts using the same phishing domain" faster than 199 separate flags.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Continuous beats one-time.&lt;/strong&gt; Three scans in a row found zero new reportable accounts. That's the success case — no new campaign appeared after the first was removed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The code
&lt;/h2&gt;

&lt;p&gt;The core scoring function:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
python
def score_comment(text, username):
    strong, weak = [], []

    for domain in PHISHING_DOMAINS:
        if domain in text.lower():
            strong.append(f"phishing-domain:{domain}")

    hits = [p for p in IMPERSONATION_PHRASES if p in text.lower()]
    if len(hits) &amp;gt;= 2:
        strong.append("impersonation-pair")
    elif len(hits) == 1:
        weak.append(f"imp-phrase:{hits[0]}")

    short = next((d for d in SHORTLINKS if d in text.lower()), None)
    if short:
        if VIETNAMESE_REGEX.search(text):
            strong.append(f"shortlink+vn:{short}")
        elif len(text) &amp;lt; 20:
            strong.append(f"shortlink+near-empty:{short}")
        else:
            weak.append(f"shortlink:{short}")

    if len(strong) &amp;gt;= 2:
        return "report"
    elif strong or weak:
        return "review"
    return None
That's the whole logic. The rest is pagination, rate limiting, and output formatting.

The takeaway
dev.to is mostly safe. The spam you see is a small number of accounts posting a lot. A coordinated campaign of 4 accounts produced 45 comments across the platform — enough to feel like a flood if you're one of the people getting them, but tiny against the total comment volume.

The report form works. Bulk reporting works. Trusted User status is real and earned by making reports that get acted on.

The scanner is Python on GitHub Actions. I wrote it by describing what I wanted to Google AI Studio and fixing what broke. Not production-grade — no tests, no retries, no alerting. But it caught a real campaign and got it removed.

Credit to Harun (@koda2026) for the original Tiger Sentinel concept. He built it to protect his posts. I turned it into a daily scan.

forkcount.github.io is the calorie tracker I built the same way.

The report abuse form at dev.to is /report-abuse. Bulk submission made the difference.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>security</category>
    </item>
    <item>
      <title>I built a free calorie tracker with a metabolic safety floor. No card, no email, no paywall. And I found out why every other app doesn't.</title>
      <dc:creator>forkcount</dc:creator>
      <pubDate>Wed, 28 Oct 2026 07:23:56 +0000</pubDate>
      <link>https://dev.to/forkcount/i-built-a-free-calorie-tracker-with-a-metabolic-safety-floor-no-card-no-email-no-paywall-and-i-49j7</link>
      <guid>https://dev.to/forkcount/i-built-a-free-calorie-tracker-with-a-metabolic-safety-floor-no-card-no-email-no-paywall-and-i-49j7</guid>
      <description>&lt;p&gt;Every calorie app I tried had the same problem: it let you set a target of 1,200 calories or lower and called it a plan. Some went lower. That's how people end up in the binge-starve cycle — you eat 800, your body fights back, you eat 4,000, you hate yourself, repeat.&lt;/p&gt;

&lt;p&gt;So I built one that refuses.&lt;/p&gt;

&lt;p&gt;But here's the thing I figured out after two weeks of building it: &lt;strong&gt;the apps aren't badly designed. They're designed around a number you can't argue with.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The number that matters
&lt;/h2&gt;

&lt;p&gt;Your BMR is the calories your body burns just staying alive. Mine is 1,396. If you eat below that for weeks, you don't just lose fat — you lose muscle. Your metabolism slows. The deficit that worked at 2,000 calories stops working at 1,400 because your body has adapted to the smaller intake.&lt;/p&gt;

&lt;p&gt;Every app I tried let you set a target below your BMR anyway. Because the user asked. Because a lower number is a faster result. Because "it won't let me" is a support ticket.&lt;/p&gt;

&lt;p&gt;ForkCount won't let you. It calculates your BMR (Mifflin-St Jeor), your TDEE (BMR × activity multiplier), and a target that keeps you above the BMR floor. If you try to set a target below that line, it refuses. You can override it in the settings, but the app tells you what you're doing and shows the maths.&lt;/p&gt;

&lt;p&gt;That one decision — refusing to let the user hurt themselves — is the whole app. Everything else is a food diary.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;A food diary that parses typed text. You type "200g chicken and 150g rice" and it becomes two items with macros. No database search, no barcode scan. Just type.&lt;/p&gt;

&lt;p&gt;A weekly meal plan generator that respects your dietary preference. Set vegetarian, get vegetarian. Set vegan, get vegan. Set a calorie target of 1,400, get a plan built around 1,400 — not the default 2,000 that most generators quietly use.&lt;/p&gt;

&lt;p&gt;A water tracker, weight log, exercise log with kcal estimates, and a community feed.&lt;/p&gt;

&lt;p&gt;Free. No card. No trial. No paywall. Signup is a username, email, and password.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;React + Vite + TypeScript&lt;/li&gt;
&lt;li&gt;Supabase for database and auth (started on Firebase, migrated after the outage below)&lt;/li&gt;
&lt;li&gt;GitHub Pages for hosting (deployed from &lt;code&gt;docs/&lt;/code&gt;, not the repo root)&lt;/li&gt;
&lt;li&gt;No backend. All logic is client-side.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The two bugs that taught me the most
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Bug 1: The database disappeared.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I was on Firebase Firestore. One morning, every account was gone. Login failed. The app showed "user not found" for accounts that existed the day before. The Firestore API returned &lt;code&gt;404 NOT_FOUND: The database (default) does not exist for project&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It came back four hours later, untouched. Google had a backend issue. My data was fine. But for four hours, I thought I'd lost everything.&lt;/p&gt;

&lt;p&gt;That's when I migrated to Supabase. Postgres. Real backups. No Google Cloud 2FA requirement. The migration took a weekend, and now the database is mine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 2: Firestore rejected &lt;code&gt;undefined&lt;/code&gt;. Everywhere. Deeply.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before the migration, the bug that cost me the most time was this:&lt;br&gt;
Function setDoc() called with invalid data. Unsupported field value: undefined&lt;/p&gt;

&lt;p&gt;text&lt;/p&gt;

&lt;p&gt;Firestore refuses any document containing &lt;code&gt;undefined&lt;/code&gt; anywhere inside it. Not &lt;code&gt;null&lt;/code&gt; — &lt;code&gt;undefined&lt;/code&gt;. And when it fails, it doesn't tell you which field. Just the document path. If you're saving a nested object with 40 fields, you're searching for a needle in a haystack.&lt;/p&gt;

&lt;p&gt;The fix was a recursive cleaner routed through every write:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
js
function cleanForFirestore(obj) {
  if (Array.isArray(obj)) return obj.map(cleanForFirestore);
  if (obj instanceof Date) return obj;
  if (obj &amp;amp;&amp;amp; typeof obj === 'object') {
    const out = {};
    for (const k in obj) {
      if (obj[k] !== undefined) out[k] = cleanForFirestore(obj[k]);
    }
    return out;
  }
  return obj;
}
Then I wrapped setDoc, updateDoc, and addDoc in a safeFirestore helper that cleans the data before every write. One import change, and no write in the app can send undefined to Firestore again.

The lesson wasn't "fix the bug." It was "there is a class of bug, and you can kill the whole class with one wrapper."

The deploy lesson
The site broke for two days with this:

text
Failed to load module script: Expected a JavaScript-or-Wasm module script but the server responded with a MIME type of "application/octet-stream"
I was staring at the code. The problem was in GitHub Pages settings. It was serving my repo root — which has /src/main.tsx — instead of my built dist/ folder. The fix was to copy dist/ into a docs/ folder, point Pages at /docs, and stop letting it serve source.

The error message was the browser refusing to run a TypeScript file as JavaScript. Once you know that, it makes sense. The code was never broken. The deploy was.

What I'd like feedback on
Is the calorie calculation right? Mifflin-St Jeor for BMR, activity multiplier for TDEE, 300–500 deficit, floor at BMR. I want someone who actually knows nutrition math to sanity-check it. If the numbers are wrong, the app is wrong.

Does the onboarding flow make sense the first time you see it? Country → height/weight/age → fitness level → goal. Four steps. If it's confusing, I want to know where.

Does it break on your phone? I've tested on one device. I need more.

Does the meal plan respect your dietary preference? I fixed a bug where vegetarian users were getting turkey. If you set a diet and get a plan that violates it, that's a bug and I want to hear about it.

forkcount.github.io

I'm 13 and I built this by directing Google AI Studio, not by writing code by hand. So if something looks like a beginner mistake, it probably is. Tell me and I'll fix it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>supabase</category>
    </item>
    <item>
      <title>Read this.</title>
      <dc:creator>forkcount</dc:creator>
      <pubDate>Tue, 27 Oct 2026 11:49:33 +0000</pubDate>
      <link>https://dev.to/forkcount/read-this-14o7</link>
      <guid>https://dev.to/forkcount/read-this-14o7</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/koda2026/live-terminal-demos-watch-kavlr-block-destructive-commands-hyna-stage-spin-up-in-51s-5hlm" class="crayons-story__hidden-navigation-link"&gt;Live Terminal Demos: Watch KAVLR Block Destructive Commands &amp;amp; HYNA-Stage Spin Up in 5.1s 🐯🛡️&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/koda2026" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4089803%2F9aaca018-a27e-4e97-941a-2cfcc8fa553d.png" alt="koda2026 profile" class="crayons-avatar__image" width="800" height="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/koda2026" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Harun - solo dev 
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Harun - solo dev 
                
                
              
              &lt;div id="story-author-preview-content-4922302" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/koda2026" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4089803%2F9aaca018-a27e-4e97-941a-2cfcc8fa553d.png" class="crayons-avatar__image" alt="" width="800" height="800"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Harun - solo dev &lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/koda2026/live-terminal-demos-watch-kavlr-block-destructive-commands-hyna-stage-spin-up-in-51s-5hlm" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 26&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/koda2026/live-terminal-demos-watch-kavlr-block-destructive-commands-hyna-stage-spin-up-in-51s-5hlm" id="article-link-4922302"&gt;
          Live Terminal Demos: Watch KAVLR Block Destructive Commands &amp;amp; HYNA-Stage Spin Up in 5.1s 🐯🛡️
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag crayons-tag--filled  " href="/t/showdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;showdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/buildinpublic"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;buildinpublic&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/hackerstage"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;hackerstage&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/koda2026/live-terminal-demos-watch-kavlr-block-destructive-commands-hyna-stage-spin-up-in-51s-5hlm" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;16&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/koda2026/live-terminal-demos-watch-kavlr-block-destructive-commands-hyna-stage-spin-up-in-51s-5hlm#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              10&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            2 min read
          &lt;/small&gt;
          
          &lt;div class="only-sidebar-menu-item trusted-visible-block print-hidden align-center"&gt;
                
                    
                
          &lt;/div&gt;

        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>I'm 13. I Built a Free Calorie Tracker That Shows You the Math. No Card, No Trial, No Paywall.</title>
      <dc:creator>forkcount</dc:creator>
      <pubDate>Mon, 26 Oct 2026 16:17:32 +0000</pubDate>
      <link>https://dev.to/forkcount/im-13-i-built-a-free-calorie-tracker-that-shows-you-the-math-no-card-no-trial-no-paywall-5549</link>
      <guid>https://dev.to/forkcount/im-13-i-built-a-free-calorie-tracker-that-shows-you-the-math-no-card-no-trial-no-paywall-5549</guid>
      <description>&lt;p&gt;I built a calorie tracker. It's called ForkCount. It's free. There's no card, no trial, no "start your 7-day free trial" wall, and no premium tier hiding the barcode scanner behind $12.99/month.&lt;/p&gt;

&lt;p&gt;I'm 13. I can't write JavaScript from memory. I built it using Google AI Studio — describing what I wanted, testing what came back, fixing what broke. Two weeks of debugging. It's live now.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Shows you the math.&lt;/strong&gt; Most apps give you a number. ForkCount gives you the number and the formula behind it — your BMR, your TDEE, your activity multiplier, your goal adjustment. Every step is visible. If you want to know why your target is 1,400 and not 2,000, the app tells you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Calculates from your body, not an average.&lt;/strong&gt; Mifflin-St Jeor for BMR, Katch-McArdle if you know your body fat percentage. Then activity level, then goal. The target is yours, not a default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Logs food by typing.&lt;/strong&gt; "200g chicken and 150g rice" parses to two items with macros. No searching a database for each ingredient. Type it, log it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weekly meal plans that use your target.&lt;/strong&gt; Not a generic 2000 kcal plan. Your calorie target. Your dietary preference. If you're vegetarian, the plan is vegetarian.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tracks water, weight, and exercise.&lt;/strong&gt; Water in glasses, weight in trends, exercise with kcal burned.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runs in your phone browser.&lt;/strong&gt; No download. No app store. Open the link, sign up, done.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;No food photo scan (in progress)&lt;/li&gt;
&lt;li&gt;No barcode scanner (removed after it kept misfiring — it's coming back when it works)&lt;/li&gt;
&lt;li&gt;No Apple Health / Google Fit sync&lt;/li&gt;
&lt;li&gt;No verified nutrition data for anything not in the built-in list&lt;/li&gt;
&lt;li&gt;No premium tier, because there's no business model. It's just free.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;Every calorie app I tried wanted a card before it showed me anything useful. The math was always hidden. The target was always a black box. The "free" version was always a trial, or an ad-supported version of a paid app, or a version that locked the one feature you actually needed behind a subscription.&lt;/p&gt;

&lt;p&gt;So I built one that doesn't do that. The math is visible. The target is yours. The app is free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I'm posting
&lt;/h2&gt;

&lt;p&gt;I want to know if the calorie math is right. If you've tracked long enough to know your real TDEE, run the onboarding and compare. If the number ForkCount gives you is off by more than 100 kcal, tell me how and I'll fix it. If it's right, tell me that too — I need to know the calculator works before I trust it.&lt;/p&gt;

&lt;p&gt;I also want to know what's missing. What feature do you use in MyFitnessPal that you can't live without? What would make you switch? Not "add every feature" — what's the one thing?&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;&lt;a href="https://forkcount.github.io" rel="noopener noreferrer"&gt;https://forkcount.github.io&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Free. No card. No trial. Works on any phone browser. Two weeks old, so there will be bugs — but every bug I hear about gets fixed within a day.&lt;/p&gt;

&lt;p&gt;If you try it and something breaks, tell me what you were doing and what phone you're on. I read every message.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with React, Vite, Supabase, and a lot of patience. Deployed on GitHub Pages. If you're building something similar and want to know how the Supabase migration went, ask in the comments — I'll write it up.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>I Fixed 3 Bugs in My AI-Built Calorie Tracker Today. All Three Were "The AI Is Ignoring Me." None of Them Were the AI.</title>
      <dc:creator>forkcount</dc:creator>
      <pubDate>Mon, 26 Oct 2026 16:16:02 +0000</pubDate>
      <link>https://dev.to/forkcount/i-fixed-3-bugs-in-my-ai-built-calorie-tracker-today-all-three-were-the-ai-is-ignoring-me-none-1a57</link>
      <guid>https://dev.to/forkcount/i-fixed-3-bugs-in-my-ai-built-calorie-tracker-today-all-three-were-the-ai-is-ignoring-me-none-1a57</guid>
      <description>&lt;p&gt;I'm 13. I built a free calorie tracker called ForkCount using Google AI Studio. It runs on React + Vite, deployed to GitHub Pages, database on Supabase. I can't write JavaScript from memory. I build by describing what I want and debugging what comes back.&lt;/p&gt;

&lt;p&gt;Today I fixed three bugs that all felt like "the AI is ignoring my settings." Every one turned out to be a completely different problem. Here's what happened, and what I learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 1: The plan said "vegetarian" but served turkey
&lt;/h2&gt;

&lt;p&gt;I set my profile to vegetarian. Generated a 7-day meal plan. Day three: turkey breast.&lt;/p&gt;

&lt;p&gt;The plan generator was calling the AI with a prompt that said "make a meal plan" and nothing else. It never read &lt;code&gt;profile.dietary_preference&lt;/code&gt;. It never filtered the response. It asked for food and took what came back.&lt;/p&gt;

&lt;p&gt;The fix was two layers. First, pass the diet into the generation prompt:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
js
const prompt = `Generate a 7-day meal plan for a ${profile.dietary_preference} user. 
Do NOT include any meat, poultry, or fish if vegetarian.`
Second, verify the response before showing it:

js
const forbidden = ['turkey', 'chicken', 'beef', 'pork', 'lamb', 'fish']
if (profile.dietary_preference === 'vegetarian') {
  for (const meal of plan.meals) {
    for (const item of meal.ingredients) {
      if (forbidden.some(f =&amp;gt; item.toLowerCase().includes(f))) {
        // regenerate this meal
      }
    }
  }
}
Asking the AI is not enough. You have to check the answer.

Bug 2: The plan used 2000 kcal, not my actual target
Same root cause, different field. The plan generator had a hardcoded default of 2000 calories. My profile target was 1400. The plan ignored it.

The bug was subtle: the generator did read profile.target_calories, but the read happened before the profile finished loading from Supabase. So it got undefined, fell back to 2000, and generated the plan around that number.

The fix was to await the profile load before generating:

js
const { data: profile } = await supabase
  .from('profiles')
  .select('target_calories, dietary_preference')
  .eq('id', user.id)
  .single()

if (!profile) throw new Error('Profile not loaded')
if (!profile.target_calories) throw new Error('No target set')
No fallback. If the target is missing, fail loudly instead of quietly using 2000. Silent fallbacks are how you end up with plans that don't match the user.

Bug 3: The Diary showed one target, the Edit screen showed another
This one took the longest. The Diary screen showed a calorie target of 1580. The Edit Daily Calorie Target screen showed 1396. Both were reading from "the profile." Both were wrong in different ways.

Turns out the Diary was reading a cached value from localStorage. The Edit screen was reading from Supabase. The cache was never invalidated when the target changed. Two sources of truth. One updated, one didn't. Refresh didn't help because the localStorage cache survived the refresh.

The fix was to delete the localStorage cache entirely and always read from Supabase on mount. One source of truth. Cache invalidation bugs are almost always fixed by removing the cache.

What I actually learned today
Three bugs, three patterns:

The AI does what you ask, not what you meant. "Make a plan" is not "make a vegetarian plan." You have to say the diet in the prompt, and then verify the answer. The AI will happily hand you turkey if you don't say no.

Silent fallbacks hide bugs. A ?? 2000 fallback looked harmless. It made the app silently wrong for months instead of loudly broken for 5 minutes. Fail loud. Fail early. A red error message is better than a wrong number.

Two sources of truth always disagree. If the Diary reads localStorage and the Edit screen reads Supabase, they will diverge. Not "might diverge." Will. The fix is one source. Always.

None of these were "the AI is broken" bugs. They were "the code is doing exactly what it says, and what it says is wrong" bugs. The AI wrote the code correctly. The code did the wrong thing correctly.

That's the part nobody warns you about with AI-assisted development. The AI doesn't make the same mistakes a human makes. It makes different mistakes — cleaner ones, more consistent ones, the kind that hide behind passing builds.

The fix is the same as with human-written code: read what it actually does, not what you think it does. Add a log. Check the value. Find the layer where the bug lives.

Three bugs down. No idea how many left.

ForkCount is a free calorie and macro tracker with no card, no trial, and no paywall. It calculates your BMR and TDEE from your own stats and shows you the maths. If you've ever wanted a tracker that doesn't hide the calorie math behind a subscription, try it at forkcount.github.io. It's two weeks old, so it's rough around the edges — but it works, and every bug gets fixed within a day.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>showdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>I Spent 3 Days Debugging a Deploy That Was Never Deploying. Here's the Checklist That Fixed It.</title>
      <dc:creator>forkcount</dc:creator>
      <pubDate>Sun, 18 Oct 2026 14:55:34 +0000</pubDate>
      <link>https://dev.to/forkcount/i-spent-3-days-debugging-a-deploy-that-was-never-deploying-heres-the-checklist-that-fixed-it-1mm9</link>
      <guid>https://dev.to/forkcount/i-spent-3-days-debugging-a-deploy-that-was-never-deploying-heres-the-checklist-that-fixed-it-1mm9</guid>
      <description>&lt;p&gt;Three days. That's how long I spent pushing fixes to a GitHub Pages site that never changed.&lt;/p&gt;

&lt;p&gt;Every push succeeded. Every workflow showed green. Every fix worked in my dev environment. And every single time I opened the live URL, the same console error stared back at me:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Failed to load module script: Expected a JavaScript-or-Wasm module script but the server responded with a MIME type of "application/octet-stream"&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;I'm 13. I built a calorie tracker called ForkCount using AI Studio. The app worked. The deploy didn't. And the worst part wasn't the error — it was that I couldn't tell whether my fixes were failing or whether they were never reaching the browser at all.&lt;/p&gt;

&lt;p&gt;Day one, I blamed the build. I rebuilt locally, checked the output, saw that the script tag pointed to a real &lt;code&gt;.js&lt;/code&gt; file. The build was fine.&lt;/p&gt;

&lt;p&gt;Day two, I blamed GitHub Actions. The workflow looked correct. Green checkmarks everywhere. But the live site was still serving source files instead of the built ones. I didn't understand how both could be true at once.&lt;/p&gt;

&lt;p&gt;Day three, I opened my own repo and looked at the &lt;code&gt;docs/&lt;/code&gt; folder. It had assets, it had a &lt;code&gt;.nojekyll&lt;/code&gt; file, it had a manifest. It did not have an &lt;code&gt;index.html&lt;/code&gt;. The folder I was deploying from was empty of the one file that mattered. GitHub Pages was serving a 404, which fell back to the repo root, which contained my raw source files with &lt;code&gt;/src/main.tsx&lt;/code&gt; — a file no browser can execute.&lt;/p&gt;

&lt;p&gt;That was the bug. Not the code. Not the workflow. A missing file in a folder I'd looked at ten times without seeing what was absent.&lt;/p&gt;

&lt;p&gt;The fix took four commands. Build, copy, add the missing file, push. Three days of debugging ended with a solution so simple I felt stupid for not finding it sooner.&lt;/p&gt;

&lt;p&gt;But here's what I actually learned, and it has nothing to do with GitHub Pages:&lt;/p&gt;

&lt;p&gt;When something breaks and the fix doesn't work, the first question isn't "what's wrong with the fix?" It's "did the fix actually arrive?" I spent two days debugging code that was never running. Every prompt I sent to my AI assistant went to the source. The source was fine. The source was never the problem.&lt;/p&gt;

&lt;p&gt;If you're building something and it keeps failing after every fix, check the layer between your code and your user. Look at what's actually being served, not what you think you deployed. View source. Check the hash. Open the folder. Count the files.&lt;/p&gt;

&lt;p&gt;The AI can write the code. It can't see your screen. When something breaks, you have to be the one who notices where the break is.&lt;/p&gt;

&lt;p&gt;ForkCount is live now at forkcount.github.io. Free, no card, no trial. I built it because every calorie app I tried hid the math behind a paywall. It still has bugs. But the deploy works, and I know how to check it.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>github</category>
      <category>vite</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I'm 13 and I built a free calorie tracker. It's live, and here's everything that broke.</title>
      <dc:creator>forkcount</dc:creator>
      <pubDate>Fri, 16 Oct 2026 12:24:59 +0000</pubDate>
      <link>https://dev.to/forkcount/im-13-and-i-built-a-free-calorie-tracker-its-live-and-heres-everything-that-broke-1joo</link>
      <guid>https://dev.to/forkcount/im-13-and-i-built-a-free-calorie-tracker-its-live-and-heres-everything-that-broke-1joo</guid>
      <description>&lt;p&gt;Every calorie app I tried wanted my email and a card before it showed me anything. So I built one that doesn't.&lt;/p&gt;

&lt;p&gt;I don't write code by hand. I describe what I want to Google AI Studio and it writes it. Then I test it on my phone, find what's broken, describe the fix. That's the loop. Two weeks of it.&lt;/p&gt;

&lt;p&gt;The app is called ForkCount. forkcount.github.io&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;It asks your height, weight, age, gender, and activity level. Then it works out your BMR (Mifflin-St Jeor), your TDEE, and a calorie target that keeps you above your BMR floor. If you try to set a target below that line, it shows you the numbers and explains why.&lt;/p&gt;

&lt;p&gt;The rest is a food diary, water tracker, weight log, exercise log, weekly meal plan generator, and a community feed. You type "200g chicken and 150g rice" and it parses the food. No card. No trial. No paywall. No email — just a username and password.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;React + Vite + TypeScript&lt;/li&gt;
&lt;li&gt;Firebase Firestore&lt;/li&gt;
&lt;li&gt;Firebase Auth&lt;/li&gt;
&lt;li&gt;GitHub Pages, deployed from the &lt;code&gt;docs/&lt;/code&gt; folder&lt;/li&gt;
&lt;li&gt;No backend. All logic is client-side.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The bug that cost me the most time
&lt;/h2&gt;

&lt;p&gt;Firestore rejects any document that contains an &lt;code&gt;undefined&lt;/code&gt; value anywhere inside it. Not &lt;code&gt;null&lt;/code&gt; — &lt;code&gt;undefined&lt;/code&gt;. And when it fails, it doesn't tell you which field. It just says:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Function setDoc() called with invalid data. Unsupported field value: undefined
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;If you're saving a nested object with 40 fields, you're searching for a needle in a haystack. I spent days on this. Fix one field, redeploy, same error for a different one.&lt;/p&gt;

&lt;p&gt;The fix was a recursive cleaner, routed through every Firestore write:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function cleanForFirestore(obj) {
  if (Array.isArray(obj)) return obj.map(cleanForFirestore);
  if (obj instanceof Date) return obj;
  if (obj &amp;amp;&amp;amp; typeof obj === 'object') {
    const out = {};
    for (const k in obj) {
      if (obj[k] !== undefined) out[k] = cleanForFirestore(obj[k]);
    }
    return out;
  }
  return obj;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Then I wrapped setDoc, updateDoc, and addDoc in a helper that cleans every write. One import change, and no write in the app can send undefined to Firestore again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second bug: GitHub Pages was serving my source files
&lt;/h2&gt;

&lt;p&gt;The site broke with this:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Failed to load module script: Expected a JavaScript-or-Wasm module script but the server responded with a MIME type of "application/octet-stream"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;I stared at the code for two days. The problem was in GitHub Pages settings. It was serving my repo root — which has /src/main.tsx — instead of my built dist/ folder. The fix was to copy dist/ into a docs/ folder, point Pages at /docs, and stop letting it serve source.&lt;/p&gt;

&lt;p&gt;The error was the browser refusing to run a TypeScript file as JavaScript. Once I knew that, it made sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd like feedback on
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Does the onboarding flow make sense the first time you see it?&lt;/li&gt;
&lt;li&gt;Is the calorie calculation right? Mifflin-St Jeor for BMR, activity multiplier for TDEE, 300–500 deficit, floor at BMR.&lt;/li&gt;
&lt;li&gt;Does it break on your phone?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm 13 and I built this by directing AI Studio, not by writing code by hand. If something looks like a beginner mistake, it probably is. Tell me and I'll fix it.&lt;/p&gt;

&lt;p&gt;forkcount.github.io&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>firebase</category>
    </item>
    <item>
      <title>I built a free calorie tracker with a metabolic safety floor. No card, no email, no paywall.</title>
      <dc:creator>forkcount</dc:creator>
      <pubDate>Fri, 09 Oct 2026 15:04:37 +0000</pubDate>
      <link>https://dev.to/forkcount/i-built-a-free-calorie-tracker-with-a-metabolic-safety-floor-no-card-no-email-no-paywall-4gh6</link>
      <guid>https://dev.to/forkcount/i-built-a-free-calorie-tracker-with-a-metabolic-safety-floor-no-card-no-email-no-paywall-4gh6</guid>
      <description>&lt;p&gt;Every calorie app I tried had the same problem: it let you set a target of 1,200 calories or lower and called it a plan. Some went lower. That's how people end up in the binge-starve cycle — you eat 800, your body fights back, you eat 4,000, you hate yourself, repeat.&lt;/p&gt;

&lt;p&gt;So I built one that refuses.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;ForkCount asks your height, weight, age, gender, and activity level. Then it calculates your BMR, your TDEE, and a target that keeps you above the BMR floor. If you try to set a target below that line, it won't let you.&lt;/p&gt;

&lt;p&gt;The rest is a food diary, water tracker, weight log, exercise log, weekly meal plan generator, and a community feed. You type "200g chicken and 150g rice" and it parses the food.&lt;/p&gt;

&lt;p&gt;Free. No card. No trial. No paywall. Signup is a username and password — no email required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;React + Vite + TypeScript&lt;/li&gt;
&lt;li&gt;Firebase Firestore&lt;/li&gt;
&lt;li&gt;Firebase Auth&lt;/li&gt;
&lt;li&gt;GitHub Pages&lt;/li&gt;
&lt;li&gt;No backend&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The hardest part: Firestore rejects undefined
&lt;/h2&gt;

&lt;p&gt;The error was: Function setDoc() called with invalid data. Unsupported field value: undefined&lt;/p&gt;

&lt;p&gt;Firestore refuses any document that contains an undefined value anywhere inside it. When it fails, it doesn't tell you which field. So if you're saving a nested object with 40 fields, you're searching for a needle in a haystack.&lt;/p&gt;

&lt;p&gt;The fix was a recursive cleaner routed through every write:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function cleanForFirestore(obj) {
  if (Array.isArray(obj)) return obj.map(cleanForFirestore);
  if (obj instanceof Date) return obj;
  if (obj &amp;amp;&amp;amp; typeof obj === 'object') {
    const out = {};
    for (const k in obj) {
      if (obj[k] !== undefined) out[k] = cleanForFirestore(obj[k]);
    }
    return out;
  }
  return obj;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Then I wrapped setDoc, updateDoc, and addDoc so every write cleans the data first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other lesson: deploy settings, not code
&lt;/h2&gt;

&lt;p&gt;The site broke with: Failed to load module script: Expected a JavaScript-or-Wasm module script but the server responded with a MIME type of "application/octet-stream"&lt;/p&gt;

&lt;p&gt;I was staring at the code. The problem was GitHub Pages settings. It was serving my repo root — which has /src/main.tsx — instead of my built dist/ folder. The fix was to copy dist/ into a docs/ folder and point Pages at /docs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd like feedback on
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Does the onboarding flow make sense the first time you see it?&lt;/li&gt;
&lt;li&gt;Is the calorie calculation right? Mifflin-St Jeor for BMR, activity multiplier for TDEE, 300–500 deficit, floor at BMR.&lt;/li&gt;
&lt;li&gt;Does it break on your phone?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;forkcount.github.io&lt;/p&gt;

&lt;p&gt;I'm 13 and I built this by directing Google AI Studio, not by writing code by hand. If something looks like a beginner mistake, it probably is. Tell me and I'll fix it.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>firebase</category>
    </item>
    <item>
      <title>I built a free calorie tracker with a metabolic safety floor. No card, no email, no paywall.</title>
      <dc:creator>forkcount</dc:creator>
      <pubDate>Fri, 09 Oct 2026 14:53:23 +0000</pubDate>
      <link>https://dev.to/forkcount/i-built-a-free-calorie-tracker-with-a-metabolic-safety-floor-no-card-no-email-no-paywall-2a65</link>
      <guid>https://dev.to/forkcount/i-built-a-free-calorie-tracker-with-a-metabolic-safety-floor-no-card-no-email-no-paywall-2a65</guid>
      <description>&lt;p&gt;Every calorie app I tried had the same problem: it let you set a target of 1,200 calories or lower and called it a plan. Some went lower. That's how people end up in the binge-starve cycle — you eat 800, your body fights back, you eat 4,000, you hate yourself, repeat.&lt;/p&gt;

&lt;p&gt;So I built one that refuses.&lt;/p&gt;

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

&lt;p&gt;ForkCount asks your height, weight, age, gender, and activity level. Then it calculates your BMR (Mifflin-St Jeor), your TDEE (BMR × activity multiplier), and a target that keeps you above the BMR floor. If you try to set a target below that line, it won't let you. You can override it in the settings, but the app tells you what you're doing and shows the maths.&lt;/p&gt;

&lt;p&gt;The rest is a food diary, water tracker, weight log, exercise log, weekly meal plan generator, and a community feed. You type "200g chicken and 150g rice" and it parses the food. There's a barcode scanner, weight tracking, exercise logging with a kcal estimate.&lt;/p&gt;

&lt;p&gt;Free. No card. No trial. No paywall. Signup is a username and password — no email required.&lt;/p&gt;

&lt;p&gt;Stack&lt;/p&gt;

&lt;p&gt;React + Vite + TypeScript&lt;/p&gt;

&lt;p&gt;Firebase Firestore for the database&lt;/p&gt;

&lt;p&gt;Firebase Auth for accounts&lt;/p&gt;

&lt;p&gt;GitHub Pages for hosting (deployed from the docs/ folder, not the repo root)&lt;/p&gt;

&lt;p&gt;No backend. All logic is client-side.&lt;/p&gt;

&lt;p&gt;The hardest part: Firestore rejects undefined&lt;/p&gt;

&lt;p&gt;The bug that cost me the most time was this:&lt;/p&gt;

&lt;p&gt;text&lt;br&gt;
Function setDoc() called with invalid data. Unsupported field value: undefined&lt;br&gt;
Firestore refuses any document that contains an undefined value anywhere inside it. Not null — undefined. And when it fails, it doesn't tell you which field, just the document path. So if you're saving a nested object with 40 fields, you're searching for a needle in a haystack.&lt;/p&gt;

&lt;p&gt;The fix was a recursive cleaner, routed through every write in the app:&lt;/p&gt;

&lt;p&gt;js&lt;br&gt;
function cleanForFirestore(obj) {&lt;br&gt;
  if (Array.isArray(obj)) return obj.map(cleanForFirestore);&lt;br&gt;
  if (obj instanceof Date) return obj;&lt;br&gt;
  if (obj &amp;amp;&amp;amp; typeof obj === 'object') {&lt;br&gt;
    const out = {};&lt;br&gt;
    for (const k in obj) {&lt;br&gt;
      if (obj[k] !== undefined) out[k] = cleanForFirestore(obj[k]);&lt;br&gt;
    }&lt;br&gt;
    return out;&lt;br&gt;
  }&lt;br&gt;
  return obj;&lt;br&gt;
}&lt;br&gt;
Then I wrapped setDoc, updateDoc, and addDoc in a safeFirestore helper that cleans the data before every write. One import change, and no write in the app can send undefined to Firestore again.&lt;/p&gt;

&lt;p&gt;The other lesson: the deploy settings, not the code&lt;/p&gt;

&lt;p&gt;The site broke for two days with this:&lt;/p&gt;

&lt;p&gt;text&lt;br&gt;
Failed to load module script: Expected a JavaScript-or-Wasm module script but the server responded with a MIME type of "application/octet-stream"&lt;br&gt;
I was staring at the code. The problem was in GitHub Pages settings. It was serving my repo root — which has /src/main.tsx — instead of my built dist/ folder. The fix was to copy dist/ into a docs/ folder, point Pages at /docs, and stop letting it serve source.&lt;/p&gt;

&lt;p&gt;The error message was the browser refusing to run a TypeScript file as JavaScript. Once you know that, it makes sense.&lt;/p&gt;

&lt;p&gt;What I'd like feedback on&lt;/p&gt;

&lt;p&gt;Does the onboarding flow make sense the first time you see it? It's country → height/weight/age → fitness level → goal.&lt;/p&gt;

&lt;p&gt;Is the calorie calculation right? Mifflin-St Jeor for BMR, activity multiplier for TDEE, 300–500 deficit, floor at BMR. I want someone who actually knows nutrition math to sanity-check it.&lt;/p&gt;

&lt;p&gt;Does it break on your phone? I've tested on one device and I need more.&lt;/p&gt;

&lt;p&gt;forkcount.github.io&lt;/p&gt;

&lt;p&gt;I'm 13 and I built this by directing Google AI Studio, not by writing code by hand. So if something looks like a beginner mistake, it probably is. Tell me and I'll fix it.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>firebase</category>
    </item>
  </channel>
</rss>
