Skip to content

Operations

Jakub Raczek edited this page Sep 24, 2026 · 7 revisions

Operations

File and URL reference

What Where
Club leaderboard dashboard http://<router-ip>/strava/
Club activities JSON http://<router-ip>/strava/activities.json
Per-club all-time JSON http://<router-ip>/strava/leaderboard_<clubid>.json
Club activity store $STRAVA_STATE_DIR/activities_<clubid>.ndjson
Dated leaderboard snapshots $STRAVA_STATE_DIR/snapshots/YYYYMMDD_<clubid>.json
Club token state $STRAVA_STATE_DIR/token.json (chmod 600)
Club leaderboard log /var/log/strava-leaderboard.log
My Activities dashboard http://<router-ip>/strava/me/
Activity detail page http://<router-ip>/strava/me/activity.html?id=<id>
Personal stats summary http://<router-ip>/strava/me/stats.html
Activity heatmap http://<router-ip>/strava/me/heatmap.html
Heatmap GPS data http://<router-ip>/strava/me/heatmap.json (sampled track points)
Heatmap city labels http://<router-ip>/strava/me/cities.json (generated from Overpass API each run)
Bike service tracker http://<router-ip>/strava/me/bike.html
Bike service CGI (read/write) http://<router-ip>/cgi-bin/bike-service
Bike service data store $STRAVA_MY_BIKE_DATA (default $STRAVA_MY_STATE_DIR/bike-service.json)
Annual goals CGI (read/write) http://<router-ip>/cgi-bin/ride-goals
Annual goals data store $STRAVA_MY_GOALS_DATA / $HEALTHSYNC_GOALS_DATA (default $STATE_DIR/ride-goals.json)
My activities JSON http://<router-ip>/strava/me/activities.json
Per-activity detail JSON $STRAVA_MY_DETAIL_DIR/<id>.json (default …/strava/me/details/)
My activity store $STRAVA_MY_STATE_DIR/activities.ndjson
Detail backfill skip list $STRAVA_MY_STATE_DIR/detail-skip.txt
My activities token state $STRAVA_MY_STATE_DIR/token.json (chmod 600)
My activities log /var/log/strava-my-activities.log
HealthSync Drive auth status $HEALTHSYNC_WEB_DIR/drive-status.json (ok:true / ok:false)
HealthSync re-auth CGI http://<router-ip>/cgi-bin/drive-auth (non-functional — device flow blocked for Drive scopes; use OAuth Playground + SSH instead)
HealthSync log /var/log/healthsync-activities.log

Note: on this project's router the state directories are under /mnt/sda5/ (USB persistent storage, not flash overlay).


Scripts installed to /usr/bin/

Script Type Purpose
strava-leaderboard executable (0755) Club leaderboard main script
strava-my-activities executable (0755) My Activities main script
healthsync-activities executable (0755) HealthSync / Google Drive main script
strava-cron-guard executable (0755) Cron wrapper with retry + alert
strava-email-monthly executable (0755) Monthly summary email
strava-email-weekly executable (0755) Weekly summary email
strava-lib.sh sourced (0644) Shared library: token refresh, weather, curl retry
strava-my-html-dashboard.sh sourced (0644) Renders index.html
strava-my-html-detail.sh sourced (0644) Renders activity.html
strava-my-html-bike.sh sourced (0644) Renders bike.html + installs bike CGIs
strava-my-html-stats.sh sourced (0644) Renders stats.html
strava-my-html-heatmap.sh sourced (0644) Renders heatmap.html + generates heatmap.json
strava-my-feed-api.sh sourced (0644) Strava API feed pagination
strava-my-feed-scrape.sh sourced (0644) Strava scrape feed pagination + normalization
strava-my-detail-backfill.sh sourced (0644) Per-activity detail JSON backfill
strava-my-bike-alert.sh sourced (0644) Bike-service threshold email alerts
strava-render-pages.sh sourced (0644) Shared wrapper: sources dashboard+detail+bike+stats (used by both strava-my-activities and healthsync-activities)
strava-leaderboard-html.sh sourced (0644) Club leaderboard index.html heredoc
healthsync-fit-import.sh sourced (0644) Magene FIT processing + dual-source merge

Cron self-healing

Every nightly cron job is wrapped by /usr/bin/strava-cron-guard. In addition to sending alert emails on failure, the guard now provides two automatic recovery layers:

1. Network pre-flight check

Before running the script the guard pings STRAVA_NET_CHECK_HOST (default 1.1.1.1). If the host is unreachable (e.g. the ISP modem is re-connecting) the guard waits, polling every STRAVA_NET_CHECK_INTERVAL seconds (default 15 s), for up to STRAVA_NET_CHECK_WAIT seconds (default 120 s = 2 min).

  • If the network comes up within that window the script runs normally; the recovered delay is logged to syslog.
  • If the network is still offline after STRAVA_NET_CHECK_WAIT seconds the guard logs ERROR: … skipped — no network after 120s to syslog and exits cleanly. No alert email is sent for a pure network-absence abort, to avoid alarm-fatigue when the ISP is simply slow to reconnect.

The check uses a bare IP address so DNS is not required for the pre-flight itself.

2. Automatic retry on failure

If the script exits non-zero the guard re-runs it up to STRAVA_CRON_RETRIES times (default 2), waiting STRAVA_CRON_RETRY_DELAY seconds (default 300 s = 5 min) between attempts.

  • Each failed attempt is logged to syslog: WARNING: … failed (exit N) — retry 1/2 in 300s
  • The alert email is sent only after all attempts have failed.
  • The email subject includes the total attempt count: [strava-cron] strava-leaderboard failed after 3 attempt(s) (exit 1).

Config knobs (in /etc/strava-leaderboard.conf)

Variable Default Description
STRAVA_NET_CHECK_HOST 1.1.1.1 IP to ping for connectivity check
STRAVA_NET_CHECK_WAIT 120 Max seconds to wait for network
STRAVA_NET_CHECK_INTERVAL 15 Seconds between connectivity polls
STRAVA_CRON_RETRIES 2 Retries after the first attempt (0 = no retry)
STRAVA_CRON_RETRY_DELAY 300 Seconds between retries

All five knobs are optional and commented out in config.example; the defaults are used when they are absent.


Limitations and notes

Dates are approximate (API mode only)

In STRAVA_SOURCE=api mode the club feed carries no real activity dates, so each activity is dated by the day the script first saw it, not when it was performed. Run daily, that is accurate to within a day or two; an activity older than the ~2-week feed window when you first install will be dated to install day.

In STRAVA_SOURCE=scrape mode, real activity dates are available and used directly — no approximation needed. When you switch from API to scrape mode, old entries keep their first-seen dates and new ones get real dates going forward.

The store grows over time

Each club leaderboard store (activities_<clubid>.ndjson) is append-only and never pruned (only per-club snapshots/ are capped by STRAVA_KEEP_SNAPSHOTS). The My Activities store is instead reconciled with the feed each run, so it reflects edits and deletions and can shrink.

For a club the store stays small for years, but it is the one file to watch if flash is very tight — keep STRAVA_STATE_DIR on roomy persistent storage.

Names are truncated

Strava truncates last names to an initial in the club feed (e.g. John D.). Athletes are grouped by firstname|lastname|profile_medium, matching the main app's buildAthleteKey. Activities are deduped by a content signature of those names plus the activity's shape — so two genuinely identical activities by the same person collapse into one.

Rate limits

Strava allows 100 requests / 15 min, 1000 / day for a standard (non-premium) API app. A daily cron run uses a handful of requests — well within limits.

Persistent storage

Keep STRAVA_STATE_DIR off /tmp and /var — both are RAM (tmpfs) on OpenWrt and are cleared on reboot. The default /usr/lib/... lives in the overlay and survives reboots, but for larger datasets or USB drives point the config variables accordingly.

TLS

ca-bundle is required so curl can verify strava.com. This is installed by install.sh automatically.

Don't run both data-source scripts simultaneously

strava-my-activities.sh and healthsync-activities.sh have separate NDJSON stores (different state directories), so there is no storage duplication. However, both write to the same web directory (/www/strava/me) by default — whichever runs last overwrites activities.json and all HTML. The dashboard ends up showing only that source's activities.

To run both side-by-side you would need to point them at different web directories and serve at different URLs. In practice: run strava-my-activities while you still have API access, then switch cron to healthsync-activities when it ends. See Switching-Data-Sources for migration steps.

CGI must be served

The bike page saves through /cgi-bin/bike-service. uhttpd serves /www/cgi-bin as CGI out of the box and install.sh ensures it (uci set uhttpd.main.cgi_prefix=/cgi-bin). If you only scp the script instead of running the installer, confirm with uci get uhttpd.main.cgi_prefix (should print /cgi-bin).

Clone this wiki locally