All registry keys must exist as rows in the config table: main seeds
missing keys with their defaults at startup, LoadApp fails fast on a
missing key, and the code-side fallback const/helper for the onboarding
setup timeout is gone -- the value rides in SessionConfig.SetupTimeout.
The key is renamed session.idle_timeout -> session.setup_timeout, and
the /config page's 'overridden' now means 'differs from the default'.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
session.idle_timeout (minutes, default 15) joins the app-config registry
and drives the onboarding Garmin session eviction, distinct from
session.duration (the login cookie lifetime in hours). The raw Keycloak
ID token no longer rides in auth.Claims through every request context:
it's minted into the session cookie separately and read back only by the
logout handler via IDTokenFromSessionCookie. OnboardingWizard uses the
type-imported FormEvent<HTMLFormElement> instead of the React.FormEvent
namespace alias.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
users.display_name becomes users.name and is now the only human-facing
name -- profiles.name is dropped (it duplicated the account name; the
Profile page's Name field now edits users.name through PUT /api/profile,
which carries Name alongside the profiles columns). The profile table
becomes profiles and laps becomes activity_laps, homogeneous with
activity_samples. Onboarding pre-fills the display name from the OIDC
claim's name.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
A confirmed-404 workout must stop being retried forever, but
workout_raw_json should never be fabricated -- it stays null exactly
as it does for "not yet fetched." workout_not_found_at is the
separate marker ActivitiesMissingWorkout/CountActivitiesMissingWorkout
now check for, mirroring the existing pattern of the other _fetched_at
columns. UpsertActivity's ON CONFLICT clause already never touches
these columns, so the marker persists across every later re-sync.
4 tasks: mark a wrapper-level 404 as not_found, expose it as
garmin.ErrNotFound in the Go client, add a workout_not_found_at
column + store query updates, then have fillPendingWorkouts stop
retrying a confirmed-404 workout instead of retrying forever.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A workout deleted on Garmin's side after being linked to an activity
404s forever on get_workout_by_id, and today's fillPendingWorkouts
retries it every sync indefinitely with no user-visible feedback --
just a perpetual "1 more workout pending" nudge. Distinguishes a
definitive 404 (via garminconnect's own GarminConnectNotFoundError)
from a transient failure, and stops retrying the former.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
11 tasks: the shared banner store + renderer, a friendlier
network-unreachable message in client.ts, then migrating every
existing inline error site (GarminConnection, SyncModal, Profile,
Activities, Analysis, TrainingTypesCard, LoginGate, OnboardingWizard)
to it, finishing with dead-CSS cleanup.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces every page/component's ad-hoc inline error paragraph with a
shared, module-level banner store (red/orange/blue/green, stacking,
auto-dismiss + manual close) -- from docs/IDEAS.md's backlog item.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Six tasks: remove dead Backfill/IncrementalSync wrappers, add
ActivitiesMissingWorkout/CountActivitiesMissingWorkout, split
FillPendingDetails into phase-aware activities/workouts passes,
reshape GET /api/sync/status, add SyncModal, wire it into
GarminConnection.tsx.
Discovered while verifying the sync design with the user: these two
exported methods are dead in production (only FullSync's *Core calls
are actually used), kept alive only by tests, and their doc comments
still reference the background sync loop removed in 4d2cbe4.
Phase-aware sync progress (discovering/activities/workouts), a new
/api/sync/status shape, and a blocking SyncModal replacing the Profile
page's inline sync text -- implementing the "improve synchronization"
idea from docs/IDEAS.md.
Merge the old "improve activities/workouts download" and "better UX
when backend is not available" bullets into one clearer "modern error
displays" idea (ephemeral, severity-colored banners reused across
login/logout/sync/backend-down states), and flesh out the sync-modal
idea with the activities/workouts split and progress-bar prerequisite.
Shorter, more conventional dotdir name than garmin-tokenstores. Restore
the "next to DBPath" join that got dropped when the default was
inlined via getEnvDefault -- an explicit GARMIN_TOKENSTORE still wins
verbatim, but the implicit default must still resolve relative to the
DB file's directory, not the process's CWD.
internal/garmin/client.go's GARMIN_TOKENSTORE env var is now always
set (TokenStorePath can no longer be empty), so the conditional that
only appended it when non-empty is dead code.
Two small tasks: add config.Config.FrontendURL (Task 1), then thread it
through api.SessionConfig and handleSessionCallback's five redirects
(Task 2).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
handleSessionCallback's relative redirects resolve against the backend's
own origin, which 404s in this project's own supported split-origin local
dev setup (frontend on Vite, backend on geniusrund, bridged by CORS). Adds
GENIUSRUN_FRONTEND_URL, defaulting to PublicBaseURL for the common
single-origin production case, as the fix.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Task-by-task TDD plan for docs/superpowers/specs/2026-07-25-garmin-direct-wrapper-design.md:
Python wrapper + tests, Go transport scaffolding, auth methods, data-fetch
methods, config changes, wiring/docs, and final dependency cleanup.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Since wrapper.py now lives inside this repo, its location is no longer a
deploy-time concern -- go:embed it into the binary and drop the
GARMIN_WRAPPER_SCRIPT env var entirely. Only the Python interpreter choice
remains configurable, and now optionally so (defaults to "python3").
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces internal/garmin's MCP-based mcp-garmin integration with a custom
newline-delimited-JSON subprocess protocol around python-garminconnect
directly, folded into this repo. MCP's dynamic tool-discovery value is
unused here (fixed call sites, no LLM choosing tools), and both projects
are owned by the same person, so the extra protocol layer and two SDK
dependencies (mcp-go, mcp[cli]) were pure overhead -- especially given
plans to containerize the backend.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Pre-production app, no need to preserve incremental migration history:
replace the 26 migration files with one current-state schema.sql (the
schema.sql comments are now the living documentation), simplify db.go to
apply it once instead of tracking/rebuilding through schema_migrations,
and make user_id NOT NULL everywhere now that there's no staged migration
to accommodate a nullable backfill window.
This removes the reason ClaimLegacyOwner/GENIUSRUN_LEGACY_OWNER_OIDC_SUB
existed (binding a pre-existing singleton-schema database to one account
across a staged migration), so that whole path is gone too -- the
existing dev database was wiped and reseeded fresh under the new schema.
Add cmd/dumpschema, which regenerates docs/DATABASE.md straight from the
live schema (via store.Open + sqlite_master introspection) so the
database documentation can never drift out of sync with reality.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A lightweight, low-friction place to jot future development ideas before
they're formalized into a spec/plan -- distinct from
docs/superpowers/specs/ and docs/superpowers/plans/, which are for once an
idea is ready to be built.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Update the per-user-profile plan document to reflect the corrected Task 3
design: remove the now-unreachable TestClaimLegacyOwner_NoOpOnGenuinelyFreshInstall
test from the code example, update the ClaimLegacyOwner implementation example to
remove the dead-code branch and false doc comment about fresh installs, and fix
the commit message to match the corrected design.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reverses the "single active profile, OIDC is access-only" decision from
2026-07-24 now that login needs to map each user to their own dataset
instead of a shared singleton.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Task-by-task TDD plan covering config, the new internal/auth package
(session cookies, Keycloak OIDC verifier, RequireSession middleware),
wiring into internal/api and cmd/geniusrund, and the frontend
LoginGate.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds an access-gate authentication design: Keycloak OIDC via a
backend-driven Authorization Code flow, restricted by realm role,
with geniusrun minting its own session cookie. No data model or
multi-profile changes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>