pnpm dev # start dev server (Turbopack, port 3000)
pnpm build # production build — catches TS and async-params errors
pnpm lint # eslint
```
No test suite exists. Type-check only: `pnpm tsc --noEmit`.
## Architecture
**Knur** is a single-user personal training tracker: running data from Garmin Connect, strength workouts imported from the Strong app, AI analysis via Claude.
-`client.ts` — `getAuthorizedClient()` loads saved OAuth1 tokens; `fetchRunningActivities()` fetches up to 50 activities and maps them to `RunningActivityInput`; `fetchActivityRoutePoints()` fetches GPS polyline from the activity details endpoint
-`wellness.ts` — `fetchRecentWellness(client, days)` fetches sleep/HRV data for N days concurrently via `Promise.allSettled`
`lib/strong/parser.ts` — parses the plain-text "Share workout" output from the Strong iOS app into `StrengthWorkout[]`. Handles multiple workouts in one paste, comma-decimal weights (`12,5 kg`), and optional per-exercise `Notes:` blocks.
`lib/ai/claude.ts` — builds prompts and calls the Anthropic API. Three entry points:
-`generateAnalysis(targetType, targetId)` — per-workout/run analysis with context from previous sessions
-`generateDashboardAnalysis()` — holistic analysis combining last 6 runs + 4 workouts + 7 days of Garmin wellness data; Garmin wellness fetch failure is swallowed so analysis works without it
`lib/strength/stats.ts` — pure helpers for computing exercise volume/top-weight history (`getExerciseHistory`).
| `/ai/actions.ts` | Server actions: `generateAnalysisAction`, `generateDashboardAnalysisAction` |
### Garmin auth flow
Garmin uses OAuth1 with a custom SSO. The first sync triggers a username/password login via `lib/garmin/sso.ts`. If MFA is required, the pending cookie state is stored in MongoDB (`garmin_login_pending`) and the UI prompts for the MFA code. On success, OAuth1 tokens are saved to `garmin_auth` and reused for all subsequent requests via `getAuthorizedClient()`.
### Route map (GPS)
GPS points are fetched lazily on first visit to a run detail page. `RouteMapFetcher` (async Server Component in `app/running/[id]/page.tsx`) checks `activity.routePoints` in DB — if absent and `hasRoute` is true, fetches from Garmin and caches. The map renders via Leaflet (`react-leaflet`) with CartoDB dark tiles (no API key required). Leaflet requires SSR bypass — see `components/route-map-section.tsx` ("use client" wrapper with `next/dynamic ssr: false`).
### Theming
Tailwind v4 with CSS custom properties defined in `app/globals.css`. Key tokens: `bg` (dark navy), `surface` (slightly lighter), `fg` (cream), `accent` (orange `#fb4617`), `muted`, `sand`. Use `var(--color-accent)` etc. when CSS variables are needed outside Tailwind (recharts, Leaflet SVG).
### recharts pitfall
`ResponsiveContainer` with `height="100%"` inside a dynamically-sized wrapper overflows when labels add extra space. Always use a fixed pixel `height` on `ResponsiveContainer` and do NOT set a fixed height on the wrapper div.