A private push-up tracker that installs to an iPhone home screen. No account, no server, no analytics — every rep you log stays in your phone's browser storage.
Live at https://lilburdeh.github.io/pushups/
- Today — one big button logs your usual set. A ring fills toward your daily goal and keeps going past it (a second gold arc) when you beat it. Tap any set to edit its reps or time; tap × to delete with an undo.
- History — a 26-week heatmap, plus 7-day, 30-day, and all-time totals. Tap any square to see that day's sets, and any set to correct it.
- Settings — daily goal, the quick-add value, and backup.
Both Today and History fit one screen; only the set lists scroll.
Custom amount or time… opens a sheet with reps and a date/time, so you can log a set you forgot, or re-add one you deleted by mistake, at the time it actually happened.
Open the URL in Safari → Share → Add to Home Screen. It then launches full-screen with no address bar, and works offline.
There is no build step. Edit a file, commit, push:
git add -A && git commit -m "your change" && git pushGitHub Pages redeploys in about a minute. The phone picks the change up next launch.
Bump the cache version when you deploy. sw.js starts with:
const CACHE = 'pushups-v1.0.0';Change that string on any release. The service worker is network-first, so a deploy lands even if you forget — but bumping it evicts the old cache properly.
python3 -m http.server 8765 --directory .Then open http://localhost:8765. A plain file:// open will not work: ES modules and
service workers both require a real origin.
Plain HTML, CSS, and ES modules. No framework, no dependencies, nothing to update.
| File | Role |
|---|---|
js/dates.js |
All day-bucketing. The single definition of "a day". |
js/store.js |
localStorage read/write, derived totals, export/import. |
js/today.js |
Ring, today's set list, quick-add. |
js/history.js |
Heatmap, range stats, day detail. |
js/settings.js |
Goal, quick-add values, backup. |
js/app.js |
Tab routing, day-rollover watch, service worker registration. |
js/ui.js |
Undo toast and bottom sheets. |
sw.js |
Offline cache. Network-first, so deploys always land. |
Two things worth knowing before you change anything:
- Every date question goes through
dates.js. It uses local midnight, nevertoISOString()— that's UTC and would push late-evening sets onto the next day's square.setClock()exists so tests can freeze time. - Views never touch localStorage. They read from
store.jsand re-render on its events. Any change re-renders all three tabs; at this data size that's cheaper than tracking what went stale.
Display type is Nippo Bold from Fontshare; small UI text stays on the system font, which is more legible at 11–14px than a geometric display face.
The font file is deliberately not in this repo. The ITF Free Font License permits
self-hosting on your own site but prohibits distributing the font through a "repository"
or "publicly accessible servers" — and this repo is public. So styles.css points
@font-face at Fontshare's own CDN, which is the route the licence explicitly endorses,
and sw.js allow-lists cdn.fontshare.com so the font is cached on-device for offline
use. Caching on the user's own device is not redistribution.
If Fontshare ever rotates that URL, the app falls back to the system font rather than
breaking — that is what font-display: swap and the fallback stack are for.
Known trade-off: Nippo has no tabular figures. font-variant-numeric: tabular-nums
is a no-op on it, so digits are proportionally spaced — "111" is noticeably narrower
than "888". Numbers are centred in the ring and in the stat cards so it reads as gentle
breathing rather than misalignment, but if it ever grates, the fix is to drop
var(--font-display) from .ring-total, .stat-value and .set-reps and let the
system font handle numerals.
Everything lives under the pushups.sets.v1 and pushups.settings.v1 keys in
localStorage. Three layers of protection, in order of how much they actually help:
navigator.storage.persist()is requested on launch. Safari generally grants this to an installed home-screen app, which stops iOS clearing the data to reclaim space. It does not survive a manual "Clear History and Website Data".- Save backup… uses the Web Share API, so on iOS it opens the share sheet and the file can go straight to iCloud Drive or Files. This is the only route from a web app into cloud storage — there is no automatic sync without a server and an account.
- Last-backup tracking. Settings shows how long it has been and turns amber past two weeks.
Import merges on set id rather than overwriting, so restoring an old backup can never destroy anything logged since.