A single-user, offline-first workout tracker that runs in the browser on your phone. Python (via Pyodide) for all the logic, IndexedDB for storage, plain HTML/CSS for the UI. No backend, no cloud, no build step.
- Pyodide — CPython compiled to WebAssembly, runs all
.pyfiles in the browser. - Plain HTML + CSS — no framework.
- ~150 lines of vanilla JS — IndexedDB wrapper and a delegated event dispatcher.
- Service worker + Web App Manifest — installable PWA, full offline after the first load.
- New workout from a template (Pull / Push / Legs) or blank.
- Per-set stepper buttons (±2.5 kg, ±1 rep), drop-set chains, till-failure toggle.
- Paste-to-import: drop a free-text session in your usual format and the parser fills the structured form.
- Per-exercise history with a small SVG chart (top set or estimated 1RM via Epley).
- JSON export / import — the only backup path, save to Drive yourself.
WorkoutNotes/
├── index.html # shell that loads Pyodide and runs app.boot()
├── manifest.json # PWA manifest
├── sw.js # cache-first service worker
├── styles.css # mobile-first, big tap targets
├── icons/ # SVG + 192/512 px PNGs
├── js/ # db.js, bridge.js, bootstrap.js
└── py/
├── app.py # state + central dispatcher
├── models.py # Workout / Exercise / SetGroup / Entry
├── storage.py # async CRUD over the JS IndexedDB shim
├── parser.py # free-text → Workout
├── templates.py # default Pull/Push/Legs exercises
├── views/ # one HTML-string-returning function per screen
└── tests/ # pytest tests (run on desktop, not in Pyodide)
python3 -m venv .venv
.venv/bin/pip install pytest cairosvg # cairosvg is only for re-rendering icons
.venv/bin/pytest py/tests/ # 13 tests, < 1 s
python -m http.server 8000 # open http://localhost:8000Service workers and Pyodide require either localhost or HTTPS, so opening
the file directly via file:// will not work.
When the Mac and phone are on the same network (including a hotspot):
- Find your Mac's LAN IP:
ifconfig | grep "inet " | grep -v 127.. - On the phone open
http://<that-ip>:8000. - The Python app will run; the service worker will refuse to register
because the origin is not secure. That's expected for plain HTTP — for
the full offline / "Add to Home Screen" experience use the GitHub Pages
deploy below, or tunnel via
cloudflared tunnel --url http://localhost:8000.
Pages serves over HTTPS automatically, which unlocks the service worker and "Add to Home Screen". The repo can be public or private — Pages works for both on the free plan.
Already in the repo. It excludes .venv/, .pytest_cache/, __pycache__/,
and macOS .DS_Store files.
cd /Users/aarkin/Documents/WorkoutNotes
git init
git add .
git commit -m "Initial commit: WorkoutNotes PWA"If you have the GitHub CLI (gh) and are signed in:
gh auth status # confirm you're logged in
gh repo create workout-notes --public --source=. --pushThat single command creates the remote, sets it as origin, and pushes
main in one go. If you'd rather keep it private, swap --public for
--private.
If you don't have gh:
-
Open https://github.com/new.
-
Name it
workout-notes. Don't initialize with a README, .gitignore, or license (the repo already has those). -
Run the commands GitHub shows you, which will look like:
git remote add origin https://github.com/<your-username>/workout-notes.git git branch -M main git push -u origin main
gh api -X POST repos/<your-username>/workout-notes/pages \
-f source[branch]=main -f source[path]=/Or via the web UI: Repo → Settings → Pages → Source = Deploy from branch,
Branch = main, Folder = / (root) → Save.
After a minute or so it will publish to:
https://<your-username>.github.io/workout-notes/
- Open the Pages URL in Chrome on Android (or Safari on iOS).
- Wait for the first load to finish (Pyodide is ~10 MB — happens once).
- Browser menu → Add to Home Screen.
- Launch from the home screen icon. It opens fullscreen and now works offline. Confirm by toggling airplane mode and reopening.
# bump CACHE name in sw.js if assets changed, then:
git add -A
git commit -m "..."
git pushPages redeploys in ~30 s. The service worker picks up new files the next time the browser revalidates the shell.
There is no cloud sync. Use Settings → Export JSON periodically and save the file somewhere — Drive, iCloud, an email to yourself. Import restores everything bit-for-bit. Test this at least once before you trust it.