← All work

Liftlog and Corner

A training log and a heavy bag coach for my phone, built to work together and run on my home server.

Status
active
Period
October 2026
Role
Sole developer
Two phone screens side by side: Liftlog's week view of a 10-week programme, with completed sessions, and Corner's list of bag workouts with rounds and durations.

What it does

Liftlog holds my training programme and the record of following it. The programme (sessions, phases, progression rules, milestones) is written in a YAML file and loaded in one go. Each session pre-fills last time’s numbers, suggests the next load, runs a rest timer, and rounds the suggestion to the plates and dumbbells at wherever I’m training. It also tracks bodyweight and waist as a smoothed trend with a forecast, and food against daily targets.

Corner coaches heavy bag rounds. It rings the bells, warns before the end of each round, and calls combinations in a natural recorded voice, with an optional metronome. Workouts are YAML too, and each one cites where its drills come from.

The two meet at boxing sessions. Liftlog’s programme can schedule one; “Start in Corner” runs it on the bag; Liftlog then reads back the rounds, time and effort rating and marks the session done.

How it works

Liftlog is one container: a small Node API over SQLite and a React client. The progression rules are pure functions (last time’s reps plus one at the same weight, up to the top of the range, then a load increase), which keeps them easy to test and change. Deload weeks stay in the history but are left off strength trends, so a planned easy week doesn’t read as regression.

Corner builds each session on the server as a timed script of every sound, and the phone only plays it, queued against the Web Audio clock so the bells land on time even if the page is busy. The voice is generated once on the server with an open-source text-to-speech model, in its own container so synthesis never slows the app; each clip is named by a hash of the text and voice settings, so changing a phrase re-records only that phrase.

Neither server talks to the other. Liftlog’s page asks Corner’s API for the result from the browser, and Corner only allows that from Liftlog’s address, read-only. Liftlog stays the only writer of its own data.

The interesting problem

Bluetooth. I expected the speaker’s delay to put the metronome off the beat. It doesn’t: Bluetooth delays every sound by the same amount, so the clicks, calls and bells stay in time with each other. What it actually breaks is the screen, which runs a tenth to a third of a second ahead of what you hear, and some speakers go to sleep after a second of silence and clip the first syllable of the next call. So Corner measures each output’s delay with a 16-tap test and holds the screen back to match, and a “keep awake” option plays a hiss too quiet to hear to stop the speaker sleeping.

Stack and why

Liftlog uses Node’s built-in SQLite and runs its TypeScript directly, so the server has no build step and almost no dependencies. Corner has no front-end build at all. Both are deliberately small, because each has one user and needs to keep working without attention. YAML for programmes and workouts means the content lives in git, can be written or revised outside the app (including with an AI model’s help), and the apps never become the place where training gets designed.

What I would do differently

The main risk with Liftlog was drifting back into a general-purpose fitness platform, which is what I’d replaced. Writing the requirements down before building, and refusing anything outside them, is what kept both apps small. This was a lesson from my earlier Coach project, where the design drifted for lack of exactly that.

Stack

Liftlog: Hono on Node 24 (node:sqlite)
API, programme parser and progression rules; no build step on the server
Liftlog: React 19 + Vite
Mobile-first client; charts in plain SVG
Corner: Python 3.13 + FastAPI
Library, session scripts and training log
Corner: plain ES modules + Web Audio
Phone player, scheduled on the audio clock
Kokoro (kokoro-onnx, int8)
Text-to-speech, run once on the server to record every call
YAML
Programmes and workouts, version-controlled
SQLite, Docker Compose + Tailscale
One file of data per app; tailnet-only access