half-training-app

active personal private

Personal half marathon training tracker + AI coach for AthHalf 2026

README

# AthHalf Training App

Personal half marathon training tracker for **AthHalf — Athens GA, October 11, 2026**.
24-week plan, AI coach powered by Claude, installs to iPhone home screen as a PWA.

> **Single user.** This app is just for me. The plan dates and goal times are
> baked in. Fork at your own risk; nothing here generalizes.

## Stack

- Vite + React 19 + TypeScript
- React Router v7
- Tailwind CSS v3
- Zustand (state)
- Supabase (Postgres + magic-link auth + Edge Function)
- Anthropic API (Sonnet 4.6 with prompt caching), proxied via Edge Function
- Render Static Site (hosting)
- vite-plugin-pwa (Add to Home Screen on iOS Safari)

## Local development

```bash
# 1. Install
npm install

# 2. Copy env template, fill in Supabase URL + anon key from the
#    `ken-personal` Supabase project (Settings → API)
cp .env.example .env

# 3. Run
npm run dev
```

Then run `supabase/migrations/20260429_init.sql` in the Supabase SQL editor
once. It creates tables, enables RLS, and installs a trigger that auto-creates
a `settings` row on signup.

## Edge Function

The coach is a Supabase Edge Function (`supabase/functions/coach`). Deploy
with the Supabase CLI:

```bash
supabase functions deploy coach
supabase secrets set ANTHROPIC_API_KEY=sk-ant-...
# optional: pin a different model
supabase secrets set COACH_MODEL=claude-sonnet-4-6
```

## Tests

```bash
npm test          # vitest watch
npm run test:run  # one shot
```

## Build + deploy

```bash
npm run build     # → dist/
```

Render Static Site is configured for `npm install && npm run build` with
publish dir `dist`. Set `VITE_SUPABASE_URL` and `VITE_SUPABASE_ANON_KEY` in
the Render dashboard so they're baked into the bundle at build time.

## Project files

