UW FUEL
A nutrition and fitness app for UW students. It turns the real campus dining menus into meal plans that fit your budget, logs workouts against a 1,300+ exercise library, and runs on API keys each student owns.
A one-page prototype, then a real app
UW FUEL went through two versions in about seven months. The prototype and the current scraper are in private repos, so those entries show dates without links.
- Prototype · UW Macros Planner
-
Mar 8, 2026First version
A Python scraper (Playwright against UW's dining order site, scheduled with APScheduler) that wrote menus into Firebase, with one large
index.htmlas the whole front end. -
Mar – May 2026Iterating on one page
24 commits through May 4, and 20 of them touch that single HTML file. The rebuild split it into a real app.
- UW FUEL · Next.js
-
Jun 23, 2026Rebuilt as UW FUEL
Started over in Next.js and TypeScript with three route groups (public pages, onboarding, and the signed-in app), typed Firebase hooks, the meal planner, the workout logger, and progress charts. A second commit that day sped up the workout log.
-
Jul 2, 2026Setup guide and dining-plan tracking
Added a step-by-step setup guide inside the app and tracking against your dining plan balance. Fixed a production bug and some viewport problems later the same day.
-
Jul 10, 2026Dorm pantry
A pantry tab for food you keep in your room: search ingredients, scan items with your camera, and get recipe ideas from what you already have.
-
Sep 12, 2026My Day, new sign-up, and a stricter planner
The biggest change since launch. A new Today view lays out the day's meals on a route map, login and signup got reworked, and the planner gained
fitDay.ts, the deterministic pass that enforces the budget. Two more commits that day added drag-to-reorder and meal ratings to plans, merged duplicate dining cards, and fixed the tutorial getting clipped. -
Sep 14, 2026Plans that follow your class schedule
Meal planning can now follow your class schedule, plus an accessibility pass on the interface. A follow-up commit made plans reusable.
-
Oct 5, 2026Scraper moved to GitHub Actions
Split the dining scraper into its own repo. It runs every night at midnight Pacific on GitHub Actions, with two cron entries so the time stays right through daylight saving changes.
Eating well on a meal plan is harder than it should be
Dining dollars run out before the month does. You never quite know what's open, what fits your macros, or what something costs until you're in line. And general nutrition apps know nothing about UW's dining halls, so you end up guessing at every entry.
UW FUEL uses the real UW Housing and Food Services menu, with real prices and real hours, so the easy choice and the smart choice can be the same one. I also wanted it to cost me nothing per user, which shaped most of the architecture.
Public data in one place, personal data wherever the student wants it
A Python scraper pulls the dining menus every night and writes them into a shared Firebase project, next to the exercise library. Everyone reads the same public data and nobody can write to it. Personal data (food logs, plans, workouts, settings) goes under the signed-in user's own node, and security rules keep each user inside their own subtree. A student who wants full ownership can connect their own Firebase project instead.
AI requests go straight from the browser to Cohere using the student's own key. There's no server of mine in the middle, so there's no shared quota and nothing for me to pay per request. The key is stored under the user's authenticated database node rather than in localStorage, which keeps it away from anything a cross-site script could read.
→
read-only
The maintainer-owned part of the system only ever holds public data.
The model suggests meals, and code enforces the budget
The planner's one rule: find food that fits the budget, and never bend the budget to fit a plan. The model gets your goal, your dietary preferences, your meal count, your hard cap, and today's menu with exact prices, and it's told to pick only from that list. Then a deterministic pass in fitDay.ts checks the result against real prices, because a prompt alone can't guarantee arithmetic.
While the plan is over budget, it takes the most expensive meal and tries the cheapest fix first. Add-ons go before anything else. Next it looks for a swap: the best-ranked item that fits the remaining room and is open at that meal's time. If nothing fits, that meal gets dropped. Once the plan is under the cap, it spends what's left on sides from the same dining location until you're close to your calorie target.
for (let pass = 0; pass < MAX_PASSES && totalCost() > budget + 1e-9 && drafts.length > 0; pass++) { let idx = 0; drafts.forEach((d, i) => { if (draftCost(d) > draftCost(drafts[idx])) idx = i; }); const d = drafts[idx]; if (d.addOns.length > 0) { // cheapest fix first used.delete(d.addOns.pop()!.key); continue; } const room = budget - (totalCost() - draftCost(d)); const swap = bestOf(input.pool, score, (item) => item.price <= room && isOpen(item, clockToMinute(d.time))); if (swap) { d.main = swap; swaps++; continue; } // swap for something that fits drafts.splice(idx, 1); // nothing fits: drop the meal droppedMeals++; }
Simplified from fitDay.ts. The real version also tracks used items and has a fallback swap rule.
Whatever it changes, it says so. You'll see notes like "Swapped 2 pricier picks to stay under $12.00," and if the cap is too low to reach your calories it tells you how far short you are and suggests raising the cap. It never rescales a price to make a plan look like it fits.
Ratings that fade, and a penalty for repeats
Once plans could be rated, the planner needed to use those ratings without getting stuck on them. preferences.ts turns your history into a score for each food:
"Loved" counts +1.0, "good" +0.4, and "bad" −1.2, and each rating loses half its weight every 45 days. A meal you hated in September doesn't haunt you forever.
Anything you ate in the last three days takes a growing penalty, capped at 0.9, so the same bowl doesn't show up four days running.
Food you keep skipping over a 14-day window slides down the list, up to a cap of 0.6.
Dining locations you go to most get a small boost, up to 0.3, so plans lean toward your usual spots.
Time zones, offline writes, and other details
- Open hours in Pacific time. Open, closing soon (45 minutes out), and closed are computed against Pacific time, so a dining hall never shows as open because someone's laptop is set to another time zone.
- An offline outbox. Writes made without a connection queue up in local storage (up to 300) and replay when the connection comes back, so logging lunch in a dead-zone basement still works.
- Realtime Database quirks. Firebase drops empty arrays and sometimes returns arrays as objects. Every typed hook normalizes those shapes so screens don't crash on a half-filled record.
- A real PWA. It installs to your home screen and uses a service worker to schedule meal reminders.
- No flash of the wrong theme. A small script sets light or dark mode before first paint.
Where it's going
Sign-up, onboarding, dining, planning, logging, and progress are done and in use. Next up are richer progress analytics, smarter routing between classes and dining halls, and demos for more of the exercise library.
AI features run on your own Cohere key.