Strava Dashboard is a privacy-oriented sports analytics web application for athletes who want more control, transparency, and technical depth than the default Strava experience provides. The application connects directly to a user's Strava account, retrieves activity history, enriches it with derived metrics, and renders a multi-tab analysis workspace for running, cycling, swimming, planning, weather, route mapping, gear lifecycle tracking, annual reporting, and browser-side AI-assisted coaching.
The project is designed for self-coached athletes, endurance amateurs, data-oriented hobbyists, and engineers interested in training analytics. It solves a common gap in consumer fitness products: raw workout history is easy to collect, but difficult to transform into an interpretable model of consistency, training load, performance trends, equipment wear, and sport-specific patterns. This application addresses that gap by combining Strava data retrieval, client-side preprocessing, derived feature engineering, interactive visualizations, and sport-specific heuristics in a single progressive web app.
The application is deployed as a static frontend plus Vercel serverless API layer. Authentication and token refresh are handled through serverless proxies, while the majority of transformation and visualization logic executes in the browser. No application database is used.
At a high level, the application:
- Authenticates a user with Strava using OAuth 2.0.
- Downloads complete activity history through paginated Strava API requests.
- Caches athlete, zone, gear, and activity data locally with different TTL policies.
- Computes global and sport-specific summaries such as distance, duration, elevation, pace, speed, cadence, HR-derived load, and trend metrics.
- Organizes the experience into specialized tabs for dashboarding, per-sport analysis, athlete profiling, predictions, gear lifecycle tracking, activity search, calendar consistency, weather correlation, geographic visualization, annual reporting, and AI-based question answering.
- Provides deeper activity-level analysis through dedicated detail pages that ingest Strava streams and run a point-level preprocessing and analysis pipeline.
This project is for:
- Runners who want better control over pace, consistency, PB tracking, and prediction models.
- Cyclists who want distance, elevation, power, cadence, bike-type segmentation, and route-level analysis.
- Swimmers who need pool versus open-water separation, pace-per-100m metrics, and pool-length estimation.
- Multi-sport athletes who want one consolidated training data surface.
- Engineers and analysts who prefer transparent, inspectable heuristics over opaque product metrics.
Most training platforms expose activity history, but not a production-quality analytical workflow. Athletes often need to piece together spreadsheets, exported files, or third-party dashboards to answer routine questions such as:
- Is my training load trending productively or dangerously?
- What are my real patterns by weekday, hour, or season?
- Which shoes are near replacement?
- How does weather affect my running pace?
- What can my recent race history plausibly predict for the next distance?
- Where do my routes cluster geographically?
- What happened inside a given activity at the stream level?
Strava Dashboard answers those questions in a single system with explicit data flow and reproducible calculations.
The application combines several classes of data:
- Strava athlete metadata via
/api/strava-athlete - Strava activity history via
/api/strava-activities - Strava activity detail metadata via
/api/strava-activity - Strava training zones via
/api/strava-zones - Strava gear metadata via
/api/strava-gear - Strava activity streams via
/api/strava-streams - Local browser persistence via
localStorage - Historical weather enrichment from Open-Meteo archive endpoints for geolocated runs
- Derived outputs exported as GPX, CSV, and JSON from the advanced activity analysis flow
- JSON: all API payloads, cached objects, derived analysis artifacts
- GPX: reconstructed export of analyzed activities
- CSV: tabular export of processed track points
- Encoded polylines: route rendering for the Map tab and detail pages
- Browser storage keys: authentication tokens, filters, gear settings, athlete profile, zones, cached activities, and AI conversation state
The SPA exposes thirteen main tabs and several dedicated detail pages. Each tab is implemented as a renderer in js/tabs/<name>.js.
Training-load and consistency overview. Computes daily TSS, 7-day ATL, 42-day CTL, TSB, and a 7-day rolling load over a user-selected window. Renders KPI cards, an acute-load chart, a consistency heatmap, and goal progress (km, hours, or activity count). Range selector offers nine presets (This Week, Last 7 Days, This Month, Last 30 Days, Last 3 Months, Last 6 Months, This Year, Last 365 Days, All Time) plus a custom DD/MM/YYYY From/To pair with an Apply button. The acute-load band mode is hardcoded to 'aggressive'. Derived per-window slices are memoized in a module-local dashboardMemo Map.
Running-only subset with date and shoe (gear) filters. Summary cards, run-type chart, monthly distance and frequency, pace-vs-distance scatter, distance and elevation histograms, accumulated distance, weekly rolling-mean trend, consistency heatmap, run heatmap map, top runs, Eddington section, monthly shoe-usage Gantt, and a sortable activity table.
Cycling subset segmented by bike type (road, MTB, gravel, indoor, electric) and bike gear. Summary cards, bike-type pie, distance and elevation histograms, speed-vs-distance, distance-vs-elevation, elevation-ratio, power-vs-speed scatters, accumulated distance, weekly trend, consistency heatmap, Eddington distribution and progression, top rides, and a sortable activities table.
Pool vs open-water separation, pace per 100 m, pool-length estimation against common metric and imperial pool lengths. Summary cards, pool/open-water comparison, distance and pace histograms, pace-vs-distance scatter, pace/HR curve, volume improvement, efficiency evolution, accumulated distance, weekly trend, consistency heatmap, pool-length chart, Eddington section, top swims, and a sortable swims table.
Profile and behavior lens. Athlete card, all-time totals, records, training zones (HR and power), duration and start-time histograms, yearly comparison bars, weekly and monthly mix views, and several interactive matrix heatmaps with selectable axes and data type (time, count, distance).
Race-time prediction blending Riegel scaling, VDOT-style transfer, direct PB matching, and personal curve fitting with user-adjustable weights and conservative/moderate/aggressive scenarios. Personal-bests table, prediction table, prediction-evolution chart, training readiness section.
Gear lifecycle tracking. Per-item distance, usage count, first/last-use dates, average distance per use, average pace (for run gear), health percentage versus configurable expected durability, sortable gear list, distance-by-gear chart, monthly usage Gantt, and per-gear detail page.
Universal query layer over the preprocessed activity catalog with multi-select sport, name search, date range, distance range, duration range, HR range, and TSS range filters. Sortable table with sport-aware pace/speed formatting.
Week, month, and year views with type filtering. Day and week streaks, activities per day, total distance, hours, active days, TSS. Click a day for the detailed activity list.
Open-Meteo archive enrichment for runs with start coordinates. Summary cards, monthly multi-variable overview, weather-conditions pie, temperature-vs-pace scatter, configurable histogram (temperature, rain, wind, humidity, clouds, pressure), and a weather-per-activity table.
Leaflet visualization with route-polyline and density-heatmap modes, sport filter, date range, multiple tile providers, heatmap intensity/radius/blur sliders, and color-by-sport toggle. Polylines are decoded client-side from the Strava map.summary_polyline field.
Year-in-sport retrospective with distance, time, elevation, activities, active days, longest activity, average distance, most active month, year-over-year deltas, monthly volume, sport-distribution pie, year comparison, top weeks, and monthly heatmap.
Browser-side conversational assistant. Uses a user-supplied Gemini API key persisted to localStorage, assembles a context block from totals, sport breakdown, PB-like stats, gear summaries, and recent monthly volume, then calls Gemini Flash directly from the browser. Persistent chat history with starter suggestions.
Dedicated per-activity pages fetch metadata and streams from Strava, reconstruct a structured ActivityTrack with TrackPoints, run the preprocessing and detection pipeline in js/analysis/, render stream charts and split tables, and support GPX, CSV, and JSON export.
preprocessActivities in js/shared/preprocessing/core.js enriches each activity object with derived metrics. The current cache schema version is 'v2-efficiency-moving-ratio' and the following fields are added in addition to the existing TSS, CTL, ATL, TSB, weather, and VO2max fields:
efficiency(number | null) — sport-aware aerobic efficiency:- Run:
pace_min_per_km / avgHR(units: min/km/bpm; lower is better). - Swim:
pace_min_per_100m / avgHR(units: min/100m/bpm; lower is better). - Bike / Ride / Cycling:
avg_speed_kmh / avgHR(units: km/h/bpm; higher is better). - Null when
avgHR,distance, ormoving_timeare missing or non-positive.
- Run:
efficiency_method(string | null) — one of'pace_per_hr','pace100m_per_hr','speed_per_hr', ornull.moving_ratio(number | null) —moving_time / elapsed_time, decimal 0–1; null whenelapsed_time <= 0.
js/app/main.js defines CACHE_VERSION = 'v2-efficiency-moving-ratio' and stores it under the localStorage key strava_cache_version. On boot, the app reads the stored version and, when it does not match CACHE_VERSION, drops the previously cached preprocessed activities. The fresh activity payload is reprocessed and the new version key is written back. Bumping CACHE_VERSION whenever the preprocessing schema changes is how new derived fields propagate without manual cache busting.
The application uses a hybrid architecture:
- Frontend: static HTML plus native ES modules served directly in the browser
- Backend: Vercel serverless functions that proxy Strava API requests and handle secure OAuth token exchange and refresh
- Storage: browser
localStoragefor cached resources and UI state - Visualization: Chart.js,
chartjs-chart-matrix, D3, Cal-Heatmap, Leaflet, and Leaflet.heat - Deployment: Vercel rewrites route tab paths to the SPA entrypoint
The main architectural separation is between:
- Tab-level product analytics for the multi-tab dashboard experience
- Stream-level detailed analysis for individual activities
The primary dashboard flow is:
- The user authenticates with Strava.
- The frontend exchanges the authorization code through
/api/strava-auth. - Tokens are stored in
localStorageand encoded into an Authorization header for subsequent backend calls. - The frontend requests athlete metadata, training zones, complete paginated activity history, and gear metadata.
preprocessActivitiesenriches the activity collection with derived fields such as training stress and contextual metrics.- Tab renderers consume the processed activities and apply tab-specific filters and aggregations.
- Charts, tables, heatmaps, and map layers are rendered lazily when tabs are first opened.
The activity-detail flow is more granular:
- A selected activity ID is read from the detail-page URL.
- The frontend requests activity metadata and the required Strava streams.
- Stream data is converted into a structured track model.
- The preprocessing pipeline removes anomalies and smooths noisy series.
- Detection, segmentation, sport-specific analyzers, and advanced engines compute insights.
- The UI renders charts, splits, climb summaries, HR analysis, and export actions.
The project contains a clear ETL-style processing path.
Data ingestion occurs through the Vercel API layer. Activity history is fetched from Strava using page size 100 and repeated until an empty page is returned. Athlete profile, training zones, gear metadata, individual activity metadata, and activity streams are retrieved through dedicated endpoints.
The cleaning stage includes:
- JSON cache validation and TTL expiry checks
- Swim distance correction for a known athlete-specific historical pool misconfiguration
- GPS spike interpolation on point-level tracks
- Hampel-filter-based altitude anomaly replacement
- Speed spike reduction
Transformations include:
- Pace, speed, cadence, elevation-ratio, and time-normalized metric derivation
- HR-zone-aware TSS estimation with fallbacks to simpler methods when data is incomplete
- Calendar and weekly bucketing for trend views
- Gear aggregation and name mapping
- Weather enrichment for geolocated runs
- Polyline decoding for geographic visualization
Feature extraction includes:
- CTL, ATL, TSB, and injury-risk style heuristics
- Run and bike activity classifications
- Swim type identification and pool-length inference
- Terrain, climb, stop, and segment detection on detailed activities
- Fatigue, physiological, and aero-derived indicators on stream-based analysis
- Predictor-model features derived from PBs and race-distance matching
The visualization stage renders transformed data into summary cards, tables, histograms, scatter plots, line charts, pie charts, matrix heatmaps, and map layers.
There is no framework-managed application store. State is handled through module-level variables in js/app/main.js, DOM-driven controls, and persisted browser state.
Key state classes include:
- Loaded activities
- Global date filters
- Per-tab gear filters
- Athlete tab sport and data-type selectors
- User settings such as unit system, age, and max HR
- Cached API payloads and timestamps
- AI chat history and Gemini API key
Tabs are lazily rendered and tracked through a renderedTabs set so initial load cost remains bounded.
Filtering operates in three layers:
- Global tab-local date filters shared across several tabs
- Domain filters such as gear, sport, chart variable, or year selectors
- Table-level query filters such as name and numeric ranges
Most filters are applied in memory to the already-loaded activity array. This keeps the interface responsive once data is fetched, but it means the browser owns the analysis workload.
Charts are generated on the client and depend on per-tab aggregation logic. The system primarily uses:
- Chart.js for line, bar, pie, scatter, and histogram-style views
- Chart.js matrix plugin for dense matrix heatmaps
- Cal-Heatmap for GitHub-style calendar consistency views
- Leaflet and Leaflet.heat for map rendering
- D3 as a supporting visualization dependency
Authentication uses Strava OAuth 2.0. Tokens are stored locally and refreshed server-side when they are about to expire. Each API request sends a base64-encoded token payload in the Authorization header to the Vercel backend, where the token is decoded and validated.
Important operational details:
- Activity pagination fetches all pages until exhaustion.
- Athlete, zones, and gear are cached for 24 hours.
- Activities are cached for 1 hour.
- Updated tokens are returned by backend endpoints and written back to
localStorage. - The current implementation does not include a server-side database or queue.
The app is a path-routed SPA. Vercel rewrites route segments such as /run, /dashboard, /bike, /weather, and /ai-coach to /index.html, while static asset paths and /api/* remain directly addressable.
- HTML5
- CSS
- Vanilla JavaScript using native ES modules
- Chart.js
chartjs-chart-matrix- D3.js
- Cal-Heatmap
- Leaflet
- Leaflet.heat
- Vercel serverless functions
- Node.js runtime
node-fetch
- Strava API v3
- Open-Meteo historical archive API
- Gemini Flash preview endpoint for AI Coach
- Vercel Speed Insights
- Vercel
- PWA manifest and service worker for installability and static-asset caching
- Node.js 18+
- A Strava account
- A registered Strava developer application
- The included local Node.js dev server
Configure these server-side variables for local development and deployment:
STRAVA_CLIENT_IDSTRAVA_CLIENT_SECRET
The AI Coach feature does not require a server environment variable. The user supplies a Gemini API key inside the UI, and the key is stored only in the browser under localStorage.
npm installCreate local environment variables, install dependencies, and run the local dev server:
cp .env.example .env.local
npm install
npm run devnpm run dev runs scripts/local-dev-server.mjs, a small Node http server that serves the static frontend and dynamically imports the matching file in api/ for any request to /api/*. The default port is 3001 (override with PORT). Required env vars: STRAVA_CLIENT_ID and STRAVA_CLIENT_SECRET from .env.example.
Then open the local URL printed by the dev server and authenticate with Strava. To browse without a Strava account, click the demo button in the connect screen (js/demo/index.js sets localStorage('strava_demo_mode')='true' and generates ~250 synthetic activities). See LOCAL_SETUP.md for the full Strava OAuth setup and troubleshooting checklist.
- Create or configure your Strava developer application with the correct redirect URL.
- Copy
.env.exampleto.env.localand setSTRAVA_CLIENT_IDandSTRAVA_CLIENT_SECRET. - Install dependencies with
npm install. - Run
npm run dev. - Open the app and connect with Strava.
- If you want AI Coach, paste your Gemini API key into the AI tab after the app loads.
For production deployment on Vercel:
- Import the repository into Vercel.
- Configure the same Strava environment variables.
- Ensure the Strava redirect URI matches the deployed domain.
- Deploy and validate OAuth login, token refresh, and route rewrites.
The project contains explicit data-quality logic rather than assuming raw API cleanliness.
- Missing fields are handled with fallbacks in most summary calculations.
- Cached JSON is guarded against parse errors.
- Altitude outliers are replaced using a Hampel-style filter.
- GPS spikes are interpolated based on neighboring points.
- Speed anomalies are smoothed to prevent visually unstable stream charts.
- Swim records without GPS are treated as indoor/pool candidates.
- Historical swim distance correction is applied for a known athlete-specific case.
The detailed analysis pipeline performs several denoising steps:
- GPS jitter mitigation through spike interpolation
- Altitude smoothing through moving windows after anomaly correction
- Grade smoothing
- Speed smoothing only on moving segments
- Derived-metric recomputation after cleaning
These steps improve downstream climb detection, fatigue heuristics, and chart readability.
Across the app, the codebase uses:
- Daily bucketing for calendars and TSS distributions
- ISO-style weekly aggregation for trend charts and streak logic
- Monthly aggregation for seasonal and annual views
- Rolling means such as 5-week run and bike volume trends
- Exponential moving averages for ATL and CTL
Notable domain metrics include:
- Pace in min/km for running
- Pace per 100 m for swimming
- Speed in km/h for cycling and general movement sports
- Elevation ratio in m/km
- TSS with power, HR-zone, HR-ratio, suffer-score, and time-only fallback modes
- VO2max estimate for qualifying runs
- CTL, ATL, TSB, and injury-risk proxies
- HR zone time distribution
- Pool-length estimation for swims
- VAM and climb segmentation for analyzed activities
- Fatigue onset and severity heuristics
- Environmental difficulty scoring for weather-enriched runs
This application is analytics-heavy but intentionally lightweight on opaque machine learning. Most models are deterministic, interpretable, and built for personal training analytics rather than offline supervised training.
- Riegel prediction formula for endurance race-time scaling
- VDOT-style prediction logic for running performance transfer
- Personal curve fitting in the Predictor tab
- Heuristic run classification
- Heuristic bike classification
- HR-zone-based load estimation
- Fatigue onset heuristics on stream windows
- Environmental difficulty scoring
These methods are appropriate for a browser-first product because they:
- Are explainable to end users
- Require no external model-serving layer
- Degrade gracefully when data is incomplete
- Fit the personal analytics use case better than heavyweight generalized models
The codebase does not expose a formal offline evaluation suite for prediction accuracy or classifier benchmarking. Instead, it favors transparent formulas whose failure modes are understandable. In practical terms, the quality of outputs depends on:
- Coverage of heart rate, cadence, and power streams
- Correctness of Strava metadata
- Availability of race-like efforts in personal history
- GPS quality and route geometry
The project uses a broad visualization set, each chosen for a specific analytical role.
- Summary cards communicate scalar KPIs quickly.
- Line charts show progression and rolling behavior over time.
- Scatter plots reveal pace-distance, power-speed, and temperature-performance relationships.
- Histograms show distributions and variability.
- Pie charts expose composition, such as bike-type or sport-share splits.
- Calendar heatmaps communicate consistency and streak behavior.
- Matrix heatmaps reveal temporal patterns across combinations like hour by weekday or year by month.
- Leaflet route maps and density heatmaps provide geographic context.
Interactivity includes:
- Tab-level lazy rendering
- Sortable tables
- Gear, date, year, sport, and variable selectors
- Route mode versus heatmap mode on maps
- Sliders and scenario selectors in the Predictor tab
- AI-chat prompt entry and persisted conversation history
- All dashboard analytics execute in the browser after data fetch.
- Detailed activity analysis also runs on the main thread, so large activities may temporarily block the UI.
- Activity history pagination can trigger many Strava calls for long-tenure athletes.
- Weather enrichment increases network cost for runs with GPS coordinates.
localStorageis simple and privacy-friendly, but not ideal for large structured datasets.- Lazy tab rendering reduces first-paint cost by deferring chart generation.
- No database means no long-term server-side persistence, offline sync, or multi-device shared state.
- Strava rate limits remain a practical constraint for large activity histories and repeated refreshes.
- Some analytics depend on HR, cadence, power, or GPS streams that may be absent.
- Swim distance correction includes a hardcoded athlete-specific rule and is not generalized.
- Detailed analysis is richer than the tab-level summaries, which means some metrics exist only on activity pages.
- The project currently relies on local browser storage for tokens and preferences.
- The local development workflow depends on the included Node.js dev server so API routes work during Strava OAuth.
- The predictor and classification layers are heuristic and should be treated as decision support, not ground truth.
- Weather enrichment is approximate and based on hourly historical data, not exact on-route microclimate.
- GPX export is reconstructed from processed streams rather than original device files.
- Introduce Web Workers for heavy stream analysis.
- Add IndexedDB for larger local caches and better offline behavior.
- Generalize athlete-specific correction logic into configurable user settings.
- Add formal test coverage for predictor outputs and classification heuristics.
- Expand sport-specific models for trail running, open-water swimming, and structured cycling sessions.
- Introduce server-side aggregation or snapshot caching to reduce repeated Strava fetch pressure.
api/ Vercel serverless functions (Strava OAuth proxy and endpoints)
_shared.js shared header/token helpers
config.js Strava client id / secret bridge
strava-auth.js OAuth code exchange + refresh
strava-activities.js paginated activity history
strava-activity.js single activity metadata
strava-athlete.js athlete profile
strava-gear.js gear metadata
strava-streams.js Strava activity streams
strava-zones.js training zones (HR + power)
html/ Dedicated detail-page HTML shells
activity-router.html, activity.html, run.html, bike.html, swim.html, gear.html, wrapped.html
media/ Per-tab background images (see media/README.md)
scripts/
local-dev-server.mjs Node dev server used by `npm run dev`
js/
main.js Re-exports from js/app/main.js for direct script loading
app/ App bootstrap, OAuth, navigation, shared UI orchestration
main.js, auth.js, ui.js
tabs/ Renderer modules for the thirteen main SPA tabs
dashboard.js, run-analysis.js, bike-analysis.js, swim-analysis.js,
athlete.js, planner.js, gear.js, activities.js, calendar.js,
weather.js, maps.js, wrapped.js, ai-chat.js
index.js, utils.js, api.js barrel and tab-local helpers
pages/ Controllers for dedicated detail pages
activity/ activity.js, advanced-analysis.js, analysis-ui-components.js
run/, bike/, swim/, gear/ per-sport detail pages
shared/
preprocessing/ core.js — preprocessActivities and derived fields
utils/ core.js — formatters, math, helpers; weather-analysis.js; speed-insights.js
analysis/ Stream-level activity analysis pipeline
config.js, index.js, preprocessing.js, virtual-gpx.js
analyzers/ base-analyzer, running, trail-run, cycling, gravel-mtb, hiking
detection/ climbs, stops
engines/ fatigue, aero, physiology, insights-generator
segmentation/ distance/time/terrain segmentation
export/ gpx, csv, json
services/ API clients and browser cache layer
api.js, index.js
demo/ Offline demo-data generator and gating
index.js, generator.js, polylines.js
models/ Analysis-domain typed objects
activity-track.js, track-point.js, segment.js, climb.js, analysis-result.js
styles/
style.css single global stylesheet, includes sport CSS variables
index.html SPA entrypoint
sw.js Service worker (PWA, static-asset caching)
manifest.json PWA manifest
vercel.json Route rewrites
classifyRun.js, classifyBike.js Standalone classification heuristics
styles/style.css defines four sport color CSS variables on :root:
--sport-default-color: #fc4c02(Strava orange, used as the global accent)--sport-run-color: #2e7d32--sport-bike-color: #1565c0--sport-swim-color: #00838f
The Run Analysis (#analysis-tab), Bike Analysis (#bike-tab), and Swim Analysis (#swim-tab) containers override .chart-container border-top-color and h3 color to the matching sport color so charts inside those tabs are visually grouped. Background images per tab follow the mapping in media/README.md.
The full engineering-oriented breakdown of tabs, ETL stages, visualizations, analytics logic, and implementation caveats is documented in TECHNICAL_GUIDE.md.