-
Notifications
You must be signed in to change notification settings - Fork 0
Features

The /athlete/activities endpoint returns full activity objects with real Strava IDs and real start_date_local dates — no approximation. Activities are deduped by Strava ID.
- Sortable table — distance, time, stopped time, elevation, avg/max speed, VAM (vertical ascent rate in m/h), avg HR, avg power, work in kJ. Power / HR / VAM columns show "—" when the activity doesn't carry that data.
-
Year / month / sport-type filters — all client-side; the page fetches a single
activities.jsonand filters in the browser. - Period "bests" strip — longest ride, most climbing, fastest avg speed, best VAM, most work — recomputed for the current filter selection.
- Monthly bar charts — distance, time, and elevation per month for the selected year.

Once an activity has its cached detail file (GET /activities/{id}), its name in the dashboard links to activity.html?id=<id>.
Stat cards: distance, time, stopped time, pace/speed, elevation gain, VAM, climb per km, avg/max HR, avg/max cadence, avg power, normalized power, variability index, work (kJ), calories, relative effort, temperature + weather icon + feels-like + source badge (hist / fcst / device), wind speed + direction arrow, precipitation, gear name.
Cycling-specific metrics (VAM, power, work) appear only when present. The temperature badge distinguishes device-sensor readings from Open-Meteo historical/forecast values.
Charts (each shown only when the activity carries that data):
- Interactive route map — Leaflet + OpenStreetMap tiles; needs internet in the viewing browser
- Per-km splits bar chart
- Elevation profile chart
- Heart rate chart
- Cadence chart
Links: "Raw JSON" and "Open on Strava" are always shown.
For each activity, temperature (actual + feels-like), wind speed + direction, WMO weather code (rendered as an emoji), and precipitation are fetched from Open-Meteo using the activity's date and GPS start location.
- The ERA5 archive API is tried first (covers dates up to ~5 days ago); the forecast API is the fallback for very recent activities.
- Device-sensor temperature from Strava is preserved as-is with a "device" badge.
- Both Strava and HealthSync / Magene activities receive weather fields.

Strava's /clubs/{id}/activities feed returns no dates and no activity IDs — it's just recent activity. The script works around this by accumulating a persistent NDJSON store and stamping each newly seen activity with today's date as "first seen". In scrape mode, real activity dates are available directly.
- Month / year filter — defaults to the current month; filter back through full accumulated history.
-
Multiple clubs — set
STRAVA_CLUB_IDS="123456,789012"(comma-separated); each club gets its own store and per-club leaderboard JSON; the dashboard lets you switch between them. - Ranking — grouped by athlete, summed distance / time / elevation, ranked by total distance, avg speed in km/h.
- Scrape mode — uses the same internal feed endpoint Strava's web app uses; gives real activity dates and IDs. Required after the Club Activities API was deprecated in September 2026.

A separate page at /strava/me/stats.html (linked from the My Activities header).
- KPI cards — total distance, time, elevation, activity count, avg speed for the selected sport + year filter.
- Year overview table — one row per year.
- Monthly breakdown — bar chart + table.
- Year-over-year km/month heatmap — all years × all months in a single grid.
- Personal records — longest, most climbing, fastest avg speed, best VAM, most work — all-time for the selected sport.
- Sport breakdown — distance and time per sport type.
- Day-of-week chart — average distance per day of week.
All computed client-side from activities.json.

A full-viewport page at /strava/me/heatmap.html (linked from the My Activities header and footer).
- Leaflet.heat overlay — all GPS-tracked activities rendered as a heat layer. Routes you ride frequently appear brighter (blue → orange → yellow gradient).
- Period filter — Last 3 months (default), Last 30 days, Last 7 days, All time, or individual years built dynamically from your data.
- Sport-type filter — defaults to Ride; built dynamically from unique sport types in your data; "All sports" shows everything.
- City labels — Esri World Dark Gray Reference tile layer on a custom pane (z-index 450, above the heat canvas) so city and road names stay readable. Removed automatically if the base layer falls back to OpenStreetMap (which includes built-in labels).
- Dark basemap — Esri World Dark Gray Base (free, no API key required); falls back to OSM on tile error.
- Activity + point count shown in the top bar; map auto-fits to the visible tracks.
heatmap.json is generated alongside the other pages on each cron run. Every 10th GPS track point is sampled (~11 m precision) to keep the file small. Generation runs last so the dashboard, stats, and other pages are available immediately.
A read/write page at /strava/me/bike.html. All other pages are static; this one saves data through a CGI.
Every mileage figure is computed live in the browser from activities.json: cumulative distance of outdoor Ride activities up to a chosen date. A calendar picker (default: today) lets you pick any date and mileage auto-recomputes instantly. Map a bike to a Strava gear and only that bike's rides count; leave it unmapped and all rides are attributed.
Each part records the date it was fitted and the bike's mileage at that moment. You can:
- Service a part — logs date, current mileage, a free-text note, and an optional service cost.
- Replace a part — the old part moves to an Archived section with final mileage and calendar duration ("1 year 5 months 2 weeks"); optionally a successor is fitted on the same day with its purchase cost recorded.
Each part can have one or more named service types — e.g. a chain can have "Clean & Lube" (every 500 km) and "Replace" (every 2 000 km). Each type has its own independent thresholds:
- km threshold — distance since last service of that type
- hours threshold — riding time since last service
- calendar-time threshold — N weeks / months / years (useful for suspension, cables, or time-based intervals regardless of riding distance)
All three thresholds combine: the highest percentage of the three drives the progress bar. Once any type exceeds 100 % its row is highlighted in yellow. Existing single-threshold parts are migrated to a single "Service" type automatically on first load.
Each part has an optional purchase cost field, and each service record has an optional service cost field. When any cost is recorded the bike header shows a Total cost summary. When costs span more than one year a collapsible By year breakdown appears.
The currency code is set by STRAVA_MY_CURRENCY in /etc/strava-my-activities.conf (default PLN). It is appended to every displayed amount — e.g. 25.00 PLN or 12.50 EUR. Costs are stored as plain numbers in bike-service.json; changing the currency config only changes the label shown in the browser.

