Commit Graph

198 Commits

Author SHA1 Message Date
a828055c30 docs: fold dead Backfill/IncrementalSync wrapper cleanup into sync spec
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.
2026-07-26 22:45:59 +02:00
7f5618ce49 docs: add design spec for improved synchronization UX
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.
2026-07-26 22:09:49 +02:00
d3b26d7d41 fix(frontend): fix off-by-one date shown in the backfill horizon picker
daysAgoToISODate built the date at local midnight but formatted it with
toISOString(), which converts to UTC first -- in any timezone ahead of
UTC (e.g. Europe/Paris), local midnight is still the previous day in
UTC, so the picker displayed a date one day earlier than the one that
was actually selected/stored. Format from the Date's local
year/month/day components instead of going through UTC.
2026-07-26 21:52:38 +02:00
0211dffa1e fix(config): stop deriving GarminTokenStoreRoot's default from DBPath
There's no reason the Garmin token-store root and the SQLite database
file need to live near each other -- they're independent env vars
(GARMIN_TOKENSTORE, GENIUSRUN_DB_PATH) and should stay independently
configurable, including in their defaults. Revert the "next to DBPath"
default introduced in 77f9588 back to a plain ".garmin" relative to
the working directory, so pointing GENIUSRUN_DB_PATH elsewhere never
silently drags the token-store default along with it.
2026-07-26 21:45:39 +02:00
12cbfdbbee docs(ideas): refine backlog notes on sync UX and error banners
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.
2026-07-26 21:26:24 +02:00
92284d3b96 polish(frontend): tighten the onboarding wizard's UX
- 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.
2026-07-26 21:26:14 +02:00
45f2921971 fix(garmin): make wrapper.py's startup tokenstore login lazy
_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).
2026-07-26 21:25:53 +02:00
77f9588c2d refactor(config): rename default Garmin token-store dir to .garmin
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.
2026-07-26 21:25:39 +02:00
a12fe3e810 fix(api): stop promoting the ephemeral Garmin client after setup completes
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.
2026-07-26 21:23:24 +02:00
4d2cbe4883 refactor: remove automatic background incremental sync
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.
2026-07-26 18:01:41 +02:00
7346e2eadd docs: add implementation plan for structured JSON logging 2026-07-26 14:36:04 +02:00
c9e089a2b2 feat(garmin): log every wrapper subprocess call as structured JSON 2026-07-26 14:33:20 +02:00
1d4b1cccfd feat(api): add structured JSON access log with request-id correlation 2026-07-26 14:30:39 +02:00
379e7cc990 feat(applog): add JSON logger and context helpers 2026-07-26 14:28:22 +02:00
501d951e5b docs: add design spec for structured JSON logging 2026-07-26 14:22:28 +02:00
2fbe762290 docs: add implementation plan for deferred-commit onboarding wizard 2026-07-26 14:12:44 +02:00
84a7417781 feat(onboarding): replace CreateProfile/ConnectGarmin with a single deferred-commit wizard 2026-07-26 13:32:32 +02:00
7d68af5b3c feat(api): defer account creation until Garmin actually connects 2026-07-26 13:30:51 +02:00
a1eaa42d42 docs: add design spec for deferred-commit onboarding wizard 2026-07-26 13:17:59 +02:00
35eff03509 docs: add implementation plan for improved first-connection flow 2026-07-26 12:29:26 +02:00
a622af1bfa Merge branch 'worktree-improve-first-connection' 2026-07-26 12:28:45 +02:00
7f4f6d3493 docs: remove improve-first-connection-page from IDEAS backlog (implemented) 2026-07-26 12:26:33 +02:00
049ed16b69 feat(onboarding): add mandatory ConnectGarmin gate to first login 2026-07-26 12:25:09 +02:00
002858c1cb feat(api): persist and expose garmin_connected on successful auth 2026-07-26 12:23:39 +02:00
6a3f0201af feat(store): persist garmin_connected_at, set once on first successful auth 2026-07-26 12:21:42 +02:00
50d65b40c2 docs: add design spec for improved first-connection flow 2026-07-26 12:09:35 +02:00
d1d2d612a5 added ideas 2026-07-26 12:00:10 +02:00
4c939be6d6 added go work files 2026-07-26 12:00:00 +02:00
f7bf7e9c79 removed unwanted ide files 2026-07-26 11:57:33 +02:00
8353cd148b feat(auth): pass id_token_hint on Keycloak logout
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).
2026-07-26 11:55:13 +02:00
d7202eb9bb docs: add implementation plan for profile deletion 2026-07-26 10:23:32 +02:00
ce5057a309 Merge branch 'worktree-profile-deletion' 2026-07-26 10:22:52 +02:00
efbe6a6760 docs: remove profile deletion from IDEAS backlog (implemented) 2026-07-26 10:21:31 +02:00
e2533bd1a8 feat(profile): add Danger zone account deletion UI 2026-07-26 10:20:56 +02:00
ab8cdea214 feat(api): add DELETE /api/profile with Garmin client/tokenstore teardown 2026-07-26 10:19:00 +02:00
e8c340a00e feat(store): add DeleteUser with cascading account deletion 2026-07-26 10:17:21 +02:00
c5faaf5a17 docs: add design spec for profile deletion 2026-07-26 10:06:04 +02:00
65a2dc90d0 refactored component names 2026-07-26 10:00:24 +02:00
3b2b5d0735 refined ideas + start.sh rely on .env 2026-07-26 09:59:56 +02:00
d308201803 fix(api): logout's post_logout_redirect_uri uses FrontendURL, not BackendURL
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>
2026-07-25 23:37:22 +02:00
099e746119 refactor(config): rename GENIUSRUN_PUBLIC_BASE_URL to GENIUSRUN_BACKEND_URL
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>
2026-07-25 23:00:35 +02:00
db2e8e487d fix(api): redirect the OIDC callback to FrontendURL, not a relative path
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>
2026-07-25 22:52:15 +02:00
f93aa331d9 feat(config): add GENIUSRUN_FRONTEND_URL, defaulting to PublicBaseURL
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>
2026-07-25 22:50:46 +02:00
d73c841ab7 docs: add implementation plan for the OIDC callback redirect fix
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>
2026-07-25 22:49:02 +02:00
fe4978dbea docs: add design spec for fixing OIDC callback redirect origin
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>
2026-07-25 22:42:50 +02:00
85577c9d26 added ideas 2026-07-25 21:59:56 +02:00
487df1f9b9 fix(garmin): give startup login its own result queue, not the shared one
_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>
2026-07-25 21:52:48 +02:00
e422f86925 fix: bound startup-login timeout, refresh stale SKILL.md and doc comments
_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>
2026-07-25 21:43:52 +02:00
136567d964 docs: remove stale cmd/mcpspike references 2026-07-25 21:31:24 +02:00
2fad83fe9a chore: drop the mcp-go dependency and remove mcpspike throwaway spike
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>
2026-07-25 21:27:19 +02:00