docs: spec for application vs environment configuration
Design for the 'configuration' backlog idea: instance-global key/value config table (overrides only, cold reload), env-var renames, /api/config GET/PUT, and the /config page with header icon buttons. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,15 +7,66 @@ writing-plans flow (see `docs/superpowers/specs/` and `docs/superpowers/plans/`
|
|||||||
for that history) and remove it from here once a spec exists.
|
for that history) and remove it from here once a spec exists.
|
||||||
|
|
||||||
## Backlog
|
## Backlog
|
||||||
|
- new workout kinds
|
||||||
- dedicated workouts section: add workouts table in database and create a dedicated page in UI (use an "objective" icon). workouts are already downloaded as dependencies of activities.
|
- add "Recovery", "Quick", and "Sprint" workout types
|
||||||
- improve synchronization: make it modal, split downloads of activities and workouts, add progress bar (which requires to know in advance how many activities or workouts will have to be downloaded).
|
- order to follow (in activities page filter buttons, in analysis page combobox, in profile page workout kinds cards) is Recovery -> Easy -> Long -> Tempo -> 60' Threshold -> 30' Threshold -> Quick -> Intervals -> MAS Test -> Sprint -> Race
|
||||||
- modern error displays: have error messages displayed in ephemeral banners with background (red for error, orange for warning, blue for notice). this should be used for login, logout, synchronisation final status in profile page (the synchronisation operation itself remains a progress bar). this modern error display should be used on all pages/tabs in case of backend is not here.
|
- adapt the color palette from blue to purple according to this order (starting with blue for recovery -> green -> yellow -> orange -> red until purple for race), put the color code in DB (UI shall not resolve it with kindColor based on workout kind name)
|
||||||
- better UX for activities (list layout without the graphs, detailed view with different graphs for each phase incl. delta with workout)
|
- first time setup page
|
||||||
- add icon to indicate if an activity has an associated workout
|
- once connection is validated, add a step to ask user its best race results on 5k, 10k, half-marathon and marathon (at least one shall be provided) and store it in the profile. these values will be updated later during synchronization (as we can detect race kinds with their distance)
|
||||||
|
- add another step to enter latest known MAS (in mm:ss/km) and store it in the profile. this value will be updated later during synchronization (as we can assign an activity with "MAS test" kind)
|
||||||
|
- profile page
|
||||||
|
- move "Heart rate" card before "Activity analysis" card and rename it to "Characteristics"
|
||||||
|
- add genre (male/female) and age in "Characteristics"
|
||||||
|
- rename "Activity analysis" card to "Activities"
|
||||||
|
- in activity analysis, remove "rolling window" and consider the future AI-assisted activity kind analysis will be done on all the synced activities
|
||||||
|
- in activity analysis, remove the "warm-up" and "cool-down" as we'll detect them during activities synchronization and block identification
|
||||||
|
- Move all "Chart colors" items inside "Activities" card (and remove "Chart colors" card)
|
||||||
|
- add a personal records cards for 1k, 5k, 10k, half-marathon, marathon (we'll have at least one value coming from "first time setup page", or more after synchronization). show the date if it comes from an activity
|
||||||
|
- add a performance card showing latest know MAS (we'll have at least one value coming from "first time setup page", or a true one after synchronization). show the date if it comes from an activity
|
||||||
|
- rename "Training types" card to "Workouts"
|
||||||
|
- for each workout kind in "Workout kinds" card
|
||||||
|
- add a radio button to declare a warm-up or not (when checked, add a field to enter duration in minutes)
|
||||||
|
- add a radio button to declare a cool-down or not (when checked, add a field to enter duration in minutes)
|
||||||
|
- add a way to choose its primary objective (either "heart rate", either "% MAS")
|
||||||
|
- if "heart rate" is selected, make appear the "min heart rate", "max heart rate", "min pace", "max pace" fields like before (heart rate fields are mandatory, but not the pace ones)
|
||||||
|
- if "% MAS" is selected, make appear a "% MAS" field requiring a value between 0 and 150, and a "pace amplitude (s/km)" field requiring a number between 0 and 60
|
||||||
|
- add Claude API key (keep it optional, this will be for AI-assisted services)
|
||||||
|
- analysis page
|
||||||
|
- refactor /api/progress in /api/analysis
|
||||||
|
- there should now be history graphs per workout kind, like today (remove VO2max metric as we'll move it elsewhere) and general history graphs to monitor new metrics not linked to a workout kind
|
||||||
|
- general metrics are :
|
||||||
|
- VO2max
|
||||||
|
- (as soon as if we have a 5k race in the profile) "Endurance Index (5k)" which is (100-%MAS)/(ln(6/t)) where %MAS is 5k pace divided by MAS speed (get it from the profile) multiplied by 100, and t the duration of 5k race in minutes
|
||||||
|
- (as soon as if we have a 10k race in the profile) "Endurance Index (10k)" which is (100-%MAS)/(ln(6/t)) where %MAS is 10k pace divided by MAS speed (get it from the profile) multiplied by 100, and t the duration of 10k race in minutes
|
||||||
|
- (as soon as if we have a half-marathon race in the profile) "Endurance Index (HM)" which is (100-%MAS)/(ln(6/t)) where %MAS is half-marathon pace divided by MAS speed (get it from the profile) multiplied by 100, and t the duration of half-marathon race in minutes
|
||||||
|
- (as soon as if we have a marathon race in the profile) "Endurance Index (M)" which is (100-%MAS)/(ln(6/t)) where %MAS is marathon pace divided by MAS speed (get it from the profile) multiplied by 100, and t the duration of marathon race in minutes
|
||||||
|
- general metrics history policy is :
|
||||||
|
- VO2max, one history point for all activities (whatever their kind)
|
||||||
|
- Endurance Index metrics, one history point as soon as one formula value is changing (aka race time or MAS)
|
||||||
|
- create a "Status" card with following elements before the history graphs
|
||||||
|
- a line scale indicating current VO2max (declare and use application configuration vo2max.gxxyyy where g could be "m" for male or "f" for female, xx could be 20,30,40,50,60,70 and yyy could be "fair", "good", "excellent", "superior", get default values from https://www8.garmin.com/manuals/webhelp/venu/EN-US/GUID-1FBCCD9E-19E1-4E4C-BD60-1793B5B97EB3.html, for example vo2max.m40good=42.4)
|
||||||
|
- (as soon as if we have a 5k race in the profile) a line scale indicating current "Endurance Index (5k)" (declare and use application configuration ei.min with default value -15 and ei.max with default value 0)
|
||||||
|
- (as soon as if we have a 10k race in the profile) a line scale indicating current "Endurance Index (10k)" (declare and use application configuration ei.min with default value -15 and ei.max with default value 0)
|
||||||
|
- (as soon as if we have a half-marathon race in the profile) a line scale indicating current "Endurance Index (HM)" (declare and use application configuration ei.min with default value -15 and ei.max with default value 0)
|
||||||
|
- (as soon as if we have a marathon race in the profile) a line scale indicating current "Endurance Index (M)" (declare and use application configuration ei.min with default value -15 and ei.max with default value 0)
|
||||||
|
- training plan page
|
||||||
|
- we need to store our workouts and dedicated /api/workouts (these are not the garmin workouts)
|
||||||
|
- this page will list all the planned workouts of the plan, no consideration of schedule for now, just a list of workouts
|
||||||
|
- for each workout, we have to see
|
||||||
|
- kind (with same color policy as for the activities)
|
||||||
|
- objective (as given in workout type profile)
|
||||||
|
- min pace / max pace ()
|
||||||
|
- activities page
|
||||||
|
- add an indicator if a personal record has been beaten (with a cup icon)
|
||||||
|
- add an indicator for the maximum heart rate (just after the average heart rate)
|
||||||
|
- AI-assisted ranking button that will set workout kind on unlocked activities (based on workout kind description in profile page)
|
||||||
|
- discovery of warm-up, cool-down phases, so we can read activity per blocks (with detection of extension of a block compared to linked workout)
|
||||||
|
- discovery of additional blocks done but not requested by linked workout (for example, additional small sprints at the end of an easy run)
|
||||||
|
- add icon to indicate if an activity has an associated workout
|
||||||
|
-
|
||||||
|
- list layout with smaller graphs (no scale, no targets, no , detailed view with different graphs for each phase incl. delta with workout)
|
||||||
|
|
||||||
## Someday / maybe
|
## Someday / maybe
|
||||||
|
|
||||||
- dedicated tab for past workouts (or find a way to show them in the "training plan" tab)
|
|
||||||
- training programs with phases (tapering, easier weeks, post-race recovery)
|
- training programs with phases (tapering, easier weeks, post-race recovery)
|
||||||
-
|
- improve logs (add non-REST logs, make python generate JSON logs through stdout, adapt go<->python protocol)
|
||||||
|
|||||||
132
docs/superpowers/specs/2026-08-03-configuration-design.md
Normal file
132
docs/superpowers/specs/2026-08-03-configuration-design.md
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
# Configuration: application vs environment — design
|
||||||
|
|
||||||
|
Date: 2026-08-03
|
||||||
|
Status: approved
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
All runtime tuning today is either an env var (`internal/config`, process-level)
|
||||||
|
or a per-user `profile` column. There is no home for instance-global,
|
||||||
|
behavior-affecting settings shared by all users, and some env vars are
|
||||||
|
misnamed or shouldn't be env vars at all. From `docs/IDEAS.md` ("configuration").
|
||||||
|
|
||||||
|
## Concepts
|
||||||
|
|
||||||
|
- **Application configuration**: key/value pairs, stored in the database,
|
||||||
|
common to all users, cold (read once at startup; a change takes effect on
|
||||||
|
the next backend restart — no hot reload). Unrelated to the execution
|
||||||
|
environment.
|
||||||
|
- **Environment configuration**: env vars, tied to the execution environment
|
||||||
|
(paths, URLs, credentials for infrastructure). Unchanged mechanism.
|
||||||
|
|
||||||
|
## Data model
|
||||||
|
|
||||||
|
New table in `schema.sql`:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE config (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
- Deliberately **not** user-scoped — the single exception to the per-user
|
||||||
|
convention, because application configuration is instance-global by
|
||||||
|
definition. A schema comment records this.
|
||||||
|
- The table stores **overrides only**; defaults live in code. Absent key =
|
||||||
|
default in effect.
|
||||||
|
- Regenerate `docs/DATABASE.md` (`go run ./cmd/dumpschema`) after the schema
|
||||||
|
change. Breaking schema change: delete the local DB and let `Open()`
|
||||||
|
recreate it (normal pre-production remedy).
|
||||||
|
|
||||||
|
## Application-config registry
|
||||||
|
|
||||||
|
`internal/config` gains a registry of known keys — name, default, validator,
|
||||||
|
description. Launch set (one key):
|
||||||
|
|
||||||
|
| key | type | default | validation |
|
||||||
|
|--------------------|---------------|---------|----------------------|
|
||||||
|
| `session.duration` | integer hours | `720` | integer, > 0 |
|
||||||
|
|
||||||
|
- `config.Load()` (env) stays as-is: fail-fast, runs before the DB opens.
|
||||||
|
- New `config.LoadApp(getter)` reads overrides through a small store-backed
|
||||||
|
getter, merges defaults, validates, and returns a typed `AppConfig` struct
|
||||||
|
(`SessionDuration time.Duration`). `main.go` calls it after `store.Open`
|
||||||
|
and wires the result into `api.SessionConfig.Duration`.
|
||||||
|
- Writes validate against the registry: unknown keys and invalid values are
|
||||||
|
rejected, so the `config` table cannot accumulate junk.
|
||||||
|
|
||||||
|
## Environment-variable changes (all breaking; pre-production, no shims)
|
||||||
|
|
||||||
|
| before | after |
|
||||||
|
|----------------------------|----------------------------|
|
||||||
|
| `GENIUSRUN_ADDR` | `GENIUSRUN_BACKEND_ADDR` |
|
||||||
|
| `GARMIN_WRAPPER_PYTHON` | `GENIUSRUN_PYTHON_PATH` |
|
||||||
|
| `GARMIN_TOKENSTORE` | `GENIUSRUN_TOKENSTORE_PATH`|
|
||||||
|
| `GENIUSRUN_MIN_CONFIDENCE` | removed — hard-coded to `classify.DefaultMinConfidence` (0.6) |
|
||||||
|
| `GENIUSRUN_SESSION_DURATION` | removed — becomes app-config key `session.duration` |
|
||||||
|
| `GENIUSRUN_SESSION_SECRET` | unchanged (stays env; still required, >= 32 chars) |
|
||||||
|
|
||||||
|
Also update: `backend/.env` (local), `CLAUDE.md`, config tests, and any doc
|
||||||
|
references.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
Both routes sit behind the usual session + `requireProvisionedUser` gates.
|
||||||
|
Any provisioned user may read and write (single-operator app in practice).
|
||||||
|
|
||||||
|
- `GET /api/config` →
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"application": [
|
||||||
|
{"key": "session.duration", "value": "720", "default": "720",
|
||||||
|
"overridden": false, "description": "Session cookie lifetime in hours"}
|
||||||
|
],
|
||||||
|
"environment": [
|
||||||
|
{"name": "GENIUSRUN_BACKEND_ADDR", "value": ":8080"},
|
||||||
|
{"name": "GENIUSRUN_OIDC_CLIENT_SECRET", "value": "•••• (set)"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Secrets (`GENIUSRUN_OIDC_CLIENT_SECRET`, `GENIUSRUN_SESSION_SECRET`) are
|
||||||
|
masked as `•••• (set)` / `(unset)`; values never leave the process.
|
||||||
|
- `main.go` builds the display-safe environment snapshot once (from
|
||||||
|
`config.Config`, masking applied) and passes it to `api.NewServer` —
|
||||||
|
handlers never call `os.Getenv`.
|
||||||
|
- `PUT /api/config` with `{"session.duration": "168"}`:
|
||||||
|
validates every pair against the registry (reject unknown key / invalid
|
||||||
|
value with 400 naming the offender; all-or-nothing), upserts, returns the
|
||||||
|
same shape as GET. Cold semantics: response carries no magic — the UI
|
||||||
|
states that changes apply after a backend restart.
|
||||||
|
|
||||||
|
## Frontend
|
||||||
|
|
||||||
|
- **Config page** at browser path `/config` — `App.tsx` derives the view from
|
||||||
|
`window.location.pathname` (no router library; `history.pushState` on
|
||||||
|
navigation so the URL is shareable/deep-linkable, and Vite's SPA fallback
|
||||||
|
serves it in dev; a production reverse proxy needs SPA fallback for the
|
||||||
|
path).
|
||||||
|
- Page content: an "Application configuration" card (editable fields per
|
||||||
|
registry entry, Save button, a visible note "changes take effect after the
|
||||||
|
backend restarts"), and a read-only "Environment configuration" card
|
||||||
|
listing name/value pairs, secrets masked by the backend.
|
||||||
|
- **Header changes** (`App.tsx`), left to right after the tabs:
|
||||||
|
1. Profile button: icon-only, user-profile glyph (👤). The profile name
|
||||||
|
moves into `title`/`aria-label` — no visible text.
|
||||||
|
2. New settings button: icon-only gear (⚙), navigates to `/config`.
|
||||||
|
3. Log out: icon-only cross (✕), still the POST `<form>` (OIDC logout
|
||||||
|
needs a real navigation). `title`/`aria-label` "Log out".
|
||||||
|
- No visible text labels on any of the three; same emoji/glyph style as
|
||||||
|
the existing tabs, no icon library added.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- `internal/store`: config upsert/read round-trip; overrides-only semantics.
|
||||||
|
- `internal/config`: table-driven registry tests — default when absent,
|
||||||
|
override applied, invalid value rejected, unknown key rejected.
|
||||||
|
- `internal/api`: httptest — GET shape + secret masking; PUT happy path,
|
||||||
|
unknown key 400, invalid value 400; 403 until provisioned (inherited gate,
|
||||||
|
asserted once).
|
||||||
|
- Frontend: live click-through — header icons, navigate to `/config`, edit
|
||||||
|
`session.duration`, save, verify persisted via GET and after restart.
|
||||||
Reference in New Issue
Block a user