Commit Graph

132 Commits

Author SHA1 Message Date
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
f122f28206 docs: fix stale mcp-garmin references in CLAUDE.md's overview
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.
2026-07-25 21:23:24 +02:00
1b9088bd03 chore: wire up the new garmin.Config shape, refresh start.sh and CLAUDE.md
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>
2026-07-25 21:18:50 +02:00
284ec3142d feat(config): drop MCP_GARMIN_SERVER, default GARMIN_WRAPPER_PYTHON
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>
2026-07-25 21:13:26 +02:00
8c94c94c3f feat(garmin): implement data-fetch methods via generic call dispatch
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>
2026-07-25 21:02:49 +02:00
9cbcf62aed feat(garmin): implement Authenticate/CompleteMFA on subprocessClient
Structured {status, message} responses replace the old string
pattern-matching (parseAuthResult) -- both sides of the protocol are now
owned by this repo, so there's no need to guess at phrasing anymore.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 20:54:54 +02:00
df197f45fd fix(garmin): wait for stderr-copy goroutine before cmd.Wait()
ensureStarted's stderr-copy goroutine could still be mid-Read when close()
called cmd.Wait(), which os/exec's StderrPipe docs call out as incorrect
and can truncate/garble trailing stderr diagnostics or surface a spurious
"file already closed" error. close() now waits on a stderrDone channel,
closed by the copy goroutine once it hits EOF (unblocked by killing the
process), before calling Wait.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 20:50:39 +02:00
6c161f2206 feat(garmin): replace mcp-go transport with a JSON-lines subprocess client
subprocessClient spawns the embedded pyscript/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.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 20:41:44 +02:00
7985000440 feat(garmin): add direct garminconnect wrapper script
Replaces mcp-garmin's server.py: a JSON-lines subprocess protocol
(authenticate/complete_mfa/call) around garminconnect directly, no MCP.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 20:35:13 +02:00
de48c44858 docs: add implementation plan for the direct Garmin wrapper
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>
2026-07-25 20:30:05 +02:00
0936d98161 docs: embed the garmin wrapper script instead of configuring its path
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>
2026-07-25 20:14:36 +02:00
961a7d8aca docs: add design spec for dropping MCP in favor of a direct Garmin wrapper
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>
2026-07-25 20:04:18 +02:00
0ff6793854 store: enable WAL journal mode
Lets an external reader (sqlite3 CLI, DB Browser, DataGrip) inspect the
database file concurrently without "database is locked" errors while
geniusrund is running. Doesn't change in-process concurrency -- queries
are already fully serialized via SetMaxOpenConns(1).

auth: fix flaky tampered-cookie tests

Both tests corrupted a signed cookie by blindly overwriting its last
character with "x", which is occasionally a no-op if that character (part
of the token's signature, so effectively randomized by the embedded
timestamp) already happened to be "x" -- silently passing without having
tampered with anything. Confirmed via 15 repeated runs (3 spurious passes)
before the fix and 30 clean runs after. flipLastChar now guarantees the
byte actually changes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 19:33:15 +02:00
644d87f1dc store: collapse migrations into a single schema.sql, drop legacy-owner path
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>
2026-07-25 19:24:19 +02:00
0f2101bce5 docs: add IDEAS.md for informal backlog, link from CLAUDE.md
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>
2026-07-25 19:07:48 +02:00
15c2b6a2b6 docs: update CLAUDE.md for per-user profile isolation
Reflects the just-merged per-user-profile work: every table is now
user_id-scoped, OIDC accounts get their own isolated dataset via
resolveUser/requireProvisionedUser + POST /api/setup, Garmin sessions are
namespaced per user, and the legacy-owner upgrade bootstrap. Supersedes the
old "single shared profile" framing throughout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 19:05:09 +02:00
1329387528 Merge branch 'worktree-per-user-profile' 2026-07-25 19:00:04 +02:00