A local-first PWA fitness training application. Plan, track, and execute your workouts entirely on your device — no server needed.
Find a file
David 352a3d5e88
Some checks are pending
CI / Lint & Format (push) Waiting to run
CI / Build (push) Waiting to run
CI / Tests (${{ matrix.browser }}) (chromium) (push) Waiting to run
CI / Tests (${{ matrix.browser }}) (firefox) (push) Waiting to run
Initial commit
2026-06-18 13:06:57 +02:00
.claude Initial commit 2026-06-18 13:06:57 +02:00
.github/workflows Initial commit 2026-06-18 13:06:57 +02:00
docs Initial commit 2026-06-18 13:06:57 +02:00
public Initial commit 2026-06-18 13:06:57 +02:00
scripts Initial commit 2026-06-18 13:06:57 +02:00
src Initial commit 2026-06-18 13:06:57 +02:00
tests Initial commit 2026-06-18 13:06:57 +02:00
website Initial commit 2026-06-18 13:06:57 +02:00
.gitignore Initial commit 2026-06-18 13:06:57 +02:00
.prettierignore Initial commit 2026-06-18 13:06:57 +02:00
.prettierrc Initial commit 2026-06-18 13:06:57 +02:00
AGENTS.md Initial commit 2026-06-18 13:06:57 +02:00
CLAUDE.md Initial commit 2026-06-18 13:06:57 +02:00
eslint.config.js Initial commit 2026-06-18 13:06:57 +02:00
index.html Initial commit 2026-06-18 13:06:57 +02:00
jsconfig.json Initial commit 2026-06-18 13:06:57 +02:00
LICENSE.md Initial commit 2026-06-18 13:06:57 +02:00
notice.md Initial commit 2026-06-18 13:06:57 +02:00
package-lock.json Initial commit 2026-06-18 13:06:57 +02:00
package.json Initial commit 2026-06-18 13:06:57 +02:00
README.md Initial commit 2026-06-18 13:06:57 +02:00
vite.config.js Initial commit 2026-06-18 13:06:57 +02:00

TrainUs

A local-first PWA fitness training application. Plan, track, and execute your workouts entirely on your device — no server needed.

Warning

IA has been massively used.
I have the technical skills to carry out this kind of project, but not the time. The rise of AI tools changed the equation: what would have taken me months of evenings and weekends became achievable. AI didnt replace expertise — it made possible what wasnt, purely due to time constraints. It was also an opportunity to learn how to work with these tools, which is a benefit in itself: I got to test and compare quite a few models and assistants throughout the development.

Tech Stack

  • Vue 3 + Vite — reactive UI framework with fast build tooling
  • SQLite WASM with OPFS — client-side database persistence
  • i18next — internationalization (English, French & Danish)
  • Chart.js — workout statistics
  • vite-plugin-pwa + Workbox — offline support and install prompt
  • Playwright (Python) — end-to-end testing

Getting Started

Prerequisites

  • Node.js ≥ 18
  • Python ≥ 3.10 (for tests)

Development

npm install
npm run dev        # dev server at http://localhost:5173
npm run lint       # ESLint (src/)
npm run format     # Prettier (auto-fix)
npm run format:check  # Prettier (check only)

The dev server runs at http://localhost:5173 with COOP/COEP headers enabled (precautionary; the SAH-pool VFS used by this app does not strictly require them). The Playwright test suite runs its own instance on port 5199 instead (see tests/README.md).

Note: OPFS persistence requires a modern browser (Firefox 111+, Chrome 102+, Safari 16.4+) and does not work in private browsing mode. If OPFS is unavailable, the app displays a compatibility error.

Build

npm run build

Produces a static dist/ folder ready for deployment.

Deploy

TrainUs uses a single deployment: copy the dist/ folder to the site root, replacing the previous build (no per-version folders). User data is preserved across updates — it lives at a stable OPFS location, and a startup migration step applies any schema changes.

TrainUs is a static site — it works on any web server or CDN. COOP/COEP headers are not required for the SAH-pool VFS used here, but are recommended for defense-in-depth and may be required if the VFS is ever changed:

