LIFTLOG — plan-driven workout logger I open before every set.
A training log I actually open mid-set, not after the session
The plan is the source of truth, not the log — which flips most logger UX and is what makes the recommendation surface defensible.
Open live app ↗System.
Interactive snapshot of how this project actually works.
tldr One-line headlinedrift_score Hairline bar + numericsnooze_count ≥ 2 FORCED DECISION chipsnoozed_until REVIVES IN Xd Yhconfidence conf X%status Open / Snoozed / Acted / DismissedDepth selector.
Brief · what it is. Approach · how it works. Deep · why the calls.
A workout logger for someone who already has a written training program and just wants to execute it. Upload the plan once. Open the app, see today’s prescription, log actuals as you go. Adherence and volume come out the other end as graphs you can argue with. Installs to your phone like a native app via PWA.
Tier 2
I had to be honest about why every other logger I’d tried got abandoned: I was being asked to reconstruct the plan inside the app, set by set, instead of executing against one.
So the spine here is inverted. A written training block — markdown, 16 weeks, blocks × days × exercises × week-by-week prescriptions — gets uploaded once, parsed into a structured plan. From then on every session is a thin actuals-overlay against that source-of-truth.
The non-obvious call: the parser is strict-first markdown with a
parse-planEdge Function as the AI fallback. Not AI-first.
Strict parsing fails loudly and forces the plan author to clean up their own document. AI is only the escape hatch when the table shape is genuinely ambiguous.
That ordering keeps the data clean enough that the analysis layer (Stats, Recs) has something it can actually reason against.
Tier 3
Schema — small on purpose
Each training block uploaded once as markdown, parsed into a structured plan. Every Logger session is a thin actuals-overlay against it.
The Plan view lights up the last completed week’s best actual set inline against the prescription — load that day, the lift you hit yesterday is right there.
The movement library is per-user — default_metrics, primary_metric, default_rest_seconds — so the Logger can swap a metric column (reps ↔ time ↔ distance) without inventing UI state.
The Recs surface is filtered to fitness and nutrition at the frontend and at the row level. Every recommendation carries:
- A one-line
tldr - A drift bar (0..1)
- A confidence score
- A status that flips between Open, Snoozed, Acted, Dismissed
Two visual signals lean on the rx primitives without explaining them:
AGINGchip on any Open rec older than 14 daysFORCED DECISIONchip once it’s been snoozed twice
Flow
PlanUpload → Plan → Logger → Stats
- PlanUpload — strict parse, AI fallback via the
parse-planEdge Function, adopt-from-other-user shortcut - Plan — 16-week grid with editable cells and last-actuals overlay
- Logger — long-press to group sets into supersets, per-set rest countdown, session stopwatch, voice input, prefill from last performance
- Stats — adherence, volume, PRs
The PWA wrapper matters more than it looks. manifest + apple-touch-icon + viewport-fit cover means the app installs to the home screen and behaves like a native logger — which is what made the friction-to-log low enough that I keep it open through the set instead of journaling after.
What’s deferred
- Wearable auto-import — conditioning + zone-2 work go through an activity logger with manual HR fields. Auto-import is deferred on purpose; the manual flow has to prove valuable before paying the HealthKit/OAuth cost.
- Plan-edit history — versioned by an
is_activeflag plus a per-migration backup table (plans_bk_20260515,recommendations_bk_v0_20260515). The cheap version of point-in-time recovery. Proper history is deferred until there’s a real reason to diff across blocks. - Multi-tenant — access is a shared-password
AccessGateplus aUserPickeroverapp_users. Single-tenant by construction. RLS is incidental. Multi-user wouldn’t be a feature flag.
Where this fits
The Recs surface is the fitness door of a cross-domain recommendation pattern I run on myself. The pattern itself — storage routing per domain, bounded snooze, forced counter-thesis, action-rate gate, and a corpus I curate by hand — is documented on /operating-model. LIFTLOG is where the fitness dispositions get made.
DB column → UI label
The lookup peer readers actually want.
recommendations column | What the user sees on /recommendations |
|---|---|
tldr | One-line headline per row |
domain | Uppercased pill (FITNESS / NUTRITION) |
created_at | ”just now” / “Xh ago” / “Xd ago” |
drift_score (0..1) | Hairline bar + 2-decimal numeric |
confidence | conf X% |
status | Section grouping: Open / Snoozed / Recent decisions |
snoozed_until | REVIVES IN Xd Yh / REVIVES ANY TIME NOW |
snooze_count >= 2 | Red FORCED DECISION chip |
created_at age > 14d (+ status=open) | Red AGING chip |
id | First 8 chars, monospace |