Dashboard
Skill kaustin923/agent-fitness-coach/.claude/skills/dashboard
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 dashboardAssembled 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
Generate athlete/dashboard.html — a single-file, three-tab (Today / Plan / Stats) training dashboard rendered from plan.json and log.json — and publish it as an artifact or open it locally. Use when the athlete asks to "see" their plan or progress, and regenerate it after every sync or plan adjustment.
SKILL.md
15.1 KB, as published. Nobody here has run it
Dashboard
The dashboard is a projection: one self-contained HTML file rendered entirely from athlete/plan.json and athlete/log.json. It is never hand-edited and never a source of truth — when the data changes, you regenerate the file. This design is lifted from a shipped marathon web app; the specs below are its distilled UI, math, and pitfalls.
When to run
- The athlete asks for their dashboard, "show me my plan", or "how am I tracking" and their surfacing preference is visual.
- Immediately after
strava-sync,healthkit-import,plan-builder, orplan-adjustfinishes (those skills will suggest it). - Any time
athlete/dashboard.htmlis stale relative to its inputs.
Inputs
Files read: athlete/plan.json, athlete/log.json, athlete/profile.md (units, athlete first name). If log.json's fitness.syncedDate is stale and the athlete syncs from Strava, offer to run strava-sync first — the dashboard labels its data's provenance and freshness, so sync before render beats render-then-apologize.
File written: athlete/dashboard.html — always this exact path, so artifact redeploys keep a stable URL.
Ask the user: nothing, unless no plan exists (then point to plan-builder).
Procedure
-
Date ritual.
date +%F; build the weekday↔date table with python. You need today's date for build-time verification even though the HTML computes "today" live at view time. -
Read and validate inputs. Load both JSON files with python (
json.load— this also validates them). Confirm the planmeta.statusisactive. -
Generate the file with a python script. Write the full HTML with the two data files embedded as constants:
const PLAN = <plan.json>;andconst LOG = <log.json>;viajson.dumps, escaping</as<\/so the JSON can never close the script tag.let TODAY_OVERRIDE = null;— a QA seam.todayStr()returnsTODAY_OVERRIDEwhen set (also honoringwindow.__TODAY__), else the local date. All rendering flows through it.- Everything inline: CSS, JS, SVG icons. Zero external requests — no CDNs, fonts, or images. Artifact hosting enforces this with a strict CSP, and it's the right architecture anyway.
-
Implement the three tabs and design system per the specs below.
-
Verify before publishing (checklist below). Never publish a dashboard you haven't rendered at the edge dates.
-
Publish. If an Artifact tool is available, publish
athlete/dashboard.html— reusing the same file path on every regeneration so the URL stays stable (the athlete bookmarks it once). Otherwiseopen "athlete/dashboard.html"locally. Either way, tell the athlete what data vintage it shows ("built from your sync of<syncedDate>").
Layout: the three tabs
A sticky top bar (plan name, days-to-race countdown clamped at 0) over three tabs. Max content width 740px, widening to 1080px on desktop.
Today
One dark-gradient hero card plus a "This week" strip.
- Lead line: "Today's session" when today is a plan day; "Up next" when the next session is a future date; "Your N-week build starts
<date>" before the plan; a celebration state after the race ("You did it." with a recovery line). - Headline: the day's
titleverbatim (authored canonically, e.g. "Tempo · 5 mi @ threshold"); fall back to type conventions (Race → "<meta.goal>·<distance>", Rest → "Rest day", Long → "Long run · N mi", Easy/Recovery/Shakeout → "<Type>· N mi"). - Body: structured runs (warmup non-null) render a two-column segment grid — Warm-up, Cool-down, Workout full-width — plus a highlighted "Total distance" cell. Continuous runs render one "Target pace
<range>/mi" subline fromPLAN.paces[day.pace_zone]. Astrengthsidecar renders as a dumbbell callout; Rest and Cross-Train days get dedicated callouts ("Rest is training too"). - Evidence row: when
LOG.activities[date]exists, a chip — "Strava · X mi @ P/mi" — with a grade pill from the day's recordedactual.grade. A past run day with no match gets a gentle "Not yet on Strava" chip, never a failure mark. - Week strip: seven tappable pills — day abbreviation, short type label in the type color, miles or an em-dash, a green check when done, today outlined. Tapping opens a day-detail view reusing the hero layout.
Plan
- Phase timeline: flex segments sized by weeks per phase, colored by phase; a translucent fade covers the future portion; a TODAY marker (START before the plan begins) at
todayPctclamped to 2–98%, its label left-aligned below 12%, right-aligned above 88%, else centered. Meta line: "Week N of T ·<Phase>phase" and days to race. - Week accordions: one card per week, current week open by default. Header: "Week N", phase tag, focus text with "begins
<date>", a thin green progress bar (fraction of the week's run days done), weekly miles, chevron. Body: one row per day — day abbreviation + MM-DD date, type badge, workout text, warm-up/cool-down sublines, strength summary, a green verified line ("X mi @ P/mi") when synced, planned miles, and a check indicator on days withtotal_miles > 0. - Check states are display-only in the agent model: the files are the state, and you are the write path. Footer copy: "Mark runs done in chat — Strava confirms them automatically."
Stats
Five cards; on desktop (min-width: 900px) a two-column grid with the header card spanning both columns and both charts fixed at 190px height so they align.
- Header card: a 118px completion ring (spec below) plus four stat tiles — runs completed "X / Y", miles logged, miles to go, training streak — all
.tnum. - Current fitness: synced date label ("Synced from Strava ·
<date>"), four cells — last-7-day miles + run count, 28-day average pace, longest recent run, 28-day total miles + runs — a four-bar weekly sparkline (oldest→newest, last bar accent), and a "Latest:<name>· X mi @ P/mi ·<date>" line. All values come straight fromLOG.fitness(computed at sync time perreference/tracking.md,## Current fitness); if absent, show a prompt to sync instead of zeros. - Cumulative miles: dashed plan line vs solid accent actual line with a translucent area fill and a dot on the latest actual point; legend with plan total and miles run; an "N mi ahead of/behind plan" header stat.
- Weekly mileage: one bar per week against a 10-mile-gridline axis; inner fill = completion fraction; current week outlined; taper/race weeks tinted; odd week numbers as x labels.
- Training paces: zone swatch, range "/mi", and a use hint per zone — Recovery "shake out the legs", Easy "most miles live here", Marathon "race rhythm — rehearse it", Tempo "~1hr race effort", Interval "5K effort, controlled", Strides "short, fast, relaxed". Caption anchors provenance: "From your
<target>goal and current fitness." Zone definitions live inreference/pace-zones.md.
Walking plans
A Stats-tab variant, not a fourth tab: walking-first plans keep the same card layouts with minutes as the currency — weekly-minutes bars, cumulative minutes, and a longest-walk tile in place of their mileage counterparts. The training-paces card is omitted or replaced by an intensity-cue card ("brisk — you can talk, but you couldn't sing", per reference/training-rules.md). The completion ring already counts planned Walk days; nothing changes there.
Design system
Carry these tokens verbatim; they are the visual identity of the whole project (also documented in guides/06-surfacing.md):
:root {
--bg:#eef0f4; --surface:#ffffff; --surface-2:#f7f8fa;
--ink:#0d1117; --ink-2:#39424e; --muted:#566071; --faint:#697483;
--line:#e7e9ef; --line-2:#eef0f4;
--accent:#4f46e5; --accent-ink:#3b34c4; --accent-tint:#e8e7fc; --accent-2:#7c6cf6;
--flame:#ea580c; --flame-tint:#ffece0; --green:#0f7a4a; --green-tint:#e6f6ee;
--amber:#855600; --amber-tint:#fbf1d8; --rose:#be123c; --rose-tint:#fce8ec;
--violet:#6d28d9; --violet-tint:#f0eafe; --teal:#0e7490; --teal-tint:#e2f4f8;
--hero-grad:linear-gradient(135deg,#1b1e3d 0%,#2a2467 52%,#4f46e5 130%);
--radius:18px; --radius-sm:13px;
}
Dark theme (the original app was light-only; the dashboard must support both). Under @media (prefers-color-scheme: dark), darken surfaces and lighten inks while keeping the accent hues, e.g. --bg:#0d1117; --surface:#161b22; --surface-2:#1c2128; --ink:#e6edf3; --ink-2:#c9d1d9; --muted:#9aa4b2; --faint:#7d8590; --line:#30363d; --line-2:#21262d; --accent:#7c6cf6; and convert every *-tint to a translucent version of its hue (e.g. --green-tint:rgba(16,122,74,.18)) paired with a lightened ink (--green:#34d399, etc.). Repeat the dark values under :root[data-theme="dark"] and the light values under :root[data-theme="light"] so a hosting page's theme toggle wins in both directions.
Type → color / icon / short label (badges pair the tint background with the saturated ink foreground):
| Type | Color | Icon | Strip label |
|---|---|---|---|
| Easy, Recovery, Shakeout | accent | route | Easy / Easy / Shake |
| Tempo | amber | gauge | Tempo |
| Interval | rose | zap | Interval |
| Long | green | map | Long |
| Marathon-Pace | violet | flag | MP |
| Cross-Train, Lift | teal | dumbbell | Lift |
| Walk | teal | route | Walk |
| Rest | faint | moon | Rest |
| Race | flame | trophy | Race |
Phase colors: Base → accent, Build → teal, Peak → rose, Taper → violet, Race → flame.
Grade pills: on → green tint, faster → accent tint, slower → amber tint. Deliberately no red — slower than target is caution, not failure.
Icons: inline 24×24 SVG, stroke-width 1.9, currentColor, round caps and joins, aria-hidden="true". No emoji, no icon fonts. Typography: 15px/1.55 system stack (-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif); a .tnum { font-variant-numeric: tabular-nums; } class on every figure. Layout hygiene: overflow-x: clip on html/body, touch targets ≥44px, visible :focus-visible outlines, safe-area-inset padding.
Rules
Rendering math — the constants and formulas here mirror reference/tracking.md; where they touch grading, matching, or fitness, that file is the source of truth:
- Provenance copy keys off the athlete's actual data source (
athlete/profile.md), everywhere it appears: the evidence chip ("Strava · X mi" / "Chat · X mi" / "Health · X mi"), the fitness-card label ("Synced from Strava ·<date>" / "Logged from chat ·<date>"), and the Plan-tab footer — "Mark runs done in chat — Strava confirms them automatically" is for Strava athletes only; chat-only athletes get "Tell me what you did and I'll log and grade it." - Done state: a day is done when
LOG.done[date]is true or the plan day'sstatusis"completed". - Credited miles per day (
loggedMi): not done → 0; done with a matched activity → the actual distance; done without one → the plannedtotal_miles. Every mileage stat (bar fills, cumulative actual, miles logged, miles to go) builds on this. - Completion: denominator = "plan days with planned miles > 0, plus standalone Lift days and planned Walk sessions"; Rest, Cross-Train, and active-recovery days are excluded (
reference/tracking.md,## Completion stats).pct = Math.round(done/total*100). - Ring geometry: size 118, stroke 11,
r=(118-11)/2, circumferencec=2πr,stroke-dashoffset = c*(1-Math.min(1,pct/100)), rotated −90°, gradient stroke#6d28d9 → #4f46e5, round linecap. - Miles to go:
Math.max(0, Math.round(planTotal - milesLogged)). - Streak: the training streak (
reference/tracking.md,## Adherence and streaks) — walk actionable plan days newest-first (the## Completion statsdenominator; Rest and Cross-Train days neither count nor break); done increments; an un-done today is skipped without breaking (the day isn't over); an un-done past day — including yesterday — breaks. Cap at 365. - Ahead/behind: date-to-date, never week-to-week — planned miles due by today (excluding the Race day) vs credited miles through today; whole-week comparisons always read "behind" mid-week.
- Cumulative chart: both series start with a week-0 origin point at 0 (otherwise a one-week-old actual line is a single invisible point); the actual series is
nullafter the current week so the line stops at now; y-axis maxMath.max(50, Math.ceil(planTotal/100)*100)with quarter gridlines. - Weekly bars: axis max
Math.ceil(maxWeekly/10)*10; bar heightMath.max(1, planned/axisMax*100)%; fillMath.min(100, doneMiles/planned*100)%; each bar carriesrole="img"and anaria-labellike "Week 4 · 29 mi planned · 21 done". - Pace formatting: always round total seconds first, then split into m:ss — naive minute/second math produces ":60" (
## Pace math). Grades are read fromplan.jsonactuals, not recomputed in JS. - Date handling: parse "YYYY-MM-DD" by splitting components and constructing
new Date(y, m-1, d)—new Date("YYYY-MM-DD")parses as UTC and shifts a day in western timezones. Day differences useMath.roundto absorb DST. - Clamp everything at the extremes: countdown
max(0,…), ringmin(1,…), timeline marker 2–98%, tooltip position flipped near the edges. Pre-plan, race-day, and post-race dates are where unclamped UIs break. - Wide content (tables, charts) scrolls inside its own
overflow-x:autocontainer; the page body never scrolls horizontally.
Verification checklist (run before publishing, every time):
python3 -c "import json; json.load(open('athlete/plan.json')); json.load(open('athlete/log.json'))"— inputs parse.- Render at five pinned dates by setting
TODAY_OVERRIDE: before plan start, mid-build, a taper week, race day, the day after the race. Each must produce its special state, not a generic fallback. grep -cE '[0-9]:60' athlete/dashboard.html— must be zero. The pattern is pace-shaped on purpose: a plain:60false-positives on CSS likefont-weight:600.- Confirm dark mode by toggling
data-theme(or your OS scheme): both themes readable, tints visible. - If browser tooling is available, screenshot at 390px and 1180px widths — most overflow bugs only show at phone width (the screenshot-loop method is described in
guides/06-surfacing.md). Mind the macOS headless-Chrome clamp: windows won't go below ~500 CSS px, so--window-size=390,...silently renders a 500px layout — get a true 390px viewport from a 390px iframe harness or CDP device emulation instead.
Output
Tell the athlete where the dashboard lives (artifact URL or local path), what sync vintage it reflects, and one highlight from it ("you're 4 mi ahead of plan"). Then suggest 2–3 next actions, such as:
- "Bookmark the URL — I redeploy to the same address after every sync."
- "Want this as a real website on your phone's home screen? I can run deploy-webapp."
- "I can run your weekly review Monday and bake it in."