Nginx

location / {
    add_header Cross-Origin-Opener-Policy "same-origin";
    add_header Cross-Origin-Embedder-Policy "require-corp";
}

Apache

<IfModule mod_headers.c>
    Header always set Cross-Origin-Opener-Policy "same-origin"
    Header always set Cross-Origin-Embedder-Policy "require-corp"
</IfModule>

Caddy

header {
    Cross-Origin-Opener-Policy "same-origin"
    Cross-Origin-Embedder-Policy "require-corp"
}

Netlify (public/_headers)

/*
  Cross-Origin-Opener-Policy: same-origin
  Cross-Origin-Embedder-Policy: require-corp

Vercel (vercel.json)

{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "Cross-Origin-Opener-Policy", "value": "same-origin" },
        { "key": "Cross-Origin-Embedder-Policy", "value": "require-corp" }
      ]
    }
  ]
}

Preview production build locally

npm run preview

Serves dist/ at http://localhost:4173 with the correct headers (injected by Vite).

Tests

Setup (first time)

cd tests
python -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/playwright install firefox chromium

Running tests

Command Browsers Mode
npm run test firefox sequential
npm run test:chromium chromium sequential
npm run test:parallel firefox + chromium parallel
npm run test:parallel:firefox firefox parallel
npm run test:parallel:chromium chromium parallel
npm run test:parallel:n firefox + chromium parallel, fixed worker count
# Sequential — Firefox only (simple)
npm run test
# or: cd tests && .venv/bin/python -m pytest --browser firefox -v

# Parallel — both browsers, all CPU cores (fastest)
npm run test:parallel

# Parallel — Firefox only, all CPU cores
npm run test:parallel:firefox

# Parallel — both browsers, limited to N workers (default 4)
WORKERS=2 npm run test:parallel:n

# Via shell script — both browsers, auto workers
./tests/run_parallel.sh

# Via shell script — Firefox only, 4 workers
PYTEST_WORKERS=4 PYTEST_BROWSERS="firefox" ./tests/run_parallel.sh

# Single test file, Firefox only
cd tests && .venv/bin/python -m pytest test_step2_settings.py --browser firefox -v

# Single test file in parallel via shell script
./tests/run_parallel.sh test_step3_exercises.py

Website

The project site (docs + blog) is a Hugo static site under website/.

Prerequisites

  • Hugo ≥ 0.123 extended edition

Development

npm run site:dev   # Hugo dev server at http://localhost:1313 (with drafts)

Build

npm run site:build   # Minified output in website/public/

Deploy

The site is a static folder — copy website/public/ to any web host. Before building for production, set two values in website/hugo.toml:

baseURL = "https://yourdomain.example/trainus/"   # full URL to the site root

[params]
  appUrl = "https://yourdomain.example/app/"      # leave empty to show "Coming soon"

No special server headers are required for the website itself (COOP/COEP are only needed for the app).

Documentation

You can access the website here.

Project

Credits

Icons

The app icon is Mighty Force by Delapouite, sourced from game-icons.net and licensed under CC BY 3.0. Thank you for making such a great icon library freely available!

UI icons are provided by Bootstrap Icons, licensed under MIT.

Third-Party Software

Package License
Vue 3 MIT
Vue Router MIT
Vite MIT
@vitejs/plugin-vue MIT
vite-plugin-pwa MIT
Workbox (bundled by vite-plugin-pwa) Apache-2.0
@sqlite.org/sqlite-wasm Apache-2.0
Bootstrap Icons MIT
Mighty Force by Delapouite (app icon) CC BY 3.0
Chart.js MIT
vue-chartjs MIT
DOMPurify Apache-2.0 / MPL-2.0
marked MIT
i18next MIT
i18next-vue MIT
Prettier MIT
Playwright Apache-2.0
pytest MIT
pytest-playwright Apache-2.0
pytest-xdist MIT

License

TrainUs — an open, local-first, offline-capable fitness tracking application where you can create and share your own training sessions. Copyright (C) 2026 David Florance

This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License along with this program. If not, see http://www.gnu.org/licenses/.