A single-purpose weight-tracking PWA for cut / bulk cycles. Log a weigh-in in one tap and judge progress by the 7-day rolling average, never the noisy daily number. Four screens, each with one job — where am I, what if, what happened, the rules — over an ink-black "Neon" theme.
On Android it also reads your daily calories from MyFitnessPal (via Health Connect) and turns the weight trend plus intake into an adaptive-TDEE maintenance estimate and a calorie target for your weekly goal.
Ships as a web PWA and as a native Android app via Capacitor.
The accent follows your current phase — cyan cutting, magenta bulking, a calmer steel while holding — cross-fading on a phase change with a one-shot shimmer across the coloured chrome.
- Today — where am I. The 7-day average and week-over-week, a rate bar against your weekly target, a Mon–Sun strip of this week's weigh-ins (today ringed), 14-day / rate / last-week stats, a per-week change strip, and — on Android — the implied energy balance from the trend.
- Trends — what if. The Reach card sits up top: set a target weight to get a date, or a date to get a weight, and it drives the dashed projection on the chart below. Weekly averages with a phase-scoped line of best fit; a window control (8W / 3M / 6M / ALL, or anchored to the current phase / last deload / last maintenance) that also sets the projection's slope; and change, fit-slope and R².
- History — what happened. Weekly averages grouped into phase cards, each showing that phase's total weight change and average rate, expandable to the individual days. Deload and maintenance weeks are tagged in place.
- Setup — the rules. Current phase (Cut / Bulk / Maintain) — changing it is staged behind a confirm with an undo, and commits by cross-fading the app's accent to match; one-tap Deload / Maintenance week markers that don't reset the phase; weekly target and week counter; units; the Health Connect connection; and your data totals.
- React + Vite + TypeScript, no router — four screens (
Today,Trends,History,Setup) switch on a singlescreenfield in a React Context +useReducerstore. - Supabase (Postgres + email magic-link auth) for storage, with a
localStoragecache and an offline write queue in front of it — logging a weigh-in works with no network and syncs when back online. - Capacitor wraps the same web build as a native Android app.
- Health Connect (Android only) through a small custom Kotlin plugin
(
android/app/src/main/java/com/blurryq/weighttracker/HealthConnectPlugin.kt) reads MyFitnessPal's daily calorie totals; the energy-balance math is insrc/lib/energy.ts. - Charts are hand-rolled inline SVG (
src/lib/chartGeometry.ts+src/components/chart/) — no charting library.
src/
lib/ pure, unit-tested math: rolling averages, phase spans, least-squares fit,
the bidirectional Reach solver, chart geometry, adaptive-TDEE energy math
store/ state shape + reducer + the AppContext that wires in persistence/sync
data/ Supabase client, offline queue, sync, auth gate, Health Connect bridge
components/ shared UI (nav, chart, entry sheet, ui primitives)
screens/ Today, Trends, History, Setup
tests/ vitest, run against a fixture of 317 real weigh-ins (tests/fixtures/weight-data.ts)
supabase/
migrations/ 0001 schema (entries, phase_log, settings + RLS), 0004 daily_nutrition,
0006-0007 Trends' phase-anchored window mode, 0008 drops dead columns
android/ Capacitor-generated native project
npm install
cp .env.example .env.local # fill in your Supabase project URL + anon key
npm run devWithout Supabase credentials the app still runs — every src/data/api.ts call checks
supabaseConfigured and no-ops, so you get a fully local, offline-only session. Fresh installs
boot with no entries (log a weigh-in to get started).
Run the migrations in supabase/migrations/ in order via the Supabase SQL Editor (or the
Supabase CLI once you're linked to a project). 0001_init.sql creates the core schema;
0004_nutrition.sql adds the daily_nutrition table for the calories feature; 0006 and
0007_trend_window_mode_maintain.sql add Trends' phase-anchored window mode (weeks /
phaseStart / lastDeload / lastMaintain); 0008_drop_dead_columns.sql drops two columns
the app never ended up reading or writing (settings.trend_horizon, daily_nutrition.source).
npm test # vitest — pure-function tests (math, chart geometry, energy, reducer) vs. the real 317-entry fixture
npm run lint # oxlint
npm run buildnpm run build
npx cap sync android
cd android && ./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apkNeeds a JDK (not just a JRE) and the Android SDK on $ANDROID_HOME. The debug APK is unsigned —
fine for sideloading to your own device, but a release build needs a signing key before wider
distribution.
The calories feature needs the READ_NUTRITION Health Connect permission, requested from the
Setup screen's "Calories" section. On web builds that section just shows an "Android only" note.
Magic-link sign-in on native builds redirects through a custom weighttracker://login-callback
URL scheme (see src/data/AuthGate.tsx and the intent-filter in
android/app/src/main/AndroidManifest.xml) so the email link opens the app instead of a browser.
That URL also needs to be added to the Supabase project's Authentication → URL Configuration →
Redirect URLs allow-list.