Google Sheets sync
scripts/fetch_google_sheet.py reads the configured sheet range, validates expected columns, parses each week, and writes data/training-plan.json.
data/strava-activities.json.Google Sheets stores the planned marathon build and Strava actual writeback fields. You can adjust daily estimated mileage, session notes, weekly summaries, and phase details there. Existing supplement and nutrition history stays in the Sheet; the website no longer publishes it.
Strava API provides actual runs: distance, time, pace, elevation, heart rate and cadence when Strava includes them, activity names, dates, and Strava links.
Cloudflare Worker is the live Strava sync worker. It receives Strava webhook events, refreshes Strava tokens privately, fetches changed activities, and stores sanitized actual-run JSON in Workers KV.
GitHub Actions is the private Sheet and deployment worker. It reads the Google service account secret, fetches the training plan, and prepares the static deployment output. GitHub is no longer used as the public website host.
Cloudflare Pages is the public static host. It has no API secrets in frontend JavaScript.
sckl-marathon-dashboard/
|-- index.html # Main static dashboard
|-- backend.html # Technical architecture page
|-- race.html # Race execution page
|-- styles.css # Shared visual system
|-- app.js # Browser rendering, charts, status, and comparisons
|-- race.js # Pace calculator and race page interactions
|-- data/
| |-- training-plan.json # Generated Google Sheets plan
| |-- strava-activities.json # Generated Strava run data
| |-- mock-training-plan.json # Local fallback plan
| `-- mock-strava-activities.json
|-- scripts/
| |-- fetch_google_sheet.py # Reads and normalises Google Sheet rows
| |-- fetch_strava.py # Refreshes Strava token and fetches runs
| |-- sync_strava_actuals_to_sheet.py # Writes run actuals back to the plan
| |-- sync_training_calendar.py # Syncs planned runs into Google Calendar
| `-- exchange_strava_code.py # One-time OAuth helper
|-- workers/strava-sync/ # Cloudflare Worker for live Strava webhooks
|-- .github/workflows/
| |-- deploy-pages.yml # Scheduled/manual sync and Cloudflare Pages deploy
| |-- deploy-strava-worker.yml # Worker deploy and webhook registration
| `-- sync-calendar.yml # Manual training calendar sync
|-- requirements.txt
|-- .env.example
`-- README.md
scripts/fetch_google_sheet.py reads the configured sheet range, validates expected columns, parses each week, and writes data/training-plan.json.
scripts/fetch_strava.py refreshes the access token with your refresh token, fetches recent activities, keeps running activities, and writes public training metrics.
scripts/sync_strava_actuals_to_sheet.py reads the generated Strava JSON and writes only the daily Actual and Actual Distance Ran fields in the Training Plan tab.
deploy-pages.yml copies HTML, CSS, JavaScript, and generated JSON into one output folder, then deploys that folder to Cloudflare Pages.
deploy-strava-worker.yml deploys the Strava webhook Worker, installs Worker secrets, and can optionally register the Strava webhook or run a full Worker sync.
The Sync data button opens the GitHub Actions workflow page. From there, run the workflow manually to fetch the latest sheet and Strava data.
scripts/sync_training_calendar.py reads the Training Plan, skips Wednesday runs by default, applies the preferred AM/PM timing rules, and tags Google Calendar events with hidden plan IDs so later Sheet edits update existing events.
The pace table is a practical coaching estimate, not a lab result. It combines race-history anchors with the historical SCKL block; the final four-week review was updated 5 September 2026, then expresses each intensity as a range for Singapore heat, humidity, hills, and day-to-day fatigue.
The baseline estimate uses a 37:00 tropical 10K, cross-checked against a 1:19 half marathon and historical 2:45 cool-weather marathon fitness. This points roughly to a VDOT-style fitness level around 57.
The actual peak was 101.0 km in the week of 17 August, including a 34.1 km long run. The 29 August run contained 6/5/4/3/2 km MP blocks; 5 September added 28.1 km with 3 x 5 km MP. The final four weeks reduce volume to protect recovery.
Sub-3 requires about 4:16/km on average. The working marathon-effort range is 4:12-4:20/km, while race execution starts slightly controlled before settling near 4:14-4:16/km if conditions and effort allow.
The updated plan sets conditional running ceilings of 48, 44 and 32 km, then 18 km before race day. Recurring shin pain requires assessment; stop painful running. Historical speed ranges below are reference only, with no track/threshold work scheduled.
| Zone | Derivation |
|---|---|
| Recovery | Easy aerobic pace plus extra buffer for tired legs, injury caution, and post-workout recovery. |
| Easy aerobic | Set around completed easy running, commonly 5:15-5:50/km, with slower social and post-PT running treated as valid recovery rather than a missed target. |
| Long run easy | Sits slightly steadier than recovery but below steady aerobic, so long runs can absorb hills and later workout sections. |
| Marathon effort | Anchored to the sub-3 requirement of about 4:16/km, with a small range for training surfaces, weather, hills, and accumulated fatigue. |
| Tempo / threshold | Cross-checks the 37:00 10K and 1:19 half against completed 800m-1600m work, then separates longer aerobic-strength running from shorter cruise intervals. |
| 10K / VO2 / speed | Uses the completed 1 km, 800m, 600m, and 400m sessions as evidence of speed reserve. These paces maintain economy; they do not by themselves predict marathon readiness. |
GOOGLE_SERVICE_ACCOUNT_JSON, STRAVA_CLIENT_ID, STRAVA_CLIENT_SECRET, STRAVA_REFRESH_TOKEN, CLOUDFLARE_ACCOUNT_ID, and CLOUDFLARE_API_TOKEN. These stay private in GitHub Actions.
Cloudflare Pages deployment needs Account > Cloudflare Pages > Edit. Worker deployment also needs Account > Workers Scripts > Edit and Account > Workers KV Storage > Edit.
GOOGLE_SHEET_ID points to your training sheet. GOOGLE_SHEET_RANGE is currently A:AQ so the daily actual columns and weekly summary column are included. GOOGLE_CALENDAR_ID, TRAINING_CALENDAR_TIMEZONE, and TRAINING_CALENDAR_COLOR_ID configure the optional calendar sync.
The frontend only knows public file paths such as data/training-plan.json. It never receives Google or Strava secrets.
.env can hold local copies of the same values for testing scripts, while .env.example documents the required names without real credentials.
data/training-plan.json contains the plan from Google Sheets: week number, week start date, phase, weekly mileage, daily sessions, daily actual writeback fields, long run target, weekly summary, and notes.
data/strava-activities.json contains the generated actual training data used by the dashboard: athlete profile, sync timestamp, run distance, moving time, elapsed time, elevation, heart rate, cadence, activity names, dates, and Strava links.
{
"metadata": { "generated_at": "2026-05-11T12:00:00+08:00" },
"weeks": [
{
"week_number": 1,
"week_start_date": "2026-05-11",
"phase": "Base",
"target_weekly_mileage_km": 61,
"week_summary": "Base week with 61 km across 5 planned runs..."
}
]
}
The website is public, so anything written into generated JSON can be viewed by anyone with the site link. API secrets are private, but published activity metrics are public.
The Nutrition page and its nutrition and supplement JSON files are excluded from current deployments. Existing Sheet history and repository utilities are retained, and the website deployment no longer processes nutrition estimates.
If a Strava token or Google service account key is ever shared outside the secret stores, rotate it and update GitHub Actions secrets.
The dashboard is a training visibility tool, not a coaching or medical system. Risk flags and progress views should guide decisions, not replace judgement around injury, sleep, heat, or recovery.
Source code and setup notes live in the GitHub repository.
The live dashboard is hosted on Cloudflare Pages. GitHub Pages is disabled.
The manual sync workflow is available at deploy-pages.yml.
The Worker deployment workflow is available at deploy-strava-worker.yml.
The manual calendar sync workflow is available at sync-calendar.yml.