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.
- Drop "Logout" everywhere in the flow: nothing is persisted to the
database until Garmin actually connects, so closing the tab is
already a clean escape hatch -- no separate control needed.
- Display-name step: replace the "Next" button with a small checkmark
icon button next to the input (Enter still submits).
- Garmin credentials step: drop "Previous" (changing the display name
just means quitting and relaunching, since nothing is persisted yet)
and rename "Next" to "Login".
- MFA step: drop "Previous" for the same reason.
- Once Garmin reports success (MFA or not), finish setup immediately
instead of waiting on a "Continue to geniusrun" click.
_startup_login() used to run unconditionally at process boot, before
main()'s stdin dispatch loop started. Whenever the very first real
command turned out to be an explicit authenticate, this was actively
counterproductive: on success it duplicated a login authenticate was
about to redo anyway, and on failure it was a wasted, unauthenticated
hit against Garmin's servers moments before the real attempt --
exactly the kind of extra load that worsens rate-limiting risk. It
also never handled MFA, so it couldn't stand in for authenticate
regardless.
Make it lazy instead: only _handle_call falls back to it, at most once
per subprocess lifetime, and only if authenticate was never explicitly
attempted first. This is what it was actually for -- silently resuming
a cached tokenstore session for a data call that never goes through
the explicit authenticate command (e.g. "Sync now" reaching an
already-connected user's client right after a backend restart cleared
the in-memory client cache).
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.
handleSetupComplete reused the onboarding client object as-is in the
permanent per-user cache, but its garmin.Config.TokenStorePath was fixed
at construction to the ephemeral setup/{hash} directory and never
corrected after that directory was renamed to the permanent {userID}
path. The next respawn of that same client (e.g. any Profile-page save,
which unconditionally calls UpdateCredentials) wrote a fresh token file
back under setup/{hash}, forcing a real re-login/MFA on the next Garmin
connect even though a valid session already existed under {userID}.
Close the ephemeral client instead and let the next garminFor call build
a fresh one against the correct, already-renamed directory.
Garmin's rate-limiting means every unattended sync attempt risks a
ban; syncing should only ever happen when explicitly triggered via
"Sync now" (POST /api/sync/run), never on an unattended timer.
Removes the periodic background loop (main.go's runIncrementalSyncLoop,
api.Server.RunIncrementalSyncForAllUsers), its GENIUSRUN_INCREMENTAL_SYNC_EVERY
config, and store.DB.ListUsers (which existed solely to feed it). The
manual "Sync now" flow (Backfill/IncrementalSync/FillPendingDetails via
FullSync) is untouched.
Carries the raw ID token in the session cookie so logout can hand it back
to Keycloak as id_token_hint, letting it skip its own logout-confirmation
prompt -- otherwise a user could cancel out of it and land back in the app
with a Keycloak SSO session but no geniusrun profile (e.g. right after
deleting their account).
Same class of bug as the OIDC callback fix: handleSessionLogout redirected
Keycloak's end-session flow back to BackendURL+"/", which 404s in a
split-origin deployment (the backend serves no "/" route). Flagged as a
known-deferred question in the callback-redirect design spec; fixing it
now that it's been hit in practice.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Now that GENIUSRUN_FRONTEND_URL exists as a separate config value, keeping
the backend's own origin named "PublicBaseURL" invited exactly the kind of
mixup that caused the OIDC callback 404 in the first place. Renamed
consistently: env var, Config.BackendURL, api.SessionConfig.BackendURL.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
handleSessionCallback's redirects (success and all 4 failure branches)
were relative paths, which resolve against the backend's own origin --
broken in this project's own supported split-origin local dev setup,
since the Go backend serves no "/" route at all. Now uses the new
config.Config.FrontendURL (defaults to PublicBaseURL, so no change for
single-origin production deployments).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Lets the OIDC callback redirect to the frontend's real origin instead of
a relative path resolved against the backend's own origin -- needed for
this project's own supported split-origin local dev setup (frontend on
Vite, backend on geniusrund, bridged by CORS).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
_startup_login's background thread and _handle_authenticate's shared the
module-level _login_result_queue with no correlation. Since _startup_login
can now time out at 10s while its thread keeps running (from the previous
fix in this wave), a slow-cold-starting subprocess's first explicit
"Connect to Garmin" call could accidentally dequeue the startup thread's
stale result instead of its own fresh one, orphaning the loser's result to
corrupt a later authenticate/complete_mfa call.
_handle_authenticate and _handle_complete_mfa still correctly share
_login_result_queue -- they're two halves of one explicit, MFA-capable
login flow. _startup_login is a background tokenstore resume with no MFA
involved, so it now uses its own private, function-local queue.Queue()
instead, making cross-contamination structurally impossible.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
_startup_login() ran garminconnect's login synchronously with no timeout
before main() ever started reading stdin, so a slow/rate-limited Garmin
login could wedge a user's whole subprocess before it became responsive.
Give it the same background-thread + bounded-10s-timeout shape
_handle_authenticate already uses, with tests for both the fast-success
and timeout paths.
Also refreshes .claude/skills/geniusrun-dev/SKILL.md (still describing
the retired mcp-garmin MCP architecture) and three stale doc comments
(garmin.AuthStatus, config.GarminTokenStoreRoot, mock package doc) left
over from the direct-wrapper migration.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Nothing in the backend speaks MCP anymore -- internal/garmin talks to its
embedded Python wrapper over plain JSON-lines instead. The cmd/mcpspike
directory was a temporary spike for validating the mcp-go client, which is
no longer needed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Updates the Project overview section to correctly describe the direct
Python wrapper instead of the retired MCP-based approach, eliminating
the contradiction with the Garmin integration section below.
Removes the last references to garmin.Config.ServerPath and the
MCP_GARMIN_* env vars now that the wrapper script is embedded in the
binary; updates CLAUDE.md's mcp-garmin section to describe the direct
wrapper instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The wrapper script is embedded in the binary now (internal/garmin), so
there's no script path left to configure. The interpreter path becomes
optional, defaulting to python3 on PATH, matching how other optional
plumbing (e.g. GENIUSRUN_OIDC_REQUIRED_ROLE) is already handled.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
GetActivities/GetActivitySplits/GetActivityDetails/GetWorkoutByID now send
{"cmd":"call","params":{"method":...,"args":...}} instead of named MCP
tools. subprocessClient fully implements Client.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>