refactor: merge internal/sync into internal/garmin, regroup api files and routes
Garmin auth/sync routes move under /api/garmin/*; sync.Service becomes garmin.Sync with garmin.SyncConfig/ClientConfig; applog becomes internal/log; the test mock moves into the garmin package as MockClient (breaking the test-only import cycle the merge created); stale test URLs and type names updated to match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,14 +7,21 @@ writing-plans flow (see `docs/superpowers/specs/` and `docs/superpowers/plans/`
|
||||
for that history) and remove it from here once a spec exists.
|
||||
|
||||
## Backlog
|
||||
- quickfixes
|
||||
- make setupSessionIdleTimeout an application configuration (key session.idle_timeout), check difference with appCfg.SessionDuration
|
||||
- don't put id_token in Claim, store it in the cookie apart from Claim
|
||||
- find alternative to deprecated React.FormEvent
|
||||
- replace all go log calls by our application logger
|
||||
- new workout kinds
|
||||
- add "Recovery", "Quick", and "Sprint" workout kinds
|
||||
- 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
|
||||
- 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)
|
||||
- workout templates
|
||||
- provide workout templates in YAML (see docs/workouts.yaml for the structure)
|
||||
- first time setup page
|
||||
- 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
|
||||
- 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"
|
||||
@@ -63,8 +70,9 @@ for that history) and remove it from here once a spec exists.
|
||||
- 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)
|
||||
- manage 2 layouts
|
||||
- list layout with smaller graphs (no scale, no targets)
|
||||
- detailed layout (once clicked on details...) showing graphs for each block incl. delta with workout)
|
||||
|
||||
## Someday / maybe
|
||||
|
||||
|
||||
@@ -1256,8 +1256,8 @@ git commit -m "feat: fix workout taxonomy to 7 types, add pace/HR-zone fields to
|
||||
### Task 7: Classification reads max HR from the profile, not static config
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/internal/sync/service.go` (`Config` struct lines 21-40, `ClassifyActivity` lines 279-310)
|
||||
- Modify: `backend/internal/sync/service_test.go` (`TestFillPendingDetailsAndClassify_EndToEnd`)
|
||||
- Modify: `../../../backend/internal/garmin/sync.go` (`Config` struct lines 21-40, `ClassifyActivity` lines 279-310)
|
||||
- Modify: `../../../backend/internal/garmin/sync_test.go` (`TestFillPendingDetailsAndClassify_EndToEnd`)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `store.Profile.MaxHeartRate` (Task 1), `db.GetProfile` (Task 1).
|
||||
@@ -1265,7 +1265,7 @@ git commit -m "feat: fix workout taxonomy to 7 types, add pace/HR-zone fields to
|
||||
|
||||
- [ ] **Step 1: Update the test first**
|
||||
|
||||
In `backend/internal/sync/service_test.go`, find this line inside `TestFillPendingDetailsAndClassify_EndToEnd` (currently constructs the service with `MaxHR: 190`):
|
||||
In `../../../backend/internal/garmin/sync_test.go`, find this line inside `TestFillPendingDetailsAndClassify_EndToEnd` (currently constructs the service with `MaxHR: 190`):
|
||||
|
||||
```go
|
||||
svc := NewService(m, db, Config{MinConfidence: 0.5, MaxHR: 190}, fixedNow(time.Date(2026, 7, 11, 0, 0, 0, 0, time.UTC)))
|
||||
@@ -1284,7 +1284,7 @@ Expected: passes as-is right now (we haven't removed the field yet) — this ste
|
||||
|
||||
- [ ] **Step 3: Remove `MaxHR` from `Config` and source it from the profile in `ClassifyActivity`**
|
||||
|
||||
In `backend/internal/sync/service.go`, the `Config` struct currently ends with (lines 35-39):
|
||||
In `../../../backend/internal/garmin/sync.go`, the `Config` struct currently ends with (lines 35-39):
|
||||
|
||||
```go
|
||||
// MinConfidence is the classify.Classify threshold below which even a
|
||||
@@ -1381,7 +1381,7 @@ Expected: all PASS. If `cmd/geniusrund/main.go` fails to build because it still
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
cd backend && git add internal/sync/service.go internal/sync/service_test.go
|
||||
cd backend && git add internal/sync/sync.go internal/sync/sync_test.go
|
||||
git commit -m "feat: classification reads max HR from the profile instead of static config"
|
||||
```
|
||||
|
||||
@@ -1390,7 +1390,7 @@ git commit -m "feat: classification reads max HR from the profile instead of sta
|
||||
### Task 8: `internal/config` and `cmd/geniusrund` — credentials come from the profile
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/internal/config/config.go` (full rewrite — small file, shown complete below)
|
||||
- Modify: `../../../backend/internal/config/envconfig.go` (full rewrite — small file, shown complete below)
|
||||
- Modify: `backend/cmd/geniusrund/main.go` (lines 21-46)
|
||||
|
||||
**Interfaces:**
|
||||
@@ -1399,7 +1399,7 @@ git commit -m "feat: classification reads max HR from the profile instead of sta
|
||||
|
||||
- [ ] **Step 1: Rewrite `internal/config/config.go`**
|
||||
|
||||
Replace the full contents of `backend/internal/config/config.go` with:
|
||||
Replace the full contents of `../../../backend/internal/config/envconfig.go` with:
|
||||
|
||||
```go
|
||||
// Package config loads geniusrund's runtime infrastructure configuration
|
||||
@@ -1590,7 +1590,7 @@ Expected: the server starts (no "GARMIN_EMAIL required" error), `/api/profile` r
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
cd backend && git add internal/config/config.go cmd/geniusrund/main.go
|
||||
cd backend && git add internal/config/envconfig.go cmd/geniusrund/main.go
|
||||
git commit -m "feat: source Garmin credentials from the profile instead of env vars"
|
||||
```
|
||||
|
||||
|
||||
@@ -61,15 +61,15 @@ CLAUDE.md [MODIFY] document the new env vars / auth architectur
|
||||
### Task 1: Config — OIDC/session settings
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/internal/config/config.go`
|
||||
- Create: `backend/internal/config/config_test.go`
|
||||
- Modify: `../../../backend/internal/config/envconfig.go`
|
||||
- Create: `../../../backend/internal/config/envconfig_test.go`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `Config.OIDCIssuerURL/OIDCClientID/OIDCClientSecret/OIDCRedirectURL/OIDCRequiredRole string`, `Config.PublicBaseURL string`, `Config.SessionSecret []byte`, `Config.SessionDuration time.Duration`, `Config.SessionSecure bool` — consumed by Task 6 (`main.go`) to build `auth.OIDCConfig` and `api.SessionConfig`.
|
||||
|
||||
- [ ] **Step 1: Write the failing tests**
|
||||
|
||||
Create `backend/internal/config/config_test.go`:
|
||||
Create `../../../backend/internal/config/envconfig_test.go`:
|
||||
|
||||
```go
|
||||
package config
|
||||
@@ -179,7 +179,7 @@ Expected: compile errors (`cfg.OIDCRedirectURL` etc. undefined) — that's the e
|
||||
|
||||
- [ ] **Step 3: Implement the config fields and validation**
|
||||
|
||||
In `backend/internal/config/config.go`, add `"strings"` to the imports, add these fields to `Config` (after `MinConfidence`/`IncrementalSyncEvery`):
|
||||
In `../../../backend/internal/config/envconfig.go`, add `"strings"` to the imports, add these fields to `Config` (after `MinConfidence`/`IncrementalSyncEvery`):
|
||||
|
||||
```go
|
||||
// OIDC login gate (Keycloak). PublicBaseURL is this app's own externally
|
||||
@@ -256,7 +256,7 @@ Expected: PASS (all `TestLoad_*` cases).
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/internal/config/config.go backend/internal/config/config_test.go
|
||||
git add backend/internal/config/envconfig.go backend/internal/config/envconfig_test.go
|
||||
git commit -m "config: add OIDC/session env vars for the login gate"
|
||||
```
|
||||
|
||||
|
||||
@@ -17,25 +17,25 @@
|
||||
- `GARMIN_WRAPPER_PYTHON` is optional, defaulting to `"python3"` (resolved via `PATH`) — `MCP_GARMIN_PYTHON`/`MCP_GARMIN_SERVER` are retired entirely, not renamed.
|
||||
- `garminconnect`'s actual method kwargs must be used exactly: `get_activities_by_date(startdate, enddate)`, `get_activity_splits(activity_id)`, `get_activity_details(activity_id)`, `get_workout_by_id(workout_id)` (confirmed via `inspect.signature` against the installed package — note `startdate`/`enddate`, not `start_date`/`end_date`).
|
||||
- `gofmt -l .` must report nothing; `go build ./...`, `go vet ./...`, and `go test ./...` must all pass before the final commit.
|
||||
- `pytest` must pass in `backend/internal/garmin/pyscript/`.
|
||||
- `pytest` must pass in `../../../backend/internal/garmin/wrapper/`.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Python wrapper (`wrapper.py`) with its own test suite
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/internal/garmin/pyscript/wrapper.py`
|
||||
- Create: `backend/internal/garmin/pyscript/pyproject.toml`
|
||||
- Create: `backend/internal/garmin/pyscript/.gitignore`
|
||||
- Test: `backend/internal/garmin/pyscript/tests/__init__.py`
|
||||
- Test: `backend/internal/garmin/pyscript/tests/test_wrapper.py`
|
||||
- Create: `../../../backend/internal/garmin/wrapper/wrapper.py`
|
||||
- Create: `../../../backend/internal/garmin/wrapper/pyproject.toml`
|
||||
- Create: `../../../backend/internal/garmin/wrapper/.gitignore`
|
||||
- Test: `../../../backend/internal/garmin/wrapper/tests/__init__.py`
|
||||
- Test: `../../../backend/internal/garmin/wrapper/tests/test_wrapper.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `wrapper.dispatch(req: dict) -> dict` — the single entry point Go's subprocess talks to indirectly via stdin/stdout; also `wrapper._auth_state`, `wrapper._client`, `wrapper._mfa_input_queue`, `wrapper._login_result_queue`, `wrapper.Garmin` (re-exported name, patchable in tests), used only within this task's own test suite.
|
||||
|
||||
- [ ] **Step 1: Create the Python project manifest**
|
||||
|
||||
`backend/internal/garmin/pyscript/pyproject.toml`:
|
||||
`../../../backend/internal/garmin/wrapper/pyproject.toml`:
|
||||
|
||||
```toml
|
||||
[project]
|
||||
@@ -55,7 +55,7 @@ requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
```
|
||||
|
||||
`backend/internal/garmin/pyscript/.gitignore`:
|
||||
`../../../backend/internal/garmin/wrapper/.gitignore`:
|
||||
|
||||
```
|
||||
.venv/
|
||||
@@ -68,7 +68,7 @@ __pycache__/
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd backend/internal/garmin/pyscript
|
||||
cd backend/internal/garmin/wrapper
|
||||
python3 -m venv .venv
|
||||
.venv/bin/pip install -e ".[dev]"
|
||||
```
|
||||
@@ -76,9 +76,9 @@ Expected: installs `garminconnect` and `pytest` into `.venv` with no errors.
|
||||
|
||||
- [ ] **Step 3: Write the failing test suite**
|
||||
|
||||
`backend/internal/garmin/pyscript/tests/__init__.py`: empty file.
|
||||
`../../../backend/internal/garmin/wrapper/tests/__init__.py`: empty file.
|
||||
|
||||
`backend/internal/garmin/pyscript/tests/test_wrapper.py`:
|
||||
`../../../backend/internal/garmin/wrapper/tests/test_wrapper.py`:
|
||||
|
||||
```python
|
||||
import os
|
||||
@@ -216,7 +216,7 @@ Expected: `ModuleNotFoundError: No module named 'wrapper'` (or collection failur
|
||||
|
||||
- [ ] **Step 5: Write `wrapper.py`**
|
||||
|
||||
`backend/internal/garmin/pyscript/wrapper.py`:
|
||||
`../../../backend/internal/garmin/wrapper/wrapper.py`:
|
||||
|
||||
```python
|
||||
"""Subprocess wrapper around garminconnect, spoken to over newline-delimited
|
||||
@@ -393,7 +393,7 @@ Expected: all tests PASS.
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/internal/garmin/pyscript
|
||||
git add backend/internal/garmin/wrapper
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(garmin): add direct garminconnect wrapper script
|
||||
|
||||
@@ -584,7 +584,7 @@ Expected: compile failure — `subprocessClient`, `wireResponse`, `roundTrip`, `
|
||||
|
||||
```go
|
||||
// Package garmin wraps a direct garminconnect subprocess (see
|
||||
// pyscript/wrapper.py) as a narrow Go client interface, so the rest of
|
||||
// wrapper/wrapper.py) as a narrow Go client interface, so the rest of
|
||||
// geniusrun never deals with the wire protocol directly.
|
||||
package garmin
|
||||
|
||||
@@ -861,7 +861,7 @@ git add backend/internal/garmin/client.go backend/internal/garmin/client_test.go
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(garmin): replace mcp-go transport with a JSON-lines subprocess client
|
||||
|
||||
subprocessClient spawns the embedded pyscript/wrapper.py over os/exec and
|
||||
subprocessClient spawns the embedded wrapper/wrapper.py over os/exec and
|
||||
speaks newline-delimited JSON instead of MCP. Auth/data methods land in
|
||||
follow-up commits; this is the transport + lifecycle plumbing only.
|
||||
|
||||
@@ -1370,8 +1370,8 @@ EOF
|
||||
### Task 5: Update `internal/config` (drop server-path env var, default the python path)
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/internal/config/config.go`
|
||||
- Modify: `backend/internal/config/config_test.go`
|
||||
- Modify: `../../../backend/internal/config/envconfig.go`
|
||||
- Modify: `../../../backend/internal/config/envconfig_test.go`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: nothing from Tasks 1–4.
|
||||
@@ -1379,7 +1379,7 @@ EOF
|
||||
|
||||
- [ ] **Step 1: Write the failing tests**
|
||||
|
||||
In `backend/internal/config/config_test.go`, update `setRequiredEnv` (remove the two now-optional lines):
|
||||
In `../../../backend/internal/config/envconfig_test.go`, update `setRequiredEnv` (remove the two now-optional lines):
|
||||
|
||||
```go
|
||||
func setRequiredEnv(t *testing.T) {
|
||||
@@ -1425,11 +1425,11 @@ func TestLoad_GarminPythonPathExplicitOverridesDefault(t *testing.T) {
|
||||
- [ ] **Step 2: Run the tests to verify they fail**
|
||||
|
||||
Run: `cd backend && go test ./internal/config/... -v`
|
||||
Expected: `TestLoad_GarminPythonPathDefaultsToPython3` fails (`GarminPythonPath` is currently `""`, and `Load()` currently errors out entirely since `MCP_GARMIN_PYTHON`/`MCP_GARMIN_SERVER` are no longer set by the trimmed `setRequiredEnv`) — every other test in the package should also now fail with "MCP_GARMIN_PYTHON is required", confirming the removed env vars were the only thing missing.
|
||||
Expected: `TestLoad_GarminPythonPathDefaultsToPython3` fails (`PythonPath` is currently `""`, and `Load()` currently errors out entirely since `MCP_GARMIN_PYTHON`/`MCP_GARMIN_SERVER` are no longer set by the trimmed `setRequiredEnv`) — every other test in the package should also now fail with "MCP_GARMIN_PYTHON is required", confirming the removed env vars were the only thing missing.
|
||||
|
||||
- [ ] **Step 3: Update `config.go`**
|
||||
- [ ] **Step 3: Update `envconfig.go`**
|
||||
|
||||
In `backend/internal/config/config.go`, replace the two Garmin subprocess-path fields:
|
||||
In `../../../backend/internal/config/envconfig.go`, replace the two Garmin subprocess-path fields:
|
||||
|
||||
```go
|
||||
// GarminPythonPath is mcp-garmin's venv python executable.
|
||||
@@ -1479,7 +1479,7 @@ Expected: all tests PASS.
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/internal/config/config.go backend/internal/config/config_test.go
|
||||
git add backend/internal/config/envconfig.go backend/internal/config/envconfig_test.go
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(config): drop MCP_GARMIN_SERVER, default GARMIN_WRAPPER_PYTHON
|
||||
|
||||
@@ -1528,7 +1528,7 @@ to:
|
||||
}, appsync.Config{
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update the `GarminBase` doc comment in `server.go`**
|
||||
- [ ] **Step 2: Update the `ClientConfig` doc comment in `server.go`**
|
||||
|
||||
In `backend/internal/api/server.go`, change:
|
||||
|
||||
@@ -1749,4 +1749,4 @@ EOF
|
||||
|
||||
## Follow-up note (not a task in this plan)
|
||||
|
||||
The standalone `mcp-garmin` repo (`/Users/kriss/Dev/scm/scm.vilanet.fr/kriss/mcp-garmin`) is superseded by `backend/internal/garmin/pyscript/` and can be archived once this plan lands — that's a separate-repo action for you to do directly (e.g. marking it archived on your SCM host), not something this plan's tasks touch.
|
||||
The standalone `mcp-garmin` repo (`/Users/kriss/Dev/scm/scm.vilanet.fr/kriss/mcp-garmin`) is superseded by `../../../backend/internal/garmin/wrapper/` and can be archived once this plan lands — that's a separate-repo action for you to do directly (e.g. marking it archived on your SCM host), not something this plan's tasks touch.
|
||||
|
||||
@@ -20,15 +20,15 @@
|
||||
### Task 1: Add `FrontendURL` to `internal/config`
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/internal/config/config.go`
|
||||
- Modify: `backend/internal/config/config_test.go`
|
||||
- Modify: `../../../backend/internal/config/envconfig.go`
|
||||
- Modify: `../../../backend/internal/config/envconfig_test.go`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `Config.FrontendURL string` — set by `Load()`, defaults to `Config.PublicBaseURL` when `GENIUSRUN_FRONTEND_URL` is unset, trimmed of any trailing slash otherwise. Consumed by Task 2's `main.go` wiring.
|
||||
|
||||
- [ ] **Step 1: Write the failing tests**
|
||||
|
||||
Add to `backend/internal/config/config_test.go` (anywhere in the file, e.g. after `TestLoad_GarminPythonPathExplicitOverridesDefault`):
|
||||
Add to `../../../backend/internal/config/envconfig_test.go` (anywhere in the file, e.g. after `TestLoad_GarminPythonPathExplicitOverridesDefault`):
|
||||
|
||||
```go
|
||||
func TestLoad_FrontendURLDefaultsToPublicBaseURL(t *testing.T) {
|
||||
@@ -65,7 +65,7 @@ Expected: compile failure — `Config.FrontendURL` doesn't exist yet.
|
||||
|
||||
- [ ] **Step 3: Add the field and default logic**
|
||||
|
||||
In `backend/internal/config/config.go`, add a new field right after `PublicBaseURL` in the `Config` struct:
|
||||
In `../../../backend/internal/config/envconfig.go`, add a new field right after `PublicBaseURL` in the `Config` struct:
|
||||
|
||||
```go
|
||||
// OIDC login gate (Keycloak). PublicBaseURL is this app's own externally
|
||||
@@ -104,7 +104,7 @@ Expected: all tests PASS, including the two new ones.
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/internal/config/config.go backend/internal/config/config_test.go
|
||||
git add backend/internal/config/envconfig.go backend/internal/config/envconfig_test.go
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(config): add GENIUSRUN_FRONTEND_URL, defaulting to PublicBaseURL
|
||||
|
||||
|
||||
@@ -2223,14 +2223,14 @@ EOF
|
||||
## Task 10: `internal/sync.Service` becomes per-user
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/internal/sync/service.go`
|
||||
- Modify: `backend/internal/sync/service_test.go`
|
||||
- Modify: `../../../backend/internal/garmin/sync.go`
|
||||
- Modify: `../../../backend/internal/garmin/sync_test.go`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: every scoped store method from Tasks 4-8.
|
||||
- Produces: `func NewService(g garmin.Client, db *store.DB, userID int64, cfg Config, now func() time.Time) *Service`. Every other `Service` method (`Backfill`, `IncrementalSync`, `FullSync`, `ResetAll`, `FillPendingDetails`, `ClassifyActivity`, `Progress`) keeps its exact current signature — the userID is baked into the `Service` instance at construction (one `Service` per logged-in user, per the design), so callers in `internal/api` (Tasks 13/15) never pass a userID to these methods directly, only to `NewService` itself.
|
||||
|
||||
- [ ] **Step 1: Add the `userID` field and thread it through every store call in `backend/internal/sync/service.go`**
|
||||
- [ ] **Step 1: Add the `userID` field and thread it through every store call in `../../../backend/internal/garmin/sync.go`**
|
||||
|
||||
Change the `Service` struct and `NewService`:
|
||||
|
||||
@@ -2269,7 +2269,7 @@ Then, in every remaining method, prefix `s.userID` as the new argument to every
|
||||
- `fillActivityDetails`: `s.db.SetActivityWorkout(ctx, a.ID, ...)` → `s.db.SetActivityWorkout(ctx, s.userID, a.ID, ...)`; `s.db.ReplaceActivitySamples(ctx, a.ID, ...)` → `s.db.ReplaceActivitySamples(ctx, s.userID, a.ID, ...)`; `s.db.ReplaceLaps(ctx, a.ID, ...)` → `s.db.ReplaceLaps(ctx, s.userID, a.ID, ...)`; `s.db.SetActivityDetails(ctx, a.ID, ...)` → `s.db.SetActivityDetails(ctx, s.userID, a.ID, ...)`; `s.db.SetActivitySplitsFetched(ctx, a.ID)` → `s.db.SetActivitySplitsFetched(ctx, s.userID, a.ID)`.
|
||||
- `ClassifyActivity`: `s.db.GetActivity(ctx, activityID)` → `s.db.GetActivity(ctx, s.userID, activityID)`; `s.db.LapsForActivity(ctx, activityID)` → `s.db.LapsForActivity(ctx, s.userID, activityID)`; `s.db.ListWorkoutKinds(ctx, true)` → `s.db.ListWorkoutKinds(ctx, s.userID, true)`; `s.db.GetProfile(ctx)` → `s.db.GetProfile(ctx, s.userID)`; `s.db.InsertKindAssignment(ctx, store.KindAssignment{...})` → `s.db.InsertKindAssignment(ctx, s.userID, store.KindAssignment{...})`.
|
||||
|
||||
- [ ] **Step 2: Fix `backend/internal/sync/service_test.go`'s `NewService` calls**
|
||||
- [ ] **Step 2: Fix `../../../backend/internal/garmin/sync_test.go`'s `NewService` calls**
|
||||
|
||||
Every `NewService(m, db, Config{...}, fixedNow(...))` call in this file needs a `userID` inserted as the third argument. Add a shared helper near the top of the file (next to `openTestDB`/`fixedNow`):
|
||||
|
||||
@@ -2355,7 +2355,7 @@ Run: `cd backend && gofmt -l internal/sync/`
|
||||
Expected: no output.
|
||||
|
||||
```bash
|
||||
git add backend/internal/sync/service.go backend/internal/sync/service_test.go
|
||||
git add backend/internal/sync/sync.go backend/internal/sync/sync_test.go
|
||||
git commit -m "$(cat <<'EOF'
|
||||
sync: scope Service to one user per instance
|
||||
|
||||
@@ -2372,12 +2372,12 @@ EOF
|
||||
## Task 11: `internal/config` additions
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/internal/config/config.go`
|
||||
- Modify: `../../../backend/internal/config/envconfig.go`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `Config.GarminTokenStoreRoot` (renamed from `GarminTokenStore`, same env var `GARMIN_TOKENSTORE`, now holds a root directory rather than a single path); `Config.LegacyOwnerOIDCSub` (new, optional, env var `GENIUSRUN_LEGACY_OWNER_OIDC_SUB`). Task 13's `main.go` wiring consumes both.
|
||||
|
||||
- [ ] **Step 1: Rename `GarminTokenStore` → `GarminTokenStoreRoot` and add `LegacyOwnerOIDCSub`**
|
||||
- [ ] **Step 1: Rename `GarminTokenStore` → `TokenStoreRoot` and add `LegacyOwnerOIDCSub`**
|
||||
|
||||
In the `Config` struct, change:
|
||||
|
||||
@@ -2427,7 +2427,7 @@ Run: `cd backend && gofmt -l internal/config/`
|
||||
Expected: no output.
|
||||
|
||||
```bash
|
||||
git add backend/internal/config/config.go
|
||||
git add backend/internal/config/envconfig.go
|
||||
git commit -m "$(cat <<'EOF'
|
||||
config: add per-user Garmin token store root and legacy-owner bootstrap var
|
||||
|
||||
@@ -2442,17 +2442,17 @@ EOF
|
||||
## Task 12: User-resolution middleware + `POST /api/setup`
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/internal/api/usercontext.go`
|
||||
- Create: `../../../backend/internal/api/user.go`
|
||||
- Create: `backend/internal/api/setup.go`
|
||||
- Modify: `backend/internal/api/session.go` (extend `sessionMeResponse`, `handleSessionMe`)
|
||||
- Create: `backend/internal/api/usercontext_test.go`
|
||||
- Create: `../../../backend/internal/api/user_test.go`
|
||||
- Create: `backend/internal/api/setup_test.go`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `store.GetUserBySub`/`store.ProvisionUser` (Task 2), `auth.ClaimsFromContext` (existing).
|
||||
- Produces: `func (s *Server) resolveUser(next http.Handler) http.Handler` (middleware); `func requireProvisionedUser(next http.Handler) http.Handler` (middleware); `func userFromContext(ctx context.Context) (resolvedUser, bool)`; `func userIDFromContext(ctx context.Context) int64`; `func (s *Server) handleSetup(w http.ResponseWriter, r *http.Request)`. Task 13 wires all of these into the router; every handler task (14/15) calls `userIDFromContext`.
|
||||
|
||||
- [ ] **Step 1: Write the failing tests in `backend/internal/api/usercontext_test.go`**
|
||||
- [ ] **Step 1: Write the failing tests in `../../../backend/internal/api/user_test.go`**
|
||||
|
||||
Since `auth`'s session context key is unexported, this test drives the real chain (`auth.RequireSession` then `s.resolveUser`) via a minted session cookie — the same `doJSON` helper `api_test.go` already uses for every other handler test — rather than trying to poke the context directly.
|
||||
|
||||
@@ -2521,7 +2521,7 @@ func TestRequireProvisionedUser_RejectsWhenNoUserResolved(t *testing.T) {
|
||||
Run: `cd backend && go vet ./internal/api/...`
|
||||
Expected: FAIL — `s.resolveUser`/`userFromContext`/`requireProvisionedUser` undefined.
|
||||
|
||||
- [ ] **Step 3: Write `backend/internal/api/usercontext.go`**
|
||||
- [ ] **Step 3: Write `../../../backend/internal/api/user.go`**
|
||||
|
||||
```go
|
||||
package api
|
||||
@@ -2790,7 +2790,7 @@ Run: `cd backend && gofmt -l internal/api/`
|
||||
Expected: no output.
|
||||
|
||||
```bash
|
||||
git add backend/internal/api/usercontext.go backend/internal/api/usercontext_test.go backend/internal/api/setup.go backend/internal/api/setup_test.go backend/internal/api/session.go backend/internal/api/server.go
|
||||
git add backend/internal/api/user.go backend/internal/api/user_test.go backend/internal/api/setup.go backend/internal/api/setup_test.go backend/internal/api/session.go backend/internal/api/server.go
|
||||
git commit -m "$(cat <<'EOF'
|
||||
api: add user-resolution middleware and POST /api/setup
|
||||
|
||||
@@ -3302,7 +3302,7 @@ func TestSetup_RejectsEmptyDisplayName(t *testing.T) {
|
||||
- [ ] **Step 4: Build everything and fix remaining call sites**
|
||||
|
||||
Run: `cd backend && go build ./... 2>&1 | head -50`
|
||||
Expected: remaining errors are all inside `internal/api/*.go` handler files (`profile.go`, `activities.go`, `sync.go`, `kinds.go`, `review.go`, `reclassify.go`, `progression.go`, `auth.go`) and `cmd/seedsample/main.go` — referencing `s.Garmin`/`s.Sync` (now removed) or store methods missing `userID`. These are fixed in Tasks 14/15/17; confirm no errors remain in `server.go`, `main.go`, or `api_test.go` itself.
|
||||
Expected: remaining errors are all inside `internal/api/*.go` handler files (`profile.go`, `activities.go`, `sync.go`, `kinds.go`, `review.go`, `reclassify.go`, `progression.go`, `garmin.go`) and `cmd/seedsample/main.go` — referencing `s.Garmin`/`s.Sync` (now removed) or store methods missing `userID`. These are fixed in Tasks 14/15/17; confirm no errors remain in `server.go`, `main.go`, or `api_test.go` itself.
|
||||
|
||||
- [ ] **Step 5: `gofmt` and commit**
|
||||
|
||||
@@ -3325,12 +3325,12 @@ EOF
|
||||
|
||||
---
|
||||
|
||||
## Task 14: Thread `userID` into `profile.go`, `kinds.go`, `auth.go`, `progression.go`
|
||||
## Task 14: Thread `userID` into `profile.go`, `kinds.go`, `garmin.go`, `progression.go`
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/internal/api/profile.go`
|
||||
- Modify: `backend/internal/api/kinds.go`
|
||||
- Modify: `backend/internal/api/auth.go`
|
||||
- Modify: `../../../backend/internal/api/garmin.go`
|
||||
- Modify: `backend/internal/api/progression.go`
|
||||
- Modify: `backend/internal/api/api_test.go` (no new call-site changes expected beyond what Task 13 already made — this task only touches handler bodies, not test bodies, since the HTTP-level test assertions are unchanged)
|
||||
|
||||
@@ -3508,7 +3508,7 @@ func (s *Server) handleUpdateWorkoutKind(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update `backend/internal/api/auth.go`**
|
||||
- [ ] **Step 3: Update `../../../backend/internal/api/garmin.go`**
|
||||
|
||||
Keep `authResponse`, `authStatusString` unchanged; replace `recordAuthResult` and the three handlers:
|
||||
|
||||
@@ -3636,7 +3636,7 @@ Run: `cd backend && gofmt -l internal/api/`
|
||||
Expected: no output.
|
||||
|
||||
```bash
|
||||
git add backend/internal/api/profile.go backend/internal/api/kinds.go backend/internal/api/auth.go backend/internal/api/progression.go
|
||||
git add backend/internal/api/profile.go backend/internal/api/kinds.go backend/internal/api/garmin.go backend/internal/api/progression.go
|
||||
git commit -m "$(cat <<'EOF'
|
||||
api: scope profile, workout-kind, Garmin auth, and progression handlers to userID
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
**Goal:** Make connecting a Garmin account a mandatory, persisted gate during onboarding (and on every subsequent login until it succeeds), instead of an optional step left to the Profile page.
|
||||
|
||||
**Architecture:** Add a nullable `garmin_connected_at` timestamp to `profile`, set once on the first successful Garmin authentication (`handleAuthLogin`/`handleAuthMFA`). Expose it via `GET /api/session/me`. `LoginGate.tsx` becomes a three-way fork (`!has_profile` → `CreateProfile`, `has_profile && !garmin_connected` → new `ConnectGarmin`, else → `App`), so a user who abandons the connect step re-enters at `ConnectGarmin` on their next login rather than skipping straight into the app.
|
||||
**Architecture:** Add a nullable `garmin_connected_at` timestamp to `profile`, set once on the first successful Garmin authentication (`handleGarminAuthLogin`/`handleGarminAuthMFA`). Expose it via `GET /api/session/me`. `LoginGate.tsx` becomes a three-way fork (`!has_profile` → `CreateProfile`, `has_profile && !garmin_connected` → new `ConnectGarmin`, else → `App`), so a user who abandons the connect step re-enters at `ConnectGarmin` on their next login rather than skipping straight into the app.
|
||||
|
||||
**Tech Stack:** Go (`database/sql` via `modernc.org/sqlite`, chi router), React + TypeScript (Vite), no frontend test framework.
|
||||
|
||||
@@ -246,7 +246,7 @@ git commit -m "feat(store): persist garmin_connected_at, set once on first succe
|
||||
### Task 2: Wire `MarkGarminConnected` into the auth handlers and expose it via session/me
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/internal/api/auth.go` (`recordAuthResult`, both call sites)
|
||||
- Modify: `../../../backend/internal/api/garmin.go` (`recordAuthResult`, both call sites)
|
||||
- Modify: `backend/internal/api/session.go` (`sessionMeResponse`, `handleSessionMe`)
|
||||
- Modify: `backend/internal/api/api_test.go` (new tests)
|
||||
- Modify: `backend/internal/api/isolation_test.go` (new adversarial test)
|
||||
@@ -342,7 +342,7 @@ func TestIsolation_GarminConnectedNeverLeaksAcrossUsers(t *testing.T) {
|
||||
Run: `cd backend && go test ./internal/api/... -run 'TestSessionMe_ReportsGarminConnected|TestSessionMe_GarminConnectedStaysFalse|TestIsolation_GarminConnected' -v`
|
||||
Expected: FAIL — `sessionMeResponse` has no field `GarminConnected`.
|
||||
|
||||
- [ ] **Step 3: Update `recordAuthResult` in `auth.go`**
|
||||
- [ ] **Step 3: Update `recordAuthResult` in `garmin.go`**
|
||||
|
||||
Change the imports:
|
||||
|
||||
@@ -379,8 +379,8 @@ func (s *Server) recordAuthResult(ctx context.Context, userID int64, res garmin.
|
||||
}
|
||||
```
|
||||
|
||||
In `handleAuthLogin`, change `s.recordAuthResult(userID, res)` to `s.recordAuthResult(r.Context(), userID, res)`.
|
||||
In `handleAuthMFA`, change `s.recordAuthResult(userID, res)` to `s.recordAuthResult(r.Context(), userID, res)`.
|
||||
In `handleGarminAuthLogin`, change `s.recordAuthResult(userID, res)` to `s.recordAuthResult(r.Context(), userID, res)`.
|
||||
In `handleGarminAuthMFA`, change `s.recordAuthResult(userID, res)` to `s.recordAuthResult(r.Context(), userID, res)`.
|
||||
|
||||
- [ ] **Step 4: Update `sessionMeResponse` and `handleSessionMe` in `session.go`**
|
||||
|
||||
@@ -446,7 +446,7 @@ Expected: `gofmt -l .` empty; everything else passes.
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/internal/api/auth.go backend/internal/api/session.go backend/internal/api/api_test.go backend/internal/api/isolation_test.go
|
||||
git add backend/internal/api/garmin.go backend/internal/api/session.go backend/internal/api/api_test.go backend/internal/api/isolation_test.go
|
||||
git commit -m "feat(api): persist and expose garmin_connected on successful auth"
|
||||
```
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ frontend `SyncModal` component polls it and replaces all inline sync UI in `Garm
|
||||
- Frontend: `npm run build` (`tsc -b && vite build`) and `npm run lint` (oxlint) must be clean
|
||||
before any commit. No frontend test suite exists -- verify new UI manually in a browser against
|
||||
`cmd/seedsample` data.
|
||||
- Reset all (`ResetAll`/`handleSyncReset`) is explicitly out of scope -- do not touch it beyond
|
||||
- Reset all (`ResetAll`/`handleGarminSyncReset`) is explicitly out of scope -- do not touch it beyond
|
||||
what's incidentally required (none is required).
|
||||
- Follow this repo's existing JSON convention exactly: hand-built response maps use snake_case
|
||||
keys (`in_progress`, `activities_pending_details`), but a Go struct serialized directly (like
|
||||
@@ -40,7 +40,7 @@ frontend `SyncModal` component polls it and replaces all inline sync UI in `Garm
|
||||
- Modify: `backend/internal/sync/service.go:99-123` (delete `Backfill`), `:194-209` (delete
|
||||
`IncrementalSync`), `:224-232` (fix `FullSync`'s doc comment)
|
||||
- Modify: `backend/internal/store/syncruns.go:9-11` (delete `SyncKindBackfill`/`SyncKindIncremental`)
|
||||
- Modify: `backend/internal/sync/service_test.go` (rename/rewrite tests listed below)
|
||||
- Modify: `../../../backend/internal/garmin/sync_test.go` (rename/rewrite tests listed below)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: nothing new.
|
||||
@@ -58,7 +58,7 @@ check, not new information).
|
||||
|
||||
- [ ] **Step 2: Delete `Backfill` and `IncrementalSync`, fix `FullSync`'s doc comment**
|
||||
|
||||
In `backend/internal/sync/service.go`, delete this entire method (lines 99-123):
|
||||
In `../../../backend/internal/garmin/sync.go`, delete this entire method (lines 99-123):
|
||||
|
||||
```go
|
||||
// Backfill pages backward in Config.BackfillWindowDays windows until
|
||||
@@ -187,7 +187,7 @@ const (
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Update `backend/internal/sync/service_test.go`'s callers**
|
||||
- [ ] **Step 4: Update `../../../backend/internal/garmin/sync_test.go`'s callers**
|
||||
|
||||
Rename and rewrite (drop the redundant SyncRun assertion -- `TestFullSync_RecordsOneCombinedSyncRun`
|
||||
already covers SyncRun recording thoroughly):
|
||||
@@ -261,7 +261,7 @@ Expected: all packages pass, `gofmt -l .` prints nothing.
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/internal/sync/service.go backend/internal/sync/service_test.go backend/internal/store/syncruns.go
|
||||
git add backend/internal/sync/sync.go backend/internal/sync/sync_test.go backend/internal/store/syncruns.go
|
||||
git commit -m "$(cat <<'EOF'
|
||||
refactor(sync): remove dead Backfill/IncrementalSync exported wrappers
|
||||
|
||||
@@ -475,7 +475,7 @@ EOF
|
||||
`fillActivityWorkout`)
|
||||
- Modify: `backend/internal/sync/mapping.go:113-125` (`alignWorkoutTargets` signature)
|
||||
- Modify: `backend/internal/garmin/mock/mock.go` (add `WorkoutErrByID`)
|
||||
- Modify: `backend/internal/sync/service_test.go` (update `alignWorkoutTargets` call sites,
|
||||
- Modify: `../../../backend/internal/garmin/sync_test.go` (update `alignWorkoutTargets` call sites,
|
||||
rewrite `TestFillPendingDetails_ReportsLiveProgress`, add new tests)
|
||||
|
||||
**Interfaces:**
|
||||
@@ -490,7 +490,7 @@ EOF
|
||||
`alignWorkoutTargets` only ever uses `len(laps)`, never any lap's actual content -- change its
|
||||
signature to take the count directly, since Task 3's `fillActivityWorkout` (added in Step 4)
|
||||
needs to call it without holding a `[]garmin.Lap` (it only has `[]store.Lap` read back from the
|
||||
DB). In `backend/internal/sync/service_test.go`, update all three call sites:
|
||||
DB). In `../../../backend/internal/garmin/sync_test.go`, update all three call sites:
|
||||
|
||||
```go
|
||||
targets := alignWorkoutTargets(len(laps), workout)
|
||||
@@ -509,7 +509,7 @@ a `[]garmin.Lap`, not an `int` -- `len(laps)` is an `int`, mismatched argument t
|
||||
|
||||
- [ ] **Step 3: Change `alignWorkoutTargets`'s signature**
|
||||
|
||||
In `backend/internal/sync/mapping.go`, replace:
|
||||
In `../../../backend/internal/garmin/mapping.go`, replace:
|
||||
|
||||
```go
|
||||
func alignWorkoutTargets(laps []garmin.Lap, workout garmin.Workout) []*garmin.WorkoutStep {
|
||||
@@ -553,7 +553,7 @@ one-extra-trailing-lap case stays accurate as-is.)
|
||||
- [ ] **Step 4: Run to verify it passes**
|
||||
|
||||
Run: `cd backend && go test ./internal/sync/... -run TestAlignWorkoutTargets -v`
|
||||
Expected: PASS (note: `fillActivityDetails` in `service.go` still calls
|
||||
Expected: PASS (note: `fillActivityDetails` in `sync.go` still calls
|
||||
`alignWorkoutTargets(splits.Laps, workout)` at this point -- that call site is rewritten in Step 6
|
||||
below, so the package won't fully build until then; run this test with `-run
|
||||
TestAlignWorkoutTargets` specifically, or expect a build failure from the not-yet-updated call
|
||||
@@ -561,7 +561,7 @@ site if running the whole package).
|
||||
|
||||
- [ ] **Step 5: Add phase-aware `Progress` and update `FullSync`**
|
||||
|
||||
In `backend/internal/sync/service.go`, replace:
|
||||
In `../../../backend/internal/garmin/sync.go`, replace:
|
||||
|
||||
```go
|
||||
// Progress reports how far a currently-running (or just-finished)
|
||||
@@ -843,7 +843,7 @@ func (c *Client) GetWorkoutByID(ctx context.Context, workoutID int64) (garmin.Wo
|
||||
|
||||
- [ ] **Step 8: Rewrite `TestFillPendingDetails_ReportsLiveProgress` for phase-awareness, add two new tests**
|
||||
|
||||
Replace `TestFillPendingDetails_ReportsLiveProgress` in `backend/internal/sync/service_test.go`
|
||||
Replace `TestFillPendingDetails_ReportsLiveProgress` in `../../../backend/internal/garmin/sync_test.go`
|
||||
with:
|
||||
|
||||
```go
|
||||
@@ -1045,9 +1045,9 @@ func TestFillPendingDetails_OneWorkoutFetchFailureDoesNotAbortTheWorkoutsPass(t
|
||||
}
|
||||
```
|
||||
|
||||
`fmt` is already imported in `service_test.go`'s package (`package sync` imports it transitively
|
||||
`fmt` is already imported in `sync_test.go`'s package (`package sync` imports it transitively
|
||||
via other test helpers)? Check: if `go vet`/`go build` complains `fmt` is undefined in
|
||||
`service_test.go`, add `"fmt"` to that file's import block.
|
||||
`sync_test.go`, add `"fmt"` to that file's import block.
|
||||
|
||||
- [ ] **Step 9: Run the full `internal/sync` test suite**
|
||||
|
||||
@@ -1063,7 +1063,7 @@ Expected: all pass, `gofmt -l .` prints nothing.
|
||||
- [ ] **Step 11: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/internal/sync/service.go backend/internal/sync/service_test.go backend/internal/sync/mapping.go backend/internal/garmin/mock/mock.go
|
||||
git add backend/internal/sync/sync.go backend/internal/sync/sync_test.go backend/internal/sync/mapping.go backend/internal/garmin/mock/mock.go
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(sync): split FillPendingDetails into activities/workouts phases
|
||||
|
||||
@@ -1095,7 +1095,7 @@ EOF
|
||||
### Task 4: Reshape `GET /api/sync/status` and update frontend types
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/internal/api/sync.go:69-102` (`handleSyncStatus`)
|
||||
- Modify: `backend/internal/api/sync.go:69-102` (`handleGarminSyncStatus`)
|
||||
- Modify: `backend/internal/api/api_test.go` (add a new test)
|
||||
- Modify: `frontend/src/types/api.ts:197-207` (`DetailFillProgress`/`SyncStatus`, `SyncRun.Kind`)
|
||||
|
||||
@@ -1165,7 +1165,7 @@ handler doesn't produce `workouts_pending` or a nested `progress` object yet (st
|
||||
|
||||
- [ ] **Step 3: Update the handler**
|
||||
|
||||
In `backend/internal/api/sync.go`, replace `handleSyncStatus`:
|
||||
In `backend/internal/api/sync.go`, replace `handleGarminSyncStatus`:
|
||||
|
||||
```go
|
||||
func (s *Server) handleSyncStatus(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -1526,7 +1526,7 @@ EOF
|
||||
- [ ] **Step 1: Replace the whole file**
|
||||
|
||||
`GarminConnection.tsx` keeps its connect/MFA/Reset-all logic completely unchanged. It loses:
|
||||
`syncStatus` state, `syncStatusRef`, the `syncProgressLabel` helper, `syncStatus` polling inside
|
||||
`garminSyncStatus` state, `syncStatusRef`, the `syncProgressLabel` helper, `garminSyncStatus` polling inside
|
||||
`refreshStatus`/the mount `useEffect`, and all inline sync-progress/last-sync JSX. It gains a
|
||||
`showSyncModal` boolean and renders `<SyncModal>` when true.
|
||||
|
||||
|
||||
@@ -368,7 +368,7 @@ import (
|
||||
)
|
||||
```
|
||||
|
||||
Add the `setupGarmin` field to `Server` and initialize it in `NewServer`:
|
||||
Add the `setupSession` field to `Server` and initialize it in `NewServer`:
|
||||
|
||||
```go
|
||||
mu sync.Mutex
|
||||
@@ -395,7 +395,7 @@ Add the `setupGarmin` field to `Server` and initialize it in `NewServer`:
|
||||
}
|
||||
```
|
||||
|
||||
Insert this block right after `NewServer` (before `garminFor`):
|
||||
Insert this block right after `NewServer` (before `clientFor`):
|
||||
|
||||
```go
|
||||
|
||||
|
||||
@@ -270,7 +270,7 @@ git commit -m "feat(store): add DeleteUser with cascading account deletion"
|
||||
### Task 2: `DELETE /api/profile` and Garmin client/tokenstore teardown
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/internal/api/server.go:5-21` (imports), `:95-117` (new `removeGarminClient` after `syncFor`), `:167-170` (route)
|
||||
- Modify: `backend/internal/api/server.go:5-21` (imports), `:95-117` (new `removeUserClient` after `syncFor`), `:167-170` (route)
|
||||
- Modify: `backend/internal/api/profile.go` (add `handleDeleteProfile`)
|
||||
- Modify: `backend/internal/api/api_test.go` (imports + 3 new tests)
|
||||
- Modify: `backend/internal/api/isolation_test.go` (1 new test)
|
||||
@@ -427,7 +427,7 @@ func TestIsolation_DeleteProfileOnlyDeletesOwnAccount(t *testing.T) {
|
||||
Run: `cd backend && go test ./internal/api/... -run 'TestDeleteProfile|TestIsolation_DeleteProfile' -v`
|
||||
Expected: FAIL — 404s (no `DELETE /api/profile` route registered yet).
|
||||
|
||||
- [ ] **Step 3: Add the `os` import and `removeGarminClient` to `server.go`**
|
||||
- [ ] **Step 3: Add the `os` import and `removeUserClient` to `server.go`**
|
||||
|
||||
Change the import block (add `"os"` between `"net/http"` and `"path/filepath"`):
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
**Goal:** Emit structured JSON logs on stdout for (1) every HTTP API request and (2) every Garmin wrapper subprocess call, correlated by a per-request id — without touching any existing `log.Printf` call site.
|
||||
|
||||
**Architecture:** A new `internal/applog` package wraps `log/slog` with a JSON handler plus context helpers (`WithLogger`/`FromContext`). `internal/api` gains a `requestLoggingMiddleware` that logs one line per HTTP request and stashes a request-id-tagged logger into the request context. `internal/garmin`'s `client.go` reads that same logger back out of context (via the `ctx` every `Client` method already receives) inside `roundTrip`, the single funnel point all six Garmin calls go through, logging one line per subprocess round-trip.
|
||||
**Architecture:** A new `internal/applog` package wraps `log/slog` with a JSON handler plus context helpers (`WithLogger`/`FromContext`). `internal/api` gains a `loggingMiddleware` that logs one line per HTTP request and stashes a request-id-tagged logger into the request context. `internal/garmin`'s `client.go` reads that same logger back out of context (via the `ctx` every `Client` method already receives) inside `roundTrip`, the single funnel point all six Garmin calls go through, logging one line per subprocess round-trip.
|
||||
|
||||
**Tech Stack:** Go stdlib `log/slog` (no new dependency), `github.com/go-chi/chi/v5/middleware` (already-available subpackage of the existing chi dependency).
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/internal/applog/applog.go`
|
||||
- Create: `backend/internal/applog/applog_test.go`
|
||||
- Create: `../../../backend/internal/log/log_test.go`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `func NewLogger(level string, w io.Writer) *slog.Logger`, `func WithLogger(ctx context.Context, logger *slog.Logger) context.Context`, `func FromContext(ctx context.Context) *slog.Logger` (never nil — falls back to `slog.Default()`).
|
||||
@@ -91,7 +91,7 @@ func TestFromContext_DefaultsWhenNoneSet(t *testing.T) {
|
||||
Run: `cd backend && go test ./internal/applog/... -v`
|
||||
Expected: FAIL — package `internal/applog` doesn't exist yet (build failure).
|
||||
|
||||
- [ ] **Step 3: Create `backend/internal/applog/applog.go`**
|
||||
- [ ] **Step 3: Create `../../../backend/internal/log/log.go`**
|
||||
|
||||
```go
|
||||
// Package applog provides geniusrun's structured JSON logging: a
|
||||
@@ -164,19 +164,19 @@ git commit -m "feat(applog): add JSON logger and context helpers"
|
||||
### Task 2: HTTP access log middleware
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/internal/config/config.go` (new `LogLevel` field)
|
||||
- Modify: `backend/internal/config/config_test.go` (new tests)
|
||||
- Modify: `../../../backend/internal/config/envconfig.go` (new `LogLevel` field)
|
||||
- Modify: `../../../backend/internal/config/envconfig_test.go` (new tests)
|
||||
- Modify: `backend/cmd/geniusrund/main.go` (wire up `slog.SetDefault`)
|
||||
- Modify: `backend/internal/api/server.go` (`requestLoggingMiddleware`, registered in `Router()`)
|
||||
- Modify: `backend/internal/api/server.go` (`loggingMiddleware`, registered in `Router()`)
|
||||
- Modify: `backend/internal/api/api_test.go` (new tests)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `applog.NewLogger`, `applog.WithLogger`, `applog.FromContext` (Task 1).
|
||||
- Produces: `requestLoggingMiddleware` (unexported chi middleware in `internal/api`), `config.Config.LogLevel string`.
|
||||
- Produces: `loggingMiddleware` (unexported chi middleware in `internal/api`), `config.Config.LogLevel string`.
|
||||
|
||||
- [ ] **Step 1: Write the failing config tests**
|
||||
|
||||
Append to `backend/internal/config/config_test.go`:
|
||||
Append to `../../../backend/internal/config/envconfig_test.go`:
|
||||
|
||||
```go
|
||||
func TestLoad_LogLevelDefaultsToInfo(t *testing.T) {
|
||||
@@ -276,7 +276,7 @@ func TestRequestLoggingMiddleware_5xxLogsAtWarnLevel(t *testing.T) {
|
||||
Run: `cd backend && go test ./internal/config/... ./internal/api/... -run 'TestLoad_LogLevel|TestRequestLoggingMiddleware' -v`
|
||||
Expected: FAIL — `LogLevel` field doesn't exist on `Config`; `applog` import unresolved; no log output produced (middleware doesn't exist).
|
||||
|
||||
- [ ] **Step 4: Add `LogLevel` to `config.go`**
|
||||
- [ ] **Step 4: Add `LogLevel` to `envconfig.go`**
|
||||
|
||||
Change:
|
||||
|
||||
@@ -471,7 +471,7 @@ Expected: `gofmt -l .` empty; everything passes.
|
||||
- [ ] **Step 9: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/internal/config/config.go backend/internal/config/config_test.go backend/cmd/geniusrund/main.go backend/internal/api/server.go backend/internal/api/api_test.go
|
||||
git add backend/internal/config/envconfig.go backend/internal/config/envconfig_test.go backend/cmd/geniusrund/main.go backend/internal/api/server.go backend/internal/api/api_test.go
|
||||
git commit -m "feat(api): add structured JSON access log with request-id correlation"
|
||||
```
|
||||
|
||||
|
||||
@@ -37,8 +37,8 @@ missing" from "not yet fetched."
|
||||
### Task 1: `wrapper.py` marks a 404 with `not_found: true`
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/internal/garmin/pyscript/wrapper.py` (imports, `dispatch`)
|
||||
- Modify: `backend/internal/garmin/pyscript/tests/test_wrapper.py` (new test)
|
||||
- Modify: `../../../backend/internal/garmin/wrapper/wrapper.py` (imports, `dispatch`)
|
||||
- Modify: `../../../backend/internal/garmin/wrapper/tests/test_wrapper.py` (new test)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `garminconnect.GarminConnectNotFoundError` (already installed as a dependency).
|
||||
@@ -48,7 +48,7 @@ missing" from "not yet fetched."
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Add to `backend/internal/garmin/pyscript/tests/test_wrapper.py`:
|
||||
Add to `../../../backend/internal/garmin/wrapper/tests/test_wrapper.py`:
|
||||
|
||||
```python
|
||||
def test_call_marks_not_found_error_specifically():
|
||||
@@ -77,14 +77,14 @@ def test_call_does_not_mark_other_errors_as_not_found():
|
||||
|
||||
Run: `cd backend/internal/garmin/pyscript && python3 -m pytest tests/test_wrapper.py -k not_found -v`
|
||||
(use whatever Python interpreter this repo's wrapper tests normally run under -- see
|
||||
`backend/internal/garmin/pyscript/.venv` if one exists, or the `GARMIN_WRAPPER_PYTHON` convention).
|
||||
`../../../backend/internal/garmin/wrapper/.venv` if one exists, or the `GARMIN_WRAPPER_PYTHON` convention).
|
||||
Expected: `test_call_marks_not_found_error_specifically` FAILS (`resp` has no `"not_found"` key
|
||||
yet); `test_call_does_not_mark_other_errors_as_not_found` already passes (nothing to change for
|
||||
that case).
|
||||
|
||||
- [ ] **Step 3: Update `dispatch()`**
|
||||
|
||||
In `backend/internal/garmin/pyscript/wrapper.py`, replace:
|
||||
In `../../../backend/internal/garmin/wrapper/wrapper.py`, replace:
|
||||
|
||||
```python
|
||||
from garminconnect import Garmin
|
||||
@@ -144,7 +144,7 @@ Expected: all tests PASS, including both new ones and every existing test unchan
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/internal/garmin/pyscript/wrapper.py backend/internal/garmin/pyscript/tests/test_wrapper.py
|
||||
git add backend/internal/garmin/wrapper/wrapper.py backend/internal/garmin/wrapper/tests/test_wrapper.py
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(garmin): mark a wrapper 404 with not_found in the error response
|
||||
|
||||
@@ -659,8 +659,8 @@ EOF
|
||||
### Task 4: `fillPendingWorkouts` stops retrying a confirmed 404
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/internal/sync/service.go` (`fillPendingWorkouts`)
|
||||
- Modify: `backend/internal/sync/service_test.go` (new test)
|
||||
- Modify: `../../../backend/internal/garmin/sync.go` (`fillPendingWorkouts`)
|
||||
- Modify: `../../../backend/internal/garmin/sync_test.go` (new test)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `garmin.ErrNotFound` (Task 2), `db.SetActivityWorkoutNotFound` (Task 3).
|
||||
@@ -668,7 +668,7 @@ EOF
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Add to `backend/internal/sync/service_test.go`, right after
|
||||
Add to `../../../backend/internal/garmin/sync_test.go`, right after
|
||||
`TestFillPendingDetails_OneWorkoutFetchFailureDoesNotAbortTheWorkoutsPass`:
|
||||
|
||||
```go
|
||||
@@ -743,7 +743,7 @@ regardless of the error's nature).
|
||||
|
||||
- [ ] **Step 3: Update `fillPendingWorkouts`**
|
||||
|
||||
In `backend/internal/sync/service.go`, replace:
|
||||
In `../../../backend/internal/garmin/sync.go`, replace:
|
||||
|
||||
```go
|
||||
for i, a := range pending {
|
||||
@@ -794,7 +794,7 @@ with:
|
||||
}
|
||||
```
|
||||
|
||||
Add `"errors"` to `service.go`'s import block.
|
||||
Add `"errors"` to `sync.go`'s import block.
|
||||
|
||||
- [ ] **Step 4: Run the test to verify it passes**
|
||||
|
||||
@@ -817,7 +817,7 @@ Expected: all pass, `gofmt -l .` prints nothing.
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/internal/sync/service.go backend/internal/sync/service_test.go
|
||||
git add backend/internal/sync/sync.go backend/internal/sync/sync_test.go
|
||||
git commit -m "$(cat <<'EOF'
|
||||
fix(sync): stop retrying a workout Garmin confirms is gone
|
||||
|
||||
|
||||
1017
docs/superpowers/plans/2026-08-03-configuration.md
Normal file
1017
docs/superpowers/plans/2026-08-03-configuration.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -23,7 +23,7 @@ This replaces the MCP transport with a small custom Python subprocess wrapper ar
|
||||
- No change to what data geniusrun actually syncs/stores/classifies today — only the transport between Go and Python changes. `internal/sync`, `internal/classify`, `internal/store`, and the frontend are untouched.
|
||||
- No security sandboxing of the generic dispatcher (see below) — the subprocess is spoken to only by geniusrun's own Go process over a private pipe it owns, not exposed to any other caller, so an open dispatch surface is an accepted risk, not a gap.
|
||||
- No auto-restart-on-crash logic for the subprocess. Matches today's behavior: a dead subprocess surfaces as a Go error on next use; only `UpdateCredentials` explicitly tears down and respawns.
|
||||
- No change to the per-user token store layout (`{GarminTokenStoreRoot}/{userID}`) or the multi-tenant `garminFor` caching in `api.Server`.
|
||||
- No change to the per-user token store layout (`{GarminTokenStoreRoot}/{userID}`) or the multi-tenant `clientFor` caching in `api.Server`.
|
||||
|
||||
## Wire protocol
|
||||
|
||||
@@ -99,5 +99,5 @@ Go's line reader uses a raised buffer size (well above the default 64KB `bufio.S
|
||||
## Rollout notes
|
||||
|
||||
- No data migration involved — this only changes how the Go backend talks to Garmin, not what's stored.
|
||||
- Existing per-user token stores under `GarminTokenStoreRoot` are unaffected (same `GARMIN_TOKENSTORE` env var name passed to the subprocess, same directory layout) — no re-login required for existing users after cutover.
|
||||
- Existing per-user token stores under `TokenStoreRoot` are unaffected (same `GARMIN_TOKENSTORE` env var name passed to the subprocess, same directory layout) — no re-login required for existing users after cutover.
|
||||
- Deploy order: ship the new wrapper + Go client together as one change (interface unchanged, so this is an internal swap, not a rolling/compat-sensitive migration).
|
||||
|
||||
@@ -85,7 +85,7 @@ A new middleware step runs immediately after the existing `RequireSession` (role
|
||||
`internal/garmin.Client` and `internal/sync.Service` already take their config/dependencies as constructor params with no hidden global state, so per-user instantiation is mechanical:
|
||||
|
||||
- `api.Server`'s current single fixed `Garmin`/`Sync` fields and flat Garmin-auth-status fields (today: plain fields on `Server`, single-user assumption) become two mutex-guarded maps: `map[int64]*garmin.Client` and `map[int64]*sync.Service`, keyed by `user_id`, lazily constructed on first access for a given user.
|
||||
- A per-user `garmin.Client` is built from that user's own `profile` row (`GarminEmail`/`GarminPassword`) and a per-user token store path: `filepath.Join(cfg.GarminTokenStoreRoot, strconv.FormatInt(userID, 10))`. `GarminTokenStore` in `internal/config` is renamed/repurposed to `GarminTokenStoreRoot` (a directory, not a single tokenstore path) to reflect this.
|
||||
- A per-user `garmin.Client` is built from that user's own `profile` row (`GarminEmail`/`GarminPassword`) and a per-user token store path: `filepath.Join(cfg.GarminTokenStoreRoot, strconv.FormatInt(userID, 10))`. `GarminTokenStore` in `internal/config` is renamed/repurposed to `TokenStoreRoot` (a directory, not a single tokenstore path) to reflect this.
|
||||
- `sync.Service.Progress()` becomes naturally per-user once each user has their own `Service` instance — no separate change needed there.
|
||||
- `store.DB` stays a single shared connection against one SQLite file — this design keeps the single-shared-DB-with-`user_id`-columns approach, so no per-user DB file/connection is needed.
|
||||
|
||||
@@ -125,5 +125,5 @@ Every existing handler in `internal/api` that reads/writes `profile`, `activitie
|
||||
## Rollout notes
|
||||
|
||||
- Before deploying: log into the current single-tenant build once, note your `sub` from `GET /api/session/me`, and set `GENIUSRUN_LEGACY_OWNER_OIDC_SUB` to it for the first startup after upgrading.
|
||||
- `GARMIN_TOKENSTORE` env var / `GarminTokenStore` config field is repurposed as a root directory (`GarminTokenStoreRoot`) rather than a single tokenstore path — existing deployments need to point it at a directory rather than mcp-garmin's old single-file/dir location, and any already-cached Garmin session under the old path is effectively invalidated (that one user will need to log into Garmin again post-migration, once, under their new per-user subdirectory).
|
||||
- `GARMIN_TOKENSTORE` env var / `GarminTokenStore` config field is repurposed as a root directory (`TokenStoreRoot`) rather than a single tokenstore path — existing deployments need to point it at a directory rather than mcp-garmin's old single-file/dir location, and any already-cached Garmin session under the old path is effectively invalidated (that one user will need to log into Garmin again post-migration, once, under their new per-user subdirectory).
|
||||
- No frontend routing changes beyond the new setup screen — everything else (Dashboard, Review Queue, Plan stub, Profile) is unchanged in shape, just now reflecting whichever user is logged in.
|
||||
|
||||
@@ -69,7 +69,7 @@ After the schema edit, regenerate `docs/DATABASE.md` via `go run
|
||||
|
||||
### Backend API
|
||||
|
||||
- **`handleAuthLogin`/`handleAuthMFA`** (`internal/api/auth.go`): right after
|
||||
- **`handleGarminAuthLogin`/`handleGarminAuthMFA`** (`internal/api/auth.go`): right after
|
||||
the existing `s.recordAuthResult(userID, res)` call, if `res.Status ==
|
||||
garmin.AuthSuccess`, call `s.DB.MarkGarminConnected(r.Context(), userID)`.
|
||||
A failure here is logged and does *not* fail the HTTP response -- the
|
||||
|
||||
@@ -22,7 +22,7 @@ only production caller of the core logic) calls `backfillCore`/`incrementalSyncC
|
||||
Only `internal/sync/service_test.go` still calls `Backfill`/`IncrementalSync`, which is the only
|
||||
reason they're not already flagged as unused by the compiler.
|
||||
|
||||
Since this plan already restructures `service.go`'s progress model, remove these two dead
|
||||
Since this plan already restructures `sync.go`'s progress model, remove these two dead
|
||||
exported methods (and their now-inaccurate doc comments) as an early task, rewriting the tests
|
||||
that called them to exercise the same behavior through `FullSync` or the `*Core` functions
|
||||
directly (same package, so unexported functions are still directly testable) -- before any of
|
||||
@@ -120,7 +120,7 @@ able to report it; the frontend type was just never updated to match).
|
||||
## Frontend design
|
||||
|
||||
`GarminConnection.tsx` keeps its connect/MFA/Reset-all logic and buttons untouched, but loses
|
||||
its `syncStatus` polling, `syncProgressLabel` helper, and all inline sync-progress/last-sync
|
||||
its `garminSyncStatus` polling, `syncProgressLabel` helper, and all inline sync-progress/last-sync
|
||||
JSX.
|
||||
|
||||
A new `SyncModal.tsx`:
|
||||
|
||||
@@ -31,7 +31,7 @@ commit); it just no longer gates anything on its own.
|
||||
|
||||
### The core problem: Garmin auth needs an account today
|
||||
|
||||
`garminFor` builds a per-user `garmin.Client` keyed by a DB user id and
|
||||
`clientFor` builds a per-user `garmin.Client` keyed by a DB user id and
|
||||
reads Garmin credentials from that user's `profile` row. To authenticate
|
||||
with Garmin *before* an account exists, onboarding needs a **temporary,
|
||||
not-yet-persisted** Garmin session -- keyed by the OIDC subject (already
|
||||
@@ -45,7 +45,7 @@ finish onboarding.
|
||||
### Backend: ephemeral pre-account Garmin sessions
|
||||
|
||||
New type and `Server` field (`server.go`, alongside the existing
|
||||
`userGarmin`/`userSync`/etc. per-user maps):
|
||||
`userClient`/`userSync`/etc. per-user maps):
|
||||
|
||||
```go
|
||||
// setupSession is a temporary, not-yet-persisted Garmin authentication
|
||||
@@ -81,7 +81,7 @@ Helper methods on `Server` (`server.go`):
|
||||
spawn new" semantics as `garmin.Client.UpdateCredentials`), builds a
|
||||
fresh ephemeral client whose `TokenStorePath` is namespaced under
|
||||
`{root}/setup/{sub}` (distinct from the permanent `{root}/{userID}`
|
||||
namespacing `garminFor` uses, so two people onboarding concurrently never
|
||||
namespacing `clientFor` uses, so two people onboarding concurrently never
|
||||
collide, and so the ephemeral session's persisted Garmin tokens have
|
||||
*some* home even though no user id exists yet).
|
||||
- `recordSetupAuthResult(sub string, res garmin.AuthResult)` -- updates a
|
||||
@@ -140,7 +140,7 @@ r.Route("/setup", func(r chi.Router) {
|
||||
resumes the already-established Garmin session instead of requiring a
|
||||
fresh login. Logged, not fatal, on error.
|
||||
|
||||
`internal/api/auth.go`'s existing `handleAuthLogin`/`handleAuthMFA`/`handleAuthStatus`
|
||||
`internal/api/auth.go`'s existing `handleGarminAuthLogin`/`handleGarminAuthMFA`/`handleGarminAuthStatus`
|
||||
(used by the Profile page's `GarminConnection.tsx` for reconnecting an
|
||||
already-provisioned user) are untouched.
|
||||
|
||||
@@ -204,7 +204,7 @@ simpler than threading a separate `"mfa"` step through, and matches how
|
||||
`/api/setup/complete` after a successful session creates the user with
|
||||
`GarminEmail`/`GarminPassword`/`GarminConnectedAt` all set, and promotes
|
||||
the *same* `*mock.Client` instance into `s.userGarmin[userID]` (asserted
|
||||
via `garminFor` returning that exact pointer, and `ClosedCalled` still
|
||||
via `clientFor` returning that exact pointer, and `ClosedCalled` still
|
||||
false -- proving no redundant re-authentication happened);
|
||||
`/api/setup/complete` with no prior login attempt → 409;
|
||||
already-provisioned subject hitting either endpoint → 409; two different
|
||||
|
||||
@@ -74,7 +74,7 @@ the `requireProvisionedUser` group (`server.go`). `userID` comes from
|
||||
`client.UpdateCredentials` outside `s.mu`).
|
||||
- Best-effort `os.RemoveAll` on `filepath.Join(s.GarminBase.TokenStorePath,
|
||||
strconv.FormatInt(userID, 10))` when `TokenStorePath` is configured --
|
||||
the same path `garminFor` computes when building a client. Log on
|
||||
the same path `clientFor` computes when building a client. Log on
|
||||
error; this is cleanup of an already-orphaned directory, not something
|
||||
that should fail the request that already deleted the DB row.
|
||||
4. Respond `204 No Content`.
|
||||
|
||||
@@ -75,7 +75,7 @@ which falls back to this default.
|
||||
|
||||
### HTTP access log (`internal/api`)
|
||||
|
||||
A new `requestLoggingMiddleware`, registered as the **first** `r.Use(...)`
|
||||
A new `loggingMiddleware`, registered as the **first** `r.Use(...)`
|
||||
in `Router()` (ahead of `corsMiddleware`), so it wraps every request
|
||||
including unauthenticated ones (login redirect, health check) and OPTIONS
|
||||
preflights:
|
||||
|
||||
@@ -128,7 +128,7 @@ Every current inline-error site, and what changes:
|
||||
| `pages/Analysis.tsx` | Local `error` state, inline paragraph. | Same. |
|
||||
| `components/TrainingTypesCard.tsx` | Local `error` state, inline paragraph. | Same. |
|
||||
| `LoginGate.tsx` | Reads `?auth_error=` from the URL synchronously during render, shows `<p className="login-gate-error">` with a mapped message. | A mount effect reads `?auth_error=` once and calls `showError(mappedMessage)` (same `AUTH_ERROR_MESSAGES` mapping as today); the inline paragraph and its CSS class are removed. |
|
||||
| `OnboardingWizard.tsx` | Single `error` state used for both field validation ("Please enter a display name.", "Please enter your Garmin email and password.") **and** real failures (Garmin login rejected, MFA rejected, `complete()` failing after a successful Garmin auth) — all rendered via the same `onboarding-wizard-error` paragraph, in 4 places. | **Split.** Field-validation messages stay exactly as they are today (local state, inline paragraph, tied to a specific input the user needs to fix — a transient top-of-page banner is the wrong medium for "you left a required field blank"). Real failures (`submitGarmin`'s catch, `submitMFA`'s catch, `complete()`'s catch) call `showError(...)` instead of `setError(...)`. The `complete()`-failure Retry button, which today is conditionally rendered on `error &&`, switches to a new local boolean `completeFailed` (set `true` in that catch block) — the button's visibility no longer depends on the error text still being present, since that text now lives only in the (transient, auto-dismissing) banner. |
|
||||
| `OnboardingWizard.tsx` | Single `error` state used for both field validation ("Please enter a display name.", "Please enter your Garmin email and password.") **and** real failures (Garmin login rejected, MFA rejected, `complete()` failing after a successful Garmin auth) — all rendered via the same `onboarding-wizard-error` paragraph, in 4 places. | **Split.** Field-validation messages stay exactly as they are today (local state, inline paragraph, tied to a specific input the user needs to fix — a transient top-of-page banner is the wrong medium for "you left a required field blank"). Real failures (`submitGarmin`'s catch, `garminMFA`'s catch, `complete()`'s catch) call `showError(...)` instead of `setError(...)`. The `complete()`-failure Retry button, which today is conditionally rendered on `error &&`, switches to a new local boolean `completeFailed` (set `true` in that catch block) — the button's visibility no longer depends on the error text still being present, since that text now lives only in the (transient, auto-dismissing) banner. |
|
||||
|
||||
**Dead CSS removal:** once no `.tsx` file references them, delete the
|
||||
`.error`, `.onboarding-wizard-error`, and `.login-gate-error` rules from
|
||||
|
||||
12
docs/workouts.yaml
Normal file
12
docs/workouts.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
version: v1
|
||||
name: geniusrun
|
||||
templates:
|
||||
- name: 'Recovery 30m'
|
||||
difficulty: 1
|
||||
kind: recovery
|
||||
blocks:
|
||||
- index: 1
|
||||
reps: 1
|
||||
block:
|
||||
- kind: run
|
||||
duration: 30m
|
||||
Reference in New Issue
Block a user