Architecture

Information flow

1You edit the marathon plan in Google Sheets. The sheet stays the source of truth for planned weeks, dates, daily mileage, notes, and weekly summaries.
2You record runs normally through Strava. The dashboard only needs Strava API access; it does not need Strava data typed into the sheet.
3Strava sends activity create, update, and delete webhook events to a Cloudflare Worker. The Worker updates a KV cache so actual runs can appear without waiting for a site rebuild.
4GitHub Actions runs the Sheet sync workflow on schedule, on code changes, or when you press the Sync data button and manually run the workflow.
5The workflow reads private credentials from GitHub secrets, fetches the training plan, and prepares one static output folder with the dashboard, race information, training plan JSON, and fallback Strava JSON.
6Cloudflare Pages hosts the static site. GitHub Pages is disabled. The dashboard loads live actuals from the Worker first, then falls back to data/strava-activities.json.
Responsibilities

What each service does

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.

Reference

Active site and automation files

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
Automation

Workflow responsibilities

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.

Strava sync

scripts/fetch_strava.py refreshes the access token with your refresh token, fetches recent activities, keeps running activities, and writes public training metrics.

Strava actual writeback

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.

Cloudflare Pages deployment

deploy-pages.yml copies HTML, CSS, JavaScript, and generated JSON into one output folder, then deploys that folder to Cloudflare Pages.

Worker deployment

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.

Manual refresh

The Sync data button opens the GitHub Actions workflow page. From there, run the workflow manually to fetch the latest sheet and Strava data.

Calendar sync

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.

Method

Pace range derivation

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.

Performance anchor

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.

Current block evidence

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.

Marathon target

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.

Adjustment rule

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
RecoveryEasy aerobic pace plus extra buffer for tired legs, injury caution, and post-workout recovery.
Easy aerobicSet 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 easySits slightly steadier than recovery but below steady aerobic, so long runs can absorb hills and later workout sections.
Marathon effortAnchored to the sub-3 requirement of about 4:16/km, with a small range for training surfaces, weather, hills, and accumulated fatigue.
Tempo / thresholdCross-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 / speedUses 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.

Secrets And Runtime Config

GitHub repository secrets

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 token scope

Cloudflare Pages deployment needs Account > Cloudflare Pages > Edit. Worker deployment also needs Account > Workers Scripts > Edit and Account > Workers KV Storage > Edit.

GitHub repository variables

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.

Frontend config

The frontend only knows public file paths such as data/training-plan.json. It never receives Google or Strava secrets.

Local development

.env can hold local copies of the same values for testing scripts, while .env.example documents the required names without real credentials.

Data Contract

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..."
    }
  ]
}

Privacy And Limits

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.

Technical Reference

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.