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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user