Track as many bikes as you like; each is a separate tab. The initial bike name is set by STRAVA_MY_DEFAULT_BIKE_NAME in the config and is used only as a seed when no bikes are stored yet.
The "Email alert when service threshold is reached" checkbox is always visible in the Add / Edit part modal. Tick it for any part you want to be notified about. Emails are only sent when STRAVA_MY_BIKE_EMAIL is set in /etc/strava-my-activities.conf; until then the checkbox shows a small grey hint (set STRAVA_MY_BIKE_EMAIL to enable).
| Threshold % | Behaviour |
|---|---|
| ≥ 90 % | WARNING email — service due soon |
| ≥ 100 % | ALERT email — service overdue; re-sends every 7 days while still overdue |
Each tier fires exactly once per threshold crossing — you won't receive the same warning every day. When a part is serviced and the % drops back below 90 %, the tier resets so you'll be warned again next cycle.
Notification state is tracked in $STATE_DIR/bike-email-state.json. Requires STRAVA_EMAIL_SMTP and STRAVA_EMAIL_USER to be set (same format as the monthly leaderboard email). See Email Notifications for SMTP setup.
The page reads and writes bike-service.json through a tiny POSIX-sh CGI installed to STRAVA_MY_CGI_DIR (default /www/cgi-bin). The CGI is the only writer — daily cron runs that regenerate bike.html never touch your data. No auth: open-on-the-LAN, intended for a private home router only.
Every page except the heatmap (which is already dark by design) has a 🌙 / ☀️ toggle button fixed to the top-right corner of the screen.
| Light | Dark |
|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
How it works:
- The default theme follows the OS
prefers-color-schemesetting — no configuration needed. - Clicking the toggle manually overrides the OS preference and stores the choice in
localStorage, so it persists across page loads and browser restarts. - The chosen theme is applied immediately via an anti-FOUC inline script in
<head>— there is no flash of the wrong theme on page load. - All colours are CSS custom properties; SVG chart fills and strokes use
var(--accent),var(--chart-grid), etc., so charts adapt without re-rendering. - Weather source badges (
.wx-arch,.wx-fcst,.wx-dev) have distinct dark-mode palettes that maintain sufficient contrast.
Each run reconciles the activity store against the feed:
- Renamed rides, corrected sport types, recalculated distance/time are updated in place.
- Activities deleted on Strava are pruned from the dashboard (cached detail file removed; changed activities have their detail re-fetched).
- Deletion is conservative — only happens when the run reached the end of the feed, or within the date window actually fetched on a capped run. An empty feed never prunes.
- Set
STRAVA_MY_PRUNE_DELETED=0to disable deletions (additions and in-place updates still apply).
Beyond the summary feed, each run fetches the full activity object for activities that don't yet have a detail file. Because Strava's read API is rate-limited (100 req / 15 min, 1 000 / day), it fetches at most STRAVA_MY_DETAIL_MAX_PER_RUN new files per run (newest first). History backfills gradually; activities Strava reports gone (HTTP 404/410) are recorded in a skip list and not retried.
In scrape mode the script fetches each activity's HTML page and extracts stats from Strava's embedded bootstrap data. It also downloads the GPX export per activity so the Leaflet route map works.
The My Activities dashboard shows a colour-coded banner indicating when the _strava4_session cookie was last verified and when it will expire (~30 days). Green = fresh, amber = expiring soon, red = expired.
When both a Magene FIT file and a HealthSync watch export cover the same ride (start times within ±10 min, end times within ±5 min), the two records are merged:
- Watch record keeps its heart-rate data (which the Magene doesn't have).
- Watch record gains the Magene's wheel-sensor–accurate distance, speed, cadence, and elevation.
- Merged record carries
dual_source: trueand amagene_idback-reference.
For a standalone Magene activity (no matching watch record), distance is extracted directly from the FIT binary's odometer field — wheel-sensor accuracy. Falls back to GPS Haversine if the FIT doesn't contain usable odometer data.