- `SPEC.md` — original build spec (read-only reference)
- `CLAUDE.md` — project rules + tech stack (this is what Claude Code reads)
- `MEMORY.md` — accumulated context, decisions, gotchas
- `STATUS.md` — what's shipped, what's next, blockers (update before every push

…(truncated for upload size)

STATUS

# AthHalf Training App — Status

*Last updated: 2026-05-22 (weather city override shipped)*

## V2e — Manual weather city ✅ shipped (2026-05-22)
Closes the "Settings-page location override" deferred item below. Ken was
travelling and the Today card still showed Athens.

- `src/lib/weather.ts`: `fetchWeather()` now takes an optional `GeoCity`
  (defaults to the active city). New `searchCities()` uses Open-Meteo's free
  geocoding API (same provider, no key). `getActiveCity()` / `saveCity()` /
  `clearSavedCity()` persist the chosen city in **localStorage** (`athhalf.weatherCity`).
- Storage is device-local on purpose — "which city am I in" belongs to the
  phone in hand, not the account. No Supabase schema change.
- Forecast `timezone` switched from hardcoded `America/New_York` to `auto`, so
  sunrise/sunset + best-window read in the destination's local time. Athens
  still resolves to ET — no change at home.
- `src/components/WeatherCitySetting.tsx`: new Settings card — search a city,
  tap a result, "Back to Athens (home)" to reset. WeatherCard header shows the
  active city ("Conditions · Portland").
- Coach Sherpa picks up the travel city for free (`buildCoachContext` calls
  `fetchWeather()` with no args → active city).
- Build clean, 101/101 tests pass (+3 for `formatCityLabel`).

## V2d — Weather refinements (3 commits) ✅ shipped (2026-05-06)
**A. Layout + 1-10 score + Georgia recalibration**
- Today's run + Weather are side-by-side on desktop (3:2 grid), stacked on mobile with workout first.
- Compact WeatherCard (~30% shorter than before).
- Run-quality is now a 1-10 score with matching label + dominant-factor reason.
- Brackets recalibrated for Southeast US: 7/10 = typical nice Georgia morning (Ken's "today is nice maybe a 7"), 2/10 = 90°F+80%RH brutal, 1/10 dangerous extreme.

**B. Best running window today**
- Hourly forecast scan from now → sunset, picks the highest-scoring 2-hour daylight stretch.
- Displays as "6:00 AM – 8:00 AM · 9/10 · ~62°, dew 55°"

…(truncated for upload size)

DECISIONS

# AthHalf Training App — Architectural Decisions

A log of non-trivial choices. Each decision lists what we picked, what we
ruled out, and why. Don't re-litigate without new information.

---

## 2026-04-29 — Tech stack baseline

**Decision:** Vite + React 19 + TypeScript, React Router v7, Tailwind v3, Zustand v5, Supabase (Postgres + magic-link auth + Edge Function), `vite-plugin-pwa`, Render Static Site for hosting.

**What we ruled out:**
- React 18 (spec's original) — no migration cost on a fresh project.
- React Router v6 — same reasoning, v7 is current.
- Tailwind v4 — keeping v3 for familiarity (Ken used v3 in the artifact prototype). v4's CSS-first config is a learning cost we don't need today.
- Next.js / SvelteKit / Remix — overkill for a single-user PWA with no server.

**Why:** Smallest stack that gets the three things working (log a run, see today's workout, ask the coach). No backend service to run; Supabase Edge Functions handle the AI proxy.

---

## 2026-04-29 — AI model: Sonnet 4.6 with prompt caching

**Decision:** Use `claude-sonnet-4-6` for the coach, with prompt caching on the system prompt. Model is configurable via `COACH_MODEL` env var.

**What we ruled out:**
- Opus 4.7 — overkill for short coaching responses, ~5x the cost.
- Haiku 4.5 — workable, but tone/specificity matters here. Coaching reads flatter.
- Gemini — used in tts-tax-app for IRS-grounded RAG (different job). Coaching tone is Claude's strength.

**Why:** Sonnet is the right balance for tone-driven, instruction-following work. Prompt caching cuts ~90% off input cost since the ~3KB system prompt is static across every call. Estimated cost: ~$1–3/month at expected usage. Env var means we can A/B with Haiku later without redeploy.

---

## 2026-04-29 — Personal Supabase (`ken-personal`) is separate from Sherpa

**Decision:** Create a new Supabase project `ken-personal` for the half marathon app and all future personal apps. Do not share with the Sherpa firm Supabase.

**What we ru

…(truncated for upload size)

MEMORY

# AthHalf Training App — Memory

Standing facts, decisions, and gotchas. The kind of thing you'd want to
remember in 3 months when you forget why something is the way it is.

---

## Project Constants (V2c marathon pivot, 2026-05-04)
- **Race**: **Richmond Marathon** — Saturday, November 14, 2026 (was AthHalf, dropped)
- **Plan start**: Wednesday, April 29, 2026
- **Plan length**: **29 weeks**
- **Working target**: sub-4:00 (9:09/mi)
- **Stretch target**: BQ for Men 60-64, ~3:42-3:45 (8:30/mi). Boston standard is 3:50:00 but practical entry needs 5-7 min buffer.
- **Floor**: 4:15 (9:43/mi)
- **Run days**:
  - Mon (recovery, W14-29 except W28+W29) — 25-30 min @ 11:00-11:30/mi
  - Tue (easy/MLR, W9-29 except W29) — grows from 2.5 mi to 10 mi peak
  - Wed (easy/strides, every week)
  - Fri (LONG, every week, peaks at 20 mi W26)
  - Sun (medium-quality, every week except W29)
- **Peak weekly volume**: 46.5 mpw at W26 (under 47 cap)
- **Long-run rule**: no forward week-over-week jump > 2 mi (cutback exits exempt)

## Stack Decisions (Apr 29, 2026)
- **React 19 + Router v7** — chosen over spec's React 18 / RR v6. New project, no migration cost.
- **Tailwind v3** (not v4) — deliberate. Matches artifact prototype; v4 has different config model.
- **Sonnet 4.6 with prompt caching** — coach's system prompt is ~3KB and static, so caching cuts input cost ~90% per call. Env var `COACH_MODEL` so we can A/B with Haiku 4.5 if cost matters.
- **Auto-create settings row via DB trigger** — instead of manually inserting after first signup. Set-and-forget.
- **Personal Supabase = `ken-personal`** — separate from the Sherpa firm Supabase. Reasons: blast-radius separation, different security postures, estate-planning ("one URL to find Ken's stuff"), and it's the seed for future personal apps (health, finance, legal).

## Auth + RLS Pattern
- Magic-link only (`signInWithOtp`).
- Every table has RLS enabled with `USING (user_id = auth.uid())`.
- **Never write `USING (true)`** — Postgres OR'

…(truncated for upload size)

CLAUDE.md

# AthHalf Training App — Project Instructions
*Last updated: 2026-04-29*

## Owner
Ken Lill — turning 60 in August 2026. CPA running The Tax Shelter in Athens, GA.
Learning to code, not a career engineer. Keep code readable; over-explain where it helps.

## This Repo
`half-training-app` — Personal half marathon training tracker for AthHalf
(Athens, GA, October 11, 2026). 24-week plan, AI coach powered by Anthropic.

Local path: `D:\Personal\half-training-app`

## Read This First Every Session
Read `SPEC.md` (the full build spec), `STATUS.md` (current state), and
`DECISIONS.md` (architectural choices) at the start of each session.

## Tech Stack (locked in — do not change without discussion)
| Layer | Choice |
|-------|--------|
| Frontend | Vite + React **19** + TypeScript |
| Routing | React Router **v7** |
| State | Zustand v5 |
| Styling | **Tailwind CSS v3** (deliberate — not v4) |
| DB + Auth | Supabase (Postgres + magic-link auth, RLS on every table) |
| AI proxy | Supabase Edge Function (Deno) → Anthropic API |
| AI model | `claude-sonnet-4-6` (env-var swappable via `COACH_MODEL`) with prompt caching |
| Hosting | Render Static Site (free tier, never sleeps) |
| PWA | vite-plugin-pwa (installs to iPhone home screen via Safari) |

## Supabase Project
Personal Supabase project named **`ken-personal`** — separate from the Sherpa
firm Supabase. This personal project is the planned home for future personal
apps too (health, finance, legal). Estate-planning consideration: one
credential set for "Ken's personal data lives here."

**Never put Sherpa firm data in this Supabase. Never put personal data in the Sherpa Supabase.**

## Project Rules (enforced)
- **No secrets in repo** — all credentials via `.env` (gitignored).
- **RLS on every table** — `USING (user_id = auth.uid())`. Never write `USING (true)`.
- **No PII or real client data** — this is personal, but treat `.env` and DB
  contents as private.
- **Time zones** — all dates in DB are DATE type. Frontend uses

…(truncated for upload size)

Diary mentions

No recent diary mentions for this app.