Fix Garmin connect race, surface tool error content, add dev start scripts

- GarminConnection now flushes Profile's pending debounced autosave before
  connecting, avoiding a race where Connect fires with stale credentials.
- client.go reads res.Content before checking IsError so tool error
  messages actually include mcp-garmin's response text.
- seedsample: update kind lookups to match current taxonomy names
  ("Easy Run" -> "Easy", "Interval" -> "Intervals").
- Add backend/start.sh and frontend/start.sh dev launch scripts, and
  check in CLAUDE.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Christophe Vila
2026-07-21 18:33:49 +02:00
parent 518bccf5ab
commit 2fab788208
8 changed files with 155 additions and 63 deletions

View File

@@ -212,9 +212,6 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -232,9 +229,6 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -252,9 +246,6 @@
"ppc64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -272,9 +263,6 @@
"riscv64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -292,9 +280,6 @@
"riscv64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -312,9 +297,6 @@
"s390x"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -332,9 +314,6 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -352,9 +331,6 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -551,9 +527,6 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -571,9 +544,6 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -591,9 +561,6 @@
"ppc64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -611,9 +578,6 @@
"s390x"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -631,9 +595,6 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -651,9 +612,6 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -1252,9 +1210,6 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -1276,9 +1231,6 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -1300,9 +1252,6 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -1324,9 +1273,6 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MPL-2.0",
"optional": true,
"os": [

View File

@@ -14,7 +14,7 @@ function syncProgressLabel(status: SyncStatus): string {
return "syncing...";
}
export function GarminConnection() {
export function GarminConnection({ onBeforeConnect }: { onBeforeConnect?: () => Promise<void> }) {
const [auth, setAuth] = useState<AuthResponse | null>(null);
const [code, setCode] = useState("");
const [busy, setBusy] = useState(false);
@@ -50,6 +50,7 @@ export function GarminConnection() {
setBusy(true);
setError(null);
try {
await onBeforeConnect?.();
setAuth(await api.login());
setDisconnected(false);
} catch (e) {

View File

@@ -88,6 +88,18 @@ export function Profile({ onSaved }: { onSaved?: (p: ProfileType) => void }) {
saveTimeoutRef.current = setTimeout(() => persist(next), AUTO_SAVE_DELAY_MS);
}
// GarminConnection's "Connect" button reads credentials from the backend's
// in-memory profile, which only updates once the debounced autosave above
// actually fires -- clicking Connect right after typing a password would
// otherwise race it and authenticate with stale (possibly empty) creds.
async function flushPendingSave() {
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current);
saveTimeoutRef.current = null;
if (profileRef.current) await persist(profileRef.current);
}
}
if (!profile) {
return (
<div className="page">
@@ -132,7 +144,7 @@ export function Profile({ onSaved }: { onSaved?: (p: ProfileType) => void }) {
value={profile.BackfillHorizonDays}
onChange={(v) => set("BackfillHorizonDays", v)}
/>
<GarminConnection />
<GarminConnection onBeforeConnect={flushPendingSave} />
</fieldset>
{/* TODO: these settings currently have no explanation in the UI --

12
frontend/start.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
# node/npm are installed via Homebrew (node@22) but not linked onto PATH.
export PATH="/opt/homebrew/opt/node@22/bin:$PATH"
if [ ! -d node_modules ]; then
npm install
fi
exec npm run dev