Solo operator (design, build, run)

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
At a glance

System.

Interactive snapshot of how this project actually works.

01
Plan
Source of truth · markdown upload, strict-first parser, AI fallback
5 × 5 @ 80%
Squat
5 × 5 @ 75%
Bench
3 × 5 @ 85%
Deadlift
3 × 8 @ 70%
OHP
Hover any prescription · last week's best set reveals
02
Logger
Thin actuals-overlay · long-press to group supersets · per-set rest countdown
Set 1 185 × 5 2:00
Set 2 185 × 5 2:15
Set 3 185 × _ resting
Set 4
Voice input · prefill from last performance
03
Stats
Adherence, volume, PRs · all derived from actuals overlaid on plan
94%
Adherence · 6 wks
+8%
Volume · block
Recs surface fires when drift > threshold
DB column → UI label
tldr One-line headline
drift_score Hairline bar + numeric
snooze_count ≥ 2 FORCED DECISION chip
snoozed_until REVIVES IN Xd Yh
confidence conf X%
status Open / Snoozed / Acted / Dismissed
Read

Depth 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-plan Edge 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:

  • AGING chip on any Open rec older than 14 days
  • FORCED DECISION chip once it’s been snoozed twice

Flow

PlanUpload  →  Plan  →  Logger  →  Stats
  • PlanUpload — strict parse, AI fallback via the parse-plan Edge 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_active flag 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 AccessGate plus a UserPicker over app_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 columnWhat the user sees on /recommendations
tldrOne-line headline per row
domainUppercased pill (FITNESS / NUTRITION)
created_at”just now” / “Xh ago” / “Xd ago”
drift_score (0..1)Hairline bar + 2-decimal numeric
confidenceconf X%
statusSection grouping: Open / Snoozed / Recent decisions
snoozed_untilREVIVES IN Xd Yh / REVIVES ANY TIME NOW
snooze_count >= 2Red FORCED DECISION chip
created_at age > 14d (+ status=open)Red AGING chip
idFirst 8 chars, monospace