Minimalist body-weight workout timer. Runs entirely in the browser — no server, no account. Inspired by Feeel, built with the help of Claude.
- Trainings — create multiple named workout plans
- Exercises — add from the wger.de database or manually; drag to reorder
- Player — countdown timer with audio beeps, rest phases, "up next" preview
- PWA — installable, works offline, prevents screen from going dark during a session
- Data — stored in
localStorage; export/import as JSON
Open index.html in a browser, or install it as an app via the browser's "Add to Home Screen" prompt.
For full functionality (wger search, service worker, wake lock) serve the files over HTTP/HTTPS — e.g.:
npx serve .
# or
python3 -m http.server| File | Purpose |
|---|---|
index.html |
Entire app (HTML + CSS + JS) |
manifest.json |
PWA manifest |
icon.svg |
App icon (dumbbell) |
images/ |
Exercise illustrations (SVG) |
sw.js |
Service worker (caching + update flow) |
hooks/pre-commit |
Git hook: auto-bumps version on commit |
Code and exercise illustrations © Vitus Schuhwerk, released under the MIT License.
If you use or adapt this project or its images, please keep the license notice. A link back is appreciated but not required.
APP_VERSION in index.html and CACHE_NAME in sw.js are auto-bumped (patch) on every commit that touches index.html, via a pre-commit hook. If you bump the version manually before committing, the hook detects the change and skips.
After cloning, enable the hook:
git config core.hooksPath hooksUses the Screen Wake Lock API to keep the screen on while a workout is running. Supported in Chrome/Edge/Safari 16.4+. Falls back silently on unsupported browsers. Requires HTTPS (or localhost).
Tests are written using Playwright. Run them with:
bun test
# or
bunx playwright testNote on seeing console.log output: To keep the AI context window clean, playwright.config.js uses the dot reporter by default. This reporter hides successful test logs. If you need to see console.log output while debugging a passing test, override the reporter:
bunx playwright test --reporter=listWhen a new version ships with changed built-in trainings, a modal offers to replace your local copies. To simulate this in the browser console:
const s=JSON.parse(localStorage.getItem('pulse_data'));
s.version='0.0.0';
delete s.settings._pendingBuiltinIds;
const v=s.trainings.find(t=>t.builtinId==='vitus-2026');
if(v) v.builtinHash='stale';
localStorage.setItem('pulse_data',JSON.stringify(s));
location.reload();The modal fires when stored.builtinHash differs from the current builtin fingerprint and state.version doesn't match APP_VERSION. Changing the version alone isn't enough — you also need to fake a stale hash on the training.
- Remove the full-state backup -> only export trainings -> better merging.
- Should we update default trainings? How implement that simple?