From 845e9d7d47cb534aa523a10f21934b9e89a7aa53 Mon Sep 17 00:00:00 2001 From: Daniele De Dominicis Date: Sat, 22 Aug 2026 08:33:55 +0000 Subject: [PATCH] Fix what the documentation got wrong, and stop it repeating itself MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A read-through of the README, docs/DESIGN.md and demo/README.md, checking every claim against the code rather than against the last time it was true. The one that mattered: the documented development setup did not work. `cp .env.example .env` followed by "point DATABASE_URL at a PostgreSQL instance" pointed at a variable the file did not contain, and the setting has no default on purpose — so following the README literally ends in a pydantic ValidationError at import with nothing to say the fix is to add a line. DATABASE_URL is now in .env.example, commented, saying that Compose builds it from the POSTGRES_* values and that a local run is the case that needs it. DEFAULT_FIRST_NAME and DUPLICATE_WINDOW_SECONDS were documented in prose and absent from the file too; both are there now. Stale facts: * the upload endpoint still advertised `.tcx` or `.gpx` only * the test count said 311, and it is 343 * the repository tree omitted database.py, schemas/, services/archives.py and hooks/ — archives.py in particular, with archive import a headline feature * "CORS never comes into it either way" sat next to a wired CORSMiddleware. The middleware is for the case the sentence was not describing, so it now says which case each covers Repetition: * the phone bullet and the phone screenshot's caption said the same sentence twice, sixty lines apart * "Why it works the way it does" was a section whose whole content was a link to docs/DESIGN.md, immediately after a paragraph linking to docs/DESIGN.md. One signpost now, in the section that earns it And two smaller ones: docs/DESIGN.md listed its last two sections in the wrong order, and demo/README.md had a paragraph with a broken wrap and no word on why the demo set has no FIT file now that the app reads FIT. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019mNxXmXqwMZf6MG8PoWbq4 --- .env.example | 14 ++++++++++++++ README.md | 34 +++++++++++++++++----------------- demo/README.md | 12 +++++++++--- docs/DESIGN.md | 2 +- 4 files changed, 41 insertions(+), 21 deletions(-) diff --git a/.env.example b/.env.example index c59e51e..1bdf189 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,12 @@ POSTGRES_USER=activity POSTGRES_PASSWORD=activity POSTGRES_DB=activity_hub +# Where the API finds the database. Compose builds this itself out of the three +# values above, so leave it alone for a Docker run — it is needed only when you +# run the API on your own machine, where nothing else supplies it and startup +# fails without it. +# DATABASE_URL=postgresql+psycopg2://activity:activity@localhost:5432/activity_hub + # IANA zone used to bucket activities whose file states no UTC offset. DISPLAY_TIMEZONE=Europe/Rome @@ -19,3 +25,11 @@ API_PORT=8000 # Only needed if something other than the bundled dashboard calls the API. CORS_ORIGINS= + +# The name `scripts.ensure_user` gives the profile it creates, for a headless +# setup that never opens the first-run screen. +# DEFAULT_FIRST_NAME=Athlete + +# How close two activities of the same sport must start to count as the same +# session exported twice. Five minutes. +# DUPLICATE_WINDOW_SECONDS=300 diff --git a/README.md b/README.md index 49378ee..87eb8f8 100644 --- a/README.md +++ b/README.md @@ -63,8 +63,7 @@ To try it without your own data, load the bundled demo set: - **Export** — the summary as CSV and every activity as GPX in one zip, both honouring whatever filters are set. The zip this app writes is a zip it can read back. -- **Readable on a phone**, down to a 320px screen: the activity table keeps the - columns that fit and earns the rest as the screen widens. +- **Readable on a phone**, down to a 320px screen. ## What it looks like @@ -101,8 +100,8 @@ elapsed time, so the same hill lands in the same place on both lines: Two rides compared: speed and elevation against distance travelled -It reads on a phone too, down to a 320px screen — the activity table keeps the -columns that fit and earns the rest as the screen widens: +The activity table keeps the columns that fit on a narrow screen and earns the +rest as it widens, so nothing hides behind a sideways swipe: @@ -174,7 +173,7 @@ Interactive docs at when the stack is up. | GET | `/api/workouts/{id}/zones?user_id=X` | One activity's time in zone and the load it earned | | GET | `/api/workouts/{id}/export.gpx?user_id=X` | This activity as GPX, rebuilt from its samples | | DELETE | `/api/workouts/{id}?user_id=X` | Cascades to track points; 404 if owned by someone else | -| POST | `/api/upload?user_id=X` | Multipart `file`: one `.tcx` or `.gpx` | +| POST | `/api/upload?user_id=X` | Multipart `file`: one `.fit`, `.tcx` or `.gpx` | | POST | `/api/upload/archive?user_id=X` | Multipart `file`: a `.zip` export. Returns counts plus a per-file outcome | | GET | `/api/analysis/{user_id}` | Lifetime totals plus a per-sport breakdown | | GET | `/api/analysis/{user_id}/weekly?weeks=12` | One bucket per local week, quiet weeks zero-filled | @@ -225,15 +224,11 @@ that Monday, not to the Sunday it falls on in UTC. summaries, counts. The samples live in `track_points`, so nothing is stored twice. -Archive imports, duplicate detection and the reasoning behind both are in -[docs/DESIGN.md](docs/DESIGN.md). - -## Why it works the way it does - Most of the choices in here have a reason that is not obvious from the code, and several exist because the obvious alternative was tried and looked wrong on the -rendered page. They are collected in **[docs/DESIGN.md](docs/DESIGN.md)** — -worth a read before changing a chart, a colour or a derived figure. +rendered page — archive imports and duplicate detection among them. They are +collected in **[docs/DESIGN.md](docs/DESIGN.md)**, worth a read before changing +a chart, a colour or a derived figure. ## Demo data @@ -262,7 +257,7 @@ Docker is enough to run it; these are for working on it. You need **Python cd backend python -m venv .venv && source .venv/bin/activate pip install -r requirements.txt -cp .env.example .env # point DATABASE_URL at a PostgreSQL instance +cp .env.example .env # then uncomment DATABASE_URL and point it at PostgreSQL alembic upgrade head uvicorn app.main:app --reload ``` @@ -274,9 +269,10 @@ npm install npm run dev ``` -The dev server proxies `/api`, and the nginx image serves the API on the same -origin, so the browser never makes a cross-origin request and CORS never comes -into it either way. +The dev server proxies `/api` and the nginx image serves the API on the same +origin, so in both cases the browser makes no cross-origin request at all. The +CORS middleware is there for the other case — something that is not this +dashboard calling the API — and `CORS_ORIGINS` is what it reads. For a headless setup that never opens a browser, `python -m scripts.ensure_user` creates the profile from `DEFAULT_FIRST_NAME` instead of the first-run screen. @@ -284,7 +280,7 @@ creates the profile from `DEFAULT_FIRST_NAME` instead of the first-run screen. ### Tests ```bash -cd backend && ruff check . && ruff format --check . && pytest # 311 tests, no database needed +cd backend && ruff check . && ruff format --check . && pytest # 343 tests, no database needed cd frontend && npm run lint && npm test # 223 tests, jsdom ``` @@ -326,10 +322,13 @@ that day comes. backend/app/ ├── main.py FastAPI app, CORS, router wiring ├── config.py pydantic-settings, reads .env +├── database.py engine, session, declarative base ├── models/ User, Workout, TrackPoint, WorkoutBest +├── schemas/ the request and response shapes ├── routers/ health, users, workouts, upload, analysis, export └── services/ ├── parsers/ base, FIT, TCX, GPX, factory + ├── archives.py reading a zip without trusting the zip ├── analyzer.py metric derivation and aggregate reporting ├── records.py the window scan, and records over it ├── zones.py the heart-rate histogram, and zones over it @@ -339,6 +338,7 @@ backend/app/ frontend/src/ ├── theme.js the validated palette, both modes ├── api/client.js every call this app makes +├── hooks/ whether the viewer is in dark mode ├── utils/ formatters, geo, track measurement, zone shares └── components/ one per panel of the dashboard ``` diff --git a/demo/README.md b/demo/README.md index 4a9d8f9..1b6bc25 100644 --- a/demo/README.md +++ b/demo/README.md @@ -3,6 +3,11 @@ Synthetic TCX and GPX activities: five weeks of training plus a set of deliberately awkward files. +No FIT file, though the app reads that too. FIT is binary and this generator +writes text from one end to the other, so producing one would mean teaching it +a second output mode. The FIT path is covered by the test suite instead, where +`backend/tests/fit_builder.py` builds whatever bytes a test needs. + ## Why synthetic Real GPS traces are personal data. A GPX exported from someone's watch @@ -37,9 +42,10 @@ python -m scripts.generate_demo_data --out ../demo/activities --clean \ Output is deterministic for a given `--seed`, and `--ending` is pinned so the committed files stay stable — which also means they age. The committed block ends **2026-06-28**, so a weekly chart run much later shows empty recent weeks. -For a dashboard that looks alive, regenerate with `--ending today`. For something closer to real data — a full -training year sampled every ten seconds, about 15 MB — write it somewhere -untracked: +For a dashboard that looks alive, regenerate with `--ending today`. + +For something closer to real data — a full training year sampled every ten +seconds, about 15 MB — write it somewhere untracked: ```bash python -m scripts.generate_demo_data --out ../demo/generated \ diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 541fe95..dee1eac 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -16,8 +16,8 @@ offers. This is the long half. - [Fitness, fatigue and form](#fitness-fatigue-and-form) - [Personal bests](#personal-bests) - [Comparing two activities](#comparing-two-activities) -- [Filtering, deleting, exporting](#filtering-deleting-exporting) - [Narrow screens](#narrow-screens) +- [Filtering, deleting, exporting](#filtering-deleting-exporting) ## Reading the files