Deploy webapp
Skill kaustin923/agent-fitness-coach/.claude/skills/deploy-webapp
Give this repo to Claude and it becomes your training coach: real periodized plans, Strava + Apple Health data, progress tracking and grading — files are the database, skills are the features, the agent is the app.
npx -y skills add kaustin923/agent-fitness-coach --skill deploy-webappAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Deploy the dashboard as a real website on Vercel with live Strava sync through a serverless proxy that keeps all secrets server-side. Use when the athlete wants their dashboard at a stable URL — on their phone's home screen, shareable, self-updating — without opening Claude.
SKILL.md
10.4 KB, as published. Nobody here has run it
Deploy the dashboard as a website
This is the optional last mile: the same single-file dashboard, hosted at a real URL, with a "Sync Strava" button that works from any browser. It is the exact architecture the original marathon web app shipped with — a static page plus one serverless function that holds the Strava secrets and proxies the API. Total setup is about fifteen minutes and runs comfortably on free tiers.
The one security rule, before anything else: the Strava client_secret and refresh_token exist only as server-side environment variables. They never appear in the HTML, in any front-end JavaScript, in this repo's files, or in a git commit. Everything below is arranged around that rule.
When to run
- The athlete asks for "a real website", a home-screen app, or a dashboard they can check without opening a chat.
- They want family or a coach to follow the build from a link.
- Not needed for the artifact/local dashboard — run this only when a deployed, self-syncing page is genuinely wanted.
Inputs
athlete/dashboard.html— generate it first with thedashboardskill if missing.athlete/plan.json— for the sync window dates baked into the page.- Accounts and tools the athlete needs: a Strava account, a free Vercel account, and Node.js (for
npx vercel; check withnode --version). - What to ask the user: confirm they understand the page will be reachable by anyone who has the URL (see the passcode note in Rules), and have them keep their Strava Client ID/Secret at hand during setup — you will never write those values into a file.
Procedure
-
Scaffold the project. Create
athlete/webapp/with the dashboard as the index page and anapi/directory:mkdir -p "athlete/webapp/api" cp "athlete/dashboard.html" "athlete/webapp/index.html" -
Create a Strava API application. Have the athlete visit
https://www.strava.com/settings/apiand create an app with Authorization Callback Domain:localhost. They copy the Client ID and Client Secret — into a password manager or terminal, not into any file you write. -
One-time OAuth: get a refresh token. (The full cookbook, with troubleshooting, is in
guides/02-getting-your-data.md.)-
The athlete opens this URL in a browser (substituting their Client ID):
https://www.strava.com/oauth/authorize?client_id=CLIENT_ID&response_type=code&redirect_uri=http://localhost/exchange&approval_prompt=force&scope=activity:read_all -
After approving, the browser lands on
http://localhost/exchange?code=THE_CODE&...which won't load — that's expected. CopyTHE_CODEfrom the address bar. Make sureactivity:read_allwas left checked on the consent screen. -
Exchange the code (codes expire quickly — do this promptly). Read the secret with
read -sfirst so it never lands in shell history:read -s STRAVA_CLIENT_SECRET # paste the client secret at the silent prompt curl -X POST https://www.strava.com/oauth/token \ -d client_id=CLIENT_ID \ -d client_secret="$STRAVA_CLIENT_SECRET" \ -d code=THE_CODE \ -d grant_type=authorization_code -
Keep the
refresh_tokenfrom the JSON response. Access tokens expire in ~6 hours; the refresh token is the durable credential the proxy will use forever.
-
-
Write the serverless proxy at
athlete/webapp/api/strava.js:// api/strava.js — Vercel serverless function. // All secrets live in env vars; this file contains none. export default async function handler(req, res) { const { STRAVA_CLIENT_ID, STRAVA_CLIENT_SECRET, STRAVA_REFRESH_TOKEN } = process.env; if (!STRAVA_CLIENT_ID || !STRAVA_CLIENT_SECRET || !STRAVA_REFRESH_TOKEN) { return res.status(500).json({ error: "Missing STRAVA_* env vars" }); } // 1) Refresh the short-lived access token. const tokenRes = await fetch("https://www.strava.com/oauth/token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ client_id: STRAVA_CLIENT_ID, client_secret: STRAVA_CLIENT_SECRET, grant_type: "refresh_token", refresh_token: STRAVA_REFRESH_TOKEN, }), }); const token = await tokenRes.json(); if (!token.access_token) { return res.status(502).json({ error: "Token refresh failed", detail: token }); } // 2) Pull ~75 days of activities — covers the 28-day fitness window plus trend. // `after` is epoch SECONDS, not milliseconds. const after = Math.floor(Date.now() / 1000) - 75 * 86400; const actRes = await fetch( `https://www.strava.com/api/v3/athlete/activities?after=${after}&per_page=100`, { headers: { Authorization: `Bearer ${token.access_token}` } } ); const acts = await actRes.json(); if (!Array.isArray(acts)) { return res.status(502).json({ error: "Activity fetch failed", detail: acts }); } // 3) Reshape to the dashboard's wire format. const activities = acts.map((a) => ({ sport_type: a.sport_type || a.type, start_local: a.start_date_local, id: a.id, name: a.name, summary: { distance: a.distance, moving_time: a.moving_time }, })); // 4) Edge-cache 10 minutes so Strava's rate limit is never hammered, // no matter how often the page is refreshed. res.setHeader("Cache-Control", "s-maxage=600, stale-while-revalidate=86400"); return res.status(200).json({ activities }); } -
Wire live sync into the front end. Edit
athlete/webapp/index.html: add a "Sync Strava" button to the Stats header card that, only whenlocation.protocolis http/https, fetches"/api/strava"with{ cache: "no-store" }(the browser skips its cache; the CDN edge cache still protects Strava) and runs the standard pipeline fromreference/tracking.md: keepsport_type === "Run", key bystart_local.slice(0, 10)(local wall-clock — never timezone-convert it), longest run per date wins, auto-mark plan days withtotal_miles > 0done, format paces by rounding total seconds, recompute the fitness block, and re-render. Persist the synced maps to versionedlocalStoragekeys (e.g.afc_done_v1,afc_strava_v1,afc_recent_v1), with the baked-inLOGdata as defaults so the page shows real numbers before the first live sync. On failure, keep showing the last synced data with its date label — never empty stats. -
Set env vars and deploy. The athlete types these three commands themselves, in their own terminal, from
athlete/webapp/— each prompts interactively on stdin for its value, so the secret never appears in chat, in your context, in any file, or in shell history:npx vercel env add STRAVA_CLIENT_ID production npx vercel env add STRAVA_CLIENT_SECRET production npx vercel env add STRAVA_REFRESH_TOKEN productionYour role is to hand them those three exact commands to type, then confirm the setup with
npx vercel env ls— it lists variable names only, never values. Once all three are present, deploy:npx vercel --prodThe first run walks through Vercel login and project linking; the deploy prints the live URL.
-
Optional passcode gate. A small client-side passcode screen (store an "unlocked" flag in
localStorage) keeps casual eyes off the page. Be honest with the athlete about what it is: on-device privacy, not security. Anyone with the URL and dev tools can bypass it. That's acceptable because the page contains only a training plan and run summaries — no secrets, no server data. If that's not acceptable, don't deploy. -
Verify.
curl -s https://<your-url>/api/strava | head -c 300returns an{"activities":[...]}payload; a second request within 10 minutes returns fast from edge cache; the Sync button in the browser reports "Synced N runs from Strava."
Rules
- Secrets server-side only — the rule above is absolute. If a secret ever lands in a file or commit, revoke it at
https://www.strava.com/settings/apiand start over. afteris epoch seconds. Distance is meters (divide by exactly 1609.34 for miles); pace usesmoving_time, notelapsed_time;start_localhas no timezone suffix — slice the date, never parse it as UTC.- Keep the edge cache header (
s-maxage=600, stale-while-revalidate=86400). Strava's limits are per application (200 reads/15 min, 2,000/day); the cache makes them unreachable. - Known fragility: Strava sometimes rotates refresh tokens on refresh. This single-user setup pins one token in env and virtually always works — but if sync starts returning 401s, redo step 3 and update the env var.
- Strava is migrating its API: base URL
https://www.api-v3.strava.comandAuthorization: Bearerheaders become mandatory by June 2027. The code above uses Bearer already; swap the base URL if activity fetches start failing. - Compliance: this is a personal, single-athlete tool — you fetching your own data for your own display is the intended use. If you ever distribute an app on this pattern, read Strava's API agreement first: it caps athletes per app, restricts data use in AI applications, and requires official branding ("Connect with Strava" button, "View on Strava" links).
- The deployed page's plan is baked at build time. After
plan-adjustor a new plan, redeploy: regenerate via thedashboardskill,cp "athlete/dashboard.html" "athlete/webapp/index.html"(re-apply the sync wiring from step 5 if the generator doesn't include it), thennpx vercel --prod. Day-to-day run data needs no redeploy — the Sync button handles it. athlete/is gitignored by default, so the webapp directory stays out of the public repo. Keep it that way.
Output
Give the athlete the live URL, remind them it self-syncs from Strava (10-minute cache) while plan changes need a redeploy, and show how to add it to a phone home screen (share → Add to Home Screen). Then suggest 2–3 next actions, such as:
- "Add it to your home screen — it behaves like an app."
- "After the next plan adjustment, tell me and I'll redeploy in one step."
- "Want a passcode screen on it? Two minutes, with the honest caveat above."