255 lines
11 KiB
Markdown
255 lines
11 KiB
Markdown
# 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 didn’t replace expertise — it made possible what wasn’t, 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
|
||
|
||
```bash
|
||
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
|
||
|
||
```bash
|
||
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
|
||
|
||
```nginx
|
||
location / {
|
||
add_header Cross-Origin-Opener-Policy "same-origin";
|
||
add_header Cross-Origin-Embedder-Policy "require-corp";
|
||
}
|
||
```
|
||
|
||
#### Apache
|
||
|
||
```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
|
||
|
||
```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`)
|
||
|
||
```json
|
||
{
|
||
"headers": [
|
||
{
|
||
"source": "/(.*)",
|
||
"headers": [
|
||
{ "key": "Cross-Origin-Opener-Policy", "value": "same-origin" },
|
||
{ "key": "Cross-Origin-Embedder-Policy", "value": "require-corp" }
|
||
]
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
#### Preview production build locally
|
||
|
||
```bash
|
||
npm run preview
|
||
```
|
||
|
||
Serves `dist/` at `http://localhost:4173` with the correct headers (injected by Vite).
|
||
|
||
### Tests
|
||
|
||
#### Setup (first time)
|
||
|
||
```bash
|
||
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 |
|
||
|
||
```bash
|
||
# 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](https://gohugo.io/installation/) ≥ 0.123 extended edition
|
||
|
||
#### Development
|
||
|
||
```bash
|
||
npm run site:dev # Hugo dev server at http://localhost:1313 (with drafts)
|
||
```
|
||
|
||
#### Build
|
||
|
||
```bash
|
||
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`:
|
||
|
||
```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](https://kaivalya.eu/projects/trainUs).
|
||
|
||
### Project
|
||
|
||
- [Technical Documentation](website/content/developers/technical.en.md) -- Architecture, tech stack, data model, and feature details
|
||
- [User Documentation](website/content/docs/user-guide/index.en.md) -- How each feature works from the user's perspective
|
||
- [Implementation Plan](docs/plan.md) -- Step-by-step feature plan and progress tracking
|
||
- [Decisions Log](docs/decisions-log.md) -- All design decisions with rationale
|
||
|
||
## Credits
|
||
|
||
### Icons
|
||
|
||
The app icon is [Mighty Force](https://game-icons.net/1x1/delapouite/mighty-force.html) by [Delapouite](https://delapouite.com), sourced from [game-icons.net](https://game-icons.net) and licensed under [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/). Thank you for making such a great icon library freely available!
|
||
|
||
UI icons are provided by [Bootstrap Icons](https://icons.getbootstrap.com), licensed under MIT.
|
||
|
||
### Third-Party Software
|
||
|
||
| Package | License |
|
||
| -------------------------------------------------------------------------------------------------------------------------- | -------------------- |
|
||
| [Vue 3](https://github.com/vuejs/core) | MIT |
|
||
| [Vue Router](https://github.com/vuejs/router) | MIT |
|
||
| [Vite](https://github.com/vitejs/vite) | MIT |
|
||
| [@vitejs/plugin-vue](https://github.com/vitejs/vite-plugin-vue) | MIT |
|
||
| [vite-plugin-pwa](https://github.com/vite-pwa/vite-plugin-pwa) | MIT |
|
||
| [Workbox](https://github.com/GoogleChrome/workbox) (bundled by vite-plugin-pwa) | Apache-2.0 |
|
||
| [@sqlite.org/sqlite-wasm](https://sqlite.org/wasm/doc/trunk/index.md) | Apache-2.0 |
|
||
| [Bootstrap Icons](https://icons.getbootstrap.com) | MIT |
|
||
| [Mighty Force](https://game-icons.net/1x1/delapouite/mighty-force.html) by [Delapouite](https://delapouite.com) (app icon) | CC BY 3.0 |
|
||
| [Chart.js](https://github.com/chartjs/Chart.js) | MIT |
|
||
| [vue-chartjs](https://github.com/apertureless/vue-chartjs) | MIT |
|
||
| [DOMPurify](https://github.com/cure53/DOMPurify) | Apache-2.0 / MPL-2.0 |
|
||
| [marked](https://github.com/markedjs/marked) | MIT |
|
||
| [i18next](https://github.com/i18next/i18next) | MIT |
|
||
| [i18next-vue](https://github.com/i18next/i18next-vue) | MIT |
|
||
| [Prettier](https://github.com/prettier/prettier) | MIT |
|
||
| [Playwright](https://github.com/microsoft/playwright) | Apache-2.0 |
|
||
| [pytest](https://github.com/pytest-dev/pytest) | MIT |
|
||
| [pytest-playwright](https://github.com/microsoft/playwright-python) | Apache-2.0 |
|
||
| [pytest-xdist](https://github.com/pytest-dev/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](http://kaivalya.eu)
|
||
|
||
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/>.
|