BikeControl (SwiftControl)
使用 Zwift Click、Zwift Play、Shimano Di2,或任何藍牙控制器搭配 MyWhoosh、Rouvy、Zwift 等。可於 iOS、Android、Windows 與 macOS 免費試用。
AI 翻譯
尚未收集儲存庫資訊。
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗A personal training platform built exclusively for runners. Generates science-based 2-week rolling training plans that adapt based on real performance data from Garmin Connect.
Built by Jake Bowen (CS junior at CSUF, 2:58 marathoner, targeting sub-17 5K).
Blend of established coaching methodologies:
Key principles:
stride/
├── CLAUDE.md # AI coaching instructions & project spec
├── README.md
├── backend/
│ ├── app/
│ │ ├── main.py # FastAPI entry point
│ │ ├── config.py # Environment settings
│ │ ├── database.py # SQLAlchemy setup
│ │ ├── models/
│ │ │ ├── user.py # User + Goal models
│ │ │ └── activity.py # Activity + PersonalRecord models
│ │ ├── api/
│ │ │ ├── garmin.py # Garmin status / sync / full re-import
│ │ │ ├── analysis.py # Analysis, PRs, activities, classification endpoints
│ │ │ └── plans.py # Plan generation + goal endpoints
│ │ ├── services/
│ │ │ ├── garmin.py # Garmin Connect sync (incremental + full, Strava-history merge)
│ │ │ ├── lap_analyzer.py # Garmin laps → workout summaries
│ │ │ ├── analyzer.py # Athlete analysis, PR detection, classification
│ │ │ └── pace_calibrator.py # Parse workout descriptions for real paces
│ │ └── core/
│ │ ├── plan_engine.py # 2-week rolling plan generator
│ │ ├── pacing.py # VDOT pacing, pace calculations
│ │ └── workouts.py # Workout library (threshold, intervals, long runs)
│ ├── requirements.txt
│ └── .env # OPENAI_API_KEY, GARMIN_API_KEY etc. (not committed)
├── frontend/
│ ├── app/
│ │ ├── page.tsx # Dashboard (today page)
│ │ ├── plan/page.tsx # 2-week training plan
│ │ ├── history/page.tsx # Run history with filters
│ │ ├── settings/page.tsx # Profile, paces, PRs, connections
│ │ ├── onboarding/page.tsx # 8-step onboarding flow
│ │ └── activity/[id]/review/page.tsx # Post-run feel rating
│ ├── components/
│ │ ├── nav.tsx # Top nav + mobile bottom tabs
│ │ ├── plan-week.tsx # Week container for plan view
│ │ ├── plan-day.tsx # Expandable day row with workout details
│ │ ├── run-list.tsx # Activities grouped by month (with descriptions)
│ │ ├── run-detail-sheet.tsx # Activity detail sheet (with workout analysis)
│ │ ├── charts-wrapper.tsx # Card wrappers for Recharts
│ │ └── charts/ # Weekly mileage, pace, mileage-over-time charts
│ ├── lib/
│ │ ├── types.ts # TypeScript types
│ │ ├── format.ts # Pace/time/distance formatting
│ │ └── utils.ts # cn() utility
│ └── package.json
└── docs/
├── training-principles.md # Core training methodology
├── athlete-profile-jake.md # Jake's runner profile & history
├── 5k-training-science.md # 5K-specific training science
├── 10k-training-science.md # 10K training science
├── half-marathon-training-science.md
├── marathon-training-science.md
├── marathon-training-notes.md # Marathon-specific notes
├── ultra-training-science.md # 50K / 50mi / 100K / 100mi training + how pros train
├── frontend-plan.md # Frontend design document
├── todo-may27.md # May 27 feedback & fixes
├── changes-may27.md # May 27 changelog
└── todo-may28.md # May 28 plan (Strava full sync)
GET /api/analysis/{user_id}/baseline — full athlete analysisGET /api/analysis/{user_id}/calibrated-paces — paces from real workout dataGET /api/analysis/{user_id}/activities — paginated activities with filter (?type=workouts|long_runs|easy)GET /api/analysis/{user_id}/weekly-mileage — weekly mileage dataGET /api/analysis/{user_id}/prs — personal recordsPOST /api/analysis/{user_id}/full-sync — re-sync all Garmin data + classify + PRs + analysisPOST /api/analysis/{user_id}/classify — classify all activities by typePOST /api/analysis/{user_id}/detect-prs — run PR detectionPOST /api/plans/{user_id}/generate — generate 2-week training planGET /api/plans/{user_id}/goal — get active goalGET /api/garmin/status/{user_id} — connection + last sync infoPOST /api/garmin/sync/{user_id} — pull new runs (first ever sync pulls full history)POST /api/garmin/full-sync/{user_id} — re-import all Garmin history in the background# 1. One-time: install, then put your Garmin Personal API key in backend/.env as GARMIN_API_KEY=...
# (create it at https://connect.garmin.com/app/settings/personalApiToken)
cd backend && python3 -m venv venv && venv/bin/pip install -r requirements.txt
# 2. Start everything (backend + frontend), prints the phone URL
cd .. && ./start.sh
http://<mac-ip>:3000 URL that start.sh prints → Share → Add to Home Screen. It opens full-screen like an app.start.sh prints.OPENAI_API_KEY=... to backend/.env (without it, the coach uses template responses). See docs/ai-call-inventory.md for exactly what each review sends.Stride uses Garmin's official Connect Personal API (opened Oct 2026). GARMIN_API_KEY in
backend/.env is exchanged for a 24-hour bearer token, held in memory only. The API is pull-only
(no webhooks), so Stride syncs when the app opens and checks in the background every
GARMIN_POLL_MINUTES (default 15, hourly during GARMIN_QUIET_HOURS, default 22-5); any sync
resets that clock. A check is one request for runs since the newest stored run, with Garmin filtering
non-runs. Only a new run costs more: one FIT download for its laps. Wellness uses three range requests.
The first sync imports your full history. Keys last 30 days to a year. When Garmin rejects the key,
Settings says so; create a new key and update GARMIN_API_KEY.
Not available from the Personal API: training readiness, training status, VO2max, Garmin race
predictions and activity notes. Those cards hide themselves.
One always-on Render Starter web service (~$7/mo) runs both processes from the root
Dockerfile: FastAPI on 127.0.0.1:8000 (private) and the Next.js standalone server on
$PORT (public, password-gated). SQLite lives on a 1 GB persistent disk at
/var/data. The Garmin poller runs in-process (every 15 minutes, hourly overnight). Config is in render.yaml;
the entrypoint is deploy/start.sh.
Create the service. Render dashboard → New → Blueprint → pick this repo (branch main).
Enter the sync: false secrets when prompted:
STRIDE_PASSWORD: the app password (if it's empty, every page returns 503 instead of going public)OPENAI_API_KEY: the AI coach key (optional; without it the coach uses templates)GARMIN_API_KEY: your Garmin Connect Personal API keyApply. The first deploy starts with an empty database on the disk.
Copy your data. Stop ./start.sh locally so the DB isn't being written, then take a
consistent snapshot (it includes WAL contents) and upload it over Render SSH. Add your SSH key
under Account Settings → SSH Keys; the service's Connect → SSH shows its address.
sqlite3 backend/stride.db ".backup /tmp/stride-upload.db"
scp /tmp/stride-upload.db <service-ssh-address>:/var/data/stride-upload.db
Then Manual Deploy → Restart service. On boot, deploy/start.sh moves
stride-upload.db into place as stride.db before the backend opens it, and keeps the empty
first-boot DB as stride.db.before-import-<time>. Don't overwrite stride.db directly while the
service is running. Delete /tmp/stride-upload.db afterwards: it is your personal health data.
Garmin. Nothing to bootstrap: the key is an env var, and tokens are exchanged in memory.
On the Render service's Environment page, set GARMIN_API_KEY to your
Garmin Personal API key and save and redeploy. backend/.env stays on your
Mac; Git pushes and database uploads do not transfer its secrets. Older
GARMIN_TOKENS_JSON and GARMIN_TOKENSTORE settings do not configure the
current Personal API client. After deploying, Settings must report a key
present, and a manual sync must finish with a new successful check time.
If you previously disconnected in Stride, use Reconnect Garmin using server
key in Settings to explicitly re-enable it. Reconnecting clears the old
authentication retry delay and queues a new sync; it does not expose the key.
Check it. Open the .onrender.com URL and sign in. Settings should show Garmin connected,
and a run should appear when you open the app (or within ~15 minutes) after a watch upload. Run date in the Render shell; it
should print Pacific time (; plan logic uses ).
GARMIN_API_KEY in the Render dashboard and restart.NODE_OPTIONS=--max-old-space-size=192.
Watch the Metrics tab. If the service sits above ~450 MB, split the frontend into its own service
(or move it to Vercel) and point BACKEND_URL at the backend. Note that BACKEND_URL is baked in
at build time by the /api rewrite.MVP functional, now on Garmin:
./start.shThe Render blueprint now allocates 5 GB, retains seven local snapshots, and caps
snapshot retention at 512 MiB (STRIDE_BACKUP_MAX_BYTES). Existing Render disks
must be resized in the dashboard; a blueprint edit does not prove the live disk
was resized. Backups reserve space for verification and preserve the latest
snapshot when space is insufficient. Operations reports capacity warnings.
Configure the existing encrypted S3 export for off-disk recovery, and verify a
restore with python -m app.services.backups --restore BACKUP --destination NEW_DB.
No setting alone proves an offsite backup or a successful recovery.
Account mode is opt-in: set STRIDE_ACCOUNTS_ENABLED=true for both frontend and
backend, STRIDE_SIGNUP_INVITE_CODE on the backend, and STRIDE_PUBLIC_URL to the
public HTTPS origin. Cookies are Secure by default. Only isolated localhost
previews should set STRIDE_COOKIE_SECURE=false. Do not expose the backend on a
public interface. /auth/register creates a separate athlete; it never claims
an existing athlete's history. API ownership is checked on the backend even when
someone requests a numeric athlete URL.
Before switching an existing owner to account mode, attach an account locally:
cd backend && venv/bin/python -m app.services.accounts bootstrap --user-id 1 --email YOUR_EMAIL.
The command prompts for a password; do not put it in shell arguments. Operator
recovery uses python -m app.services.accounts recovery --user-id ID; give the
single-use 30-minute token privately to the athlete, who uses /auth/reset.
Email delivery/verification is intentionally not provisioned; registration is
invite-only. Public registration remains a separate deployment decision.
Athletes can change AI consent and timezone, export their own history, or erase their live account in Settings. Consent is off by default. Erasure does not erase already-created offsite backups immediately: the operator must define retention and let those encrypted backups expire. Never enable a shared owner Garmin key for additional athletes. Garmin multi-user integration still requires Garmin's approval and the appropriate historical-data entitlement.
The container runs python -m app.schema once before starting the web process.
Web replicas use SCHEMA_AUTO_UPGRADE=false and EMBEDDED_WORKERS=false, so
manual syncs are queued. On the single 512MB Render instance the job loop runs
inside uvicorn (STRIDE_IN_PROCESS_WORKER=true, the start.sh default) instead of
a second Python process; set it to false to run python -m app.services.worker
separately. The loop logs its process memory every 10 minutes (Memory rss=…).
Local development still supports embedded work. Sync jobs persist per athlete, use leases/retries, and
resume after a worker restart; PostgreSQL claims use row locks with SKIP LOCKED.
A schema version marker plus missing columns is treated as drift, not silently
repaired. Future changes require a new Alembic revision; do not edit the frozen
baseline manifest after release. Existing source/data migrations remain separate.
For a larger deployment use a managed PostgreSQL URL (postgresql+psycopg://…),
with the provider's encrypted backups and point-in-time recovery enabled. A
PostgreSQL CI service exercises migrations, snapshot transfer, sequence repair,
and concurrent job claims. Cutover procedure:
python -m app.services.database_transfer --source-snapshot COPY --destination-url EMPTY_POSTGRES_URL in a private operator shell. It reads the snapshot without modifying it, checks counts, and repairs PostgreSQL sequences. The destination must contain no athlete data.DATABASE_URL. Keep the original database and verified backups for rollback. Never run two writable deployments against separate copies during cutover.FIT_BLOB_BUCKET optionally stores new FIT evidence in private S3 objects,
content-addressed under each athlete. Original inline evidence remains readable.
Configure the bucket's block-public-access, encryption, least-privilege IAM, and
version/lifecycle policy. Database backups alone do not back up external objects.
Exports stream original evidence; account erasure queues idempotent object
removal. Failed SQL transactions may leave unreferenced objects; retain objects
until a separately verified reference/lifecycle cleanup rather than deleting
unreferenced data during ingestion.
STRIDE_PROVIDER_ENCRYPTION_KEYS accepts comma-separated Fernet keys (newest first)
for stored owner credentials. Keep keys in a secret manager, separate from database
backups. Rotate by adding the new key while retaining previous keys until stored
credentials are re-encrypted. Generate with Fernet.generate_key() from the
cryptography library. No provider secret is returned in APIs or exports. The
existing environment owner key remains supported in owner mode only. Separate
athletes cannot reuse it; live Business transport remains disabled pending Garmin
approval and its integration contract. Original FIT uploads work without Garmin.
Settings accepts a maximum duration for each weekday, including bookends and recoveries. Blank means no daily limit; use rest-day settings for unavailable days. Preview a revised block to apply changes. A full quality dose moves to a roomier safe easy day before its work is shortened. Paces are never increased to force a fit; shortened main work cannot earn full-dose progression. Run/walk sessions retain their walk recoveries. Race-day time is a separate event exception.
The plan states the remaining run frequency, priority sessions, estimated time, and what earns progression versus holding/reducing. The outlook exposes conflicts between finite availability and the goal. History shows cached 4/8/12-week trends, quality evidence, approximate matched easy-run comparisons, and long-run follow-up, with missing data and uncertainty kept explicit. Older runs use cursor pagination.
AI remains optional and cannot directly approve plan changes. Set
OPENAI_COACH_GLOBAL_DAILY_LIMIT (default 100 reviews across the app) alongside
the existing per-athlete limit. Operations reports token use and attempt latency;
provider billing remains the authoritative monetary cost. The offline review
rubric and grader are in docs/evaluation/; ungraded prose is never reported as
coach-approved. No hosted eval service or additional agent system is required.
See the invite beta release record for the five PRs, validation and deployment prerequisites.
TZ=America/Los_Angelesdate.today()