OpenAthlete
AGPLv3 endurance training platform: EU-oriented hosting, transparent CTL/ATL/TSB logic in code, self-hosting, and full export. Built in Grenoble.
翻译准备中
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证A race-engineer strategy simulator for the 2026 Formula 1 Gulf Air Bahrain Grand Prix at Sepang International Circuit Malaysia
翻译准备中
取自仓库 README
正在加载完整 README。 以下为已保存的摘录。
The name was inspired by the fictional Formula 1 team featured in the feature film F1 The Movie + Jalur (Flag in Malay)
A race-engineer strategy simulator for the Sepang F1 race weekend.
Pick a session (FP1 / FP2 / FP3 / Quali / Race) and get two strategy reads —
conservative vs aggressive — with confidence scores, a lap-by-lap stint plan,
and a pit window derived from modelled tyre life, built on a live weather blend
(Open-Meteo). Then run your own what-if scenarios (rain, track temp, safety
car, forced starting compound) and the model recomputes live.查看完整 README ↗The name was inspired by the fictional Formula 1 team featured in the feature film F1 The Movie + Jalur (Flag in Malay)
A race-engineer strategy simulator for the Sepang F1 race weekend. Pick a session (FP1 / FP2 / FP3 / Quali / Race) and get two strategy reads — conservative vs aggressive — with confidence scores, a lap-by-lap stint plan, and a pit window derived from modelled tyre life, built on a live weather blend (Open-Meteo). Then run your own what-if scenarios (rain, track temp, safety car, forced starting compound) and the model recomputes live.
Next.js (Vercel) → FastAPI (Vercel) → Open-Meteo + deterministic strategy blend → MLflow run logging. No database, no auth — stateless, public, read-only.
Honest framing (no "AI"): this is a deterministic, rules-based simulator,
not a trained predictor. It blends live Open-Meteo weather with Sepang
climatology, then runs a transparent tyre/compound heuristic and a lap-by-lap
stint model to derive pit windows. The response is labelled
modelKind: "deterministic-simulator" so the UI never oversells it. MLflow logs
only the unmodified live read (what-if scenarios are exploratory and are not
logged), and the API echoes the effective inputs a read ran on.
Landing page lap preview ("The lap") — a second full-bleed,
scroll-scrubbed hero, same mechanism as the circuit flyover above it,
stacked directly beneath it on the landing page (/apple-design no
longer shares this mechanism — its hero is CircuitVideoHero, the same
video as the landing page background). Source clip is a generic,
unbranded single-seater (no team livery, no sponsor decals) filmed by a
chase drone at Sepang — genuinely clean on the car itself, so this one
skips scripts/extract-frames.py --stylize entirely — but it does carry
a burned-in speed/turn-number telemetry HUD for the whole clip, feathered
out with a masked blur (not stylized away, since the car itself needed no
treatment); a matching 9:16 frame set
(frontend/public/lap-preview-frames-9x16/) is picked on narrow
viewports instead of stretching the 16:9 frames. See
frontend/public/lap-preview-frames/README.md for the mask/verification
details. Replaced an earlier compact-GIF-card version of this section,
and an earlier still that used real official F1 broadcast footage of the
2016 Malaysian Grand Prix — a bird's-eye clip with a burned-in F1 logo
watermark and a live telemetry graphic, which docs/BRAND.md explicitly
rules out and no amount of mosaic-ing fixes (the whole clip is FOM's
copyrighted broadcast, not an incidental decal) — the current clip's own
telemetry HUD is the same category of graphic docs/BRAND.md rules out,
just fixable by masking rather than a reason to reject the clip outright.
Circuit video hero (landing page background, and /apple-design) — both
now render CircuitVideoHero, a muted, looping, controls-free background
video (hero-16x9.mp4 / hero-9x16.mp4) that behaves like a GIF, replacing
the landing page's earlier static SVG map default and /apple-design's
scroll-scrubbed frame-sequence hero. docs/BRAND.md's Imagery section now
allows real circuit/on-track footage here (this project previously rejected
real footage twice on brand-safety grounds — see the lap-preview entry
above — but the concern was official broadcast graphics and copyrighted
broadcast footage, not real footage as such) provided it clears the same
checklist a generated clip would: no burned-in broadcast graphics, no
sponsor logos or team wordmarks, no readable real venue signage, and
rights the project actually has. The current clip is owner-supplied and
was checked frame-by-frame before use (one trackside sponsor board was
found — illegible but shaped enough like a real sponsor board to fail the
signage bar — and blurred out; see frontend/public/videos/README.md for
the exact fix, verification, and full provenance). It ships as two
actually-different renders rather
than one clip CSS-cropped: a 9:16 center crop for narrow viewports and the
native 16:9 otherwise, picked via <source media> art-direction (same
mechanism as <picture> — the browser picks once at load, not on resize).
A "3D flyover" toggle in the landing hero's link row swaps the video for an
opt-in alternative (CircuitFlyoverHero) instead — a continuously-looping
procedural three.js scene, built from the same real apex-point centreline as
sepang.glb and the map (lib/circuitFlyoverTrack.ts), running the
unbranded car.glb around the loop forever and easing from a high aerial
establishing shot into a low chase cam on toggle-in. Off by default so the
video is still what visitors see first.
The shared ScrollFrameSequence component (still used by the landing page's
"The lap" section below) had two real bugs surfaced and fixed while it was
still driving both hero sections: it never set a loaded texture's
colorSpace, shifting colors once real frames replaced the solid fallback;
and it mapped scroll progress to the whole document's scroll height, so
stacking a second full-bleed section on one page (or adding content below a
single one) started or ended its frame scrub off from where the section
itself was actually pinned. Fixed by measuring progress against each
section's own sticky-plus-spacer wrapper instead (rangeRef).
Strategy simulator (/predict) — session picker, dual strategy cards
with distinct conservative/aggressive accents, confidence bars, a lap-by-lap
stint plan, a pit-window band derived from modelled tyre life, reasoning +
key risk, a confidence-delta headline backed by the MLflow same-day trend
lookup, and a monsoon hourly-rain strip. Additions in this pass:
referencedCorners).og:image (via the /og route), so a
shared read previews as a real card; a "Card" button opens the image
directly.Trip planning — a single outbound link to
Tourism Malaysia on /predict, replacing the
old in-app tourism/itinerary panel (a different user's job than race strategy).
Live transit access (/tickets) — a real-time ETL read on public transit
toward Sepang, built on Malaysia's official open-data GTFS feeds
(developer.data.gov.my) for Prasarana
(RapidKL): fetch the static route/stop feed, match it by name against the
Sepang/KLIA corridor, fetch live GTFS-Realtime vehicle positions for any
matched route, and estimate ETA via a documented heuristic (live GPS
distance ÷ the vehicle's own live speed, or a conservative fallback pace
when it's stationary) — not a trained ML model, since no historical
arrival-time data exists yet for a route that's never been mapped this way
before; this service's own live reads are the path to a real one later. Two
gaps stated plainly rather than hidden: the official race-weekend shuttle
(free Rapid KL buses from KLIA 2, Mitsui Outlet Park KLIA and Bandar Baru
Enstek, 2–4 October) is an event charter outside the standing GTFS
network, so it can't be live-tracked; and the standing bus network has no
stop at the circuit gate itself, so this reports live ETA to the nearest
real stop it can find toward the corridor, not the venue. The shuttle
itself is on /tickets as static, sourced facts (lib/raceShuttle.ts),
with pick-up points, hours, a live "running now / first bus" status and
the rail route from each line to KLIA 2. See
backend/app/services/transit_service.py's module docstring for the full
design and its verification gap (api.data.gov.my is blocked by this
project's dev sandbox, so it was built and unit-tested against a mocked
transport plus a real, synthetically-constructed GTFS-RT protobuf message —
same pattern as telemetry_service.py's OpenF1 gap).
Deep links — /predict?session=FP2 preselects that session; with no
param the page falls back to getLiveOrNextSession() against the 2026
weekend schedule (Jolpica/Ergast round 16 at Sepang — the Bahrain Grand
Prix hosted in Malaysia, 2–4 Oct) rather than a hardcoded FP1.
Circuit lore (/lore) — scroll-revealed timeline of four Sepang
moments (1999 opening, 2009 monsoon red flag, 2017 farewell, 2026
return), each tied to why it shapes a strategy read.
Tickets (/tickets) — reduced to a single outbound link to the official
Sepang International Circuit site;
pricing/seating change yearly and belong at the source, not duplicated here.
Driver grid (/drivers) — a 3D start grid of the 2026 field (22
drivers, 11 teams, career stats through the 2025 season close): the
/drive game's procedural 2026-spec car in each team's livery, staggered
8 m apart in painted grid boxes on a start straight, with a broadcast-style
name plate (headshot, three-letter code, number) over every car. Lined up
in live championship order (lib/driverGridLayout.ts), labelled as such
and never as a qualifying result; picking a driver flies the camera to
their car. The "Sepang history" set stands its three drivers on podium
plinths (1999: Irvine P1, Schumacher P2; 2009: Button P1) over the real
circuit outline, tied to moments in /lore. Each 2026-grid driver also
carries a "last time out" recap of the latest round; deep-linkable via
/drivers?driver=<id>, which opens the scene on that car.
A compact championship standings strip (top five drivers and
constructors) tracks the current, still-in-progress season live from
Jolpica/Ergast (refetched every few minutes server-side, not a fixed
snapshot) rather than hand-copied — a different, deliberately static
number from the career-totals-through-2025 stats above it.
Teams (/teams) — all 11 constructors (base, 2026 power unit,
constructors' titles, roster, and the same Zandvoort recap from the
team's side) as neutral engineer sheets.
Fan cards (/fan) — collectible-style constructor cards with team
accent colors, flip-to-recap, and a browser-local “my team” pick. Unofficial
fan surface; not licensed merch. Brand rules in docs/BRAND.md allow
constructor accents on fan surfaces.
News (/news) — curated links to real F1 reporting, filterable by
category. Titles, sources, and this app's own one-line framing only —
never reproduced article text, per docs/BRAND.md. A live-session
ticker at the top shows synthetic, schedule-derived markers ("Lights
out", "Chequered flag imminent") only while now genuinely falls inside
the fixed Sepang weekend window; otherwise it says so plainly rather
than faking a live feed this app has no telemetry source for. Also links
out to the official F1 Apple TV channel,
the Bahrain Grand Prix
event hub (the round Sepang is hosting in 2026), and
F1 The Movie
under Lifestyle & Culture. A small "Watch this weekend →" link on the
landing hero and /predict points at that Bahrain GP hub.
3D models — /models/sepang.glb (the landing page's "Orbit Sepang"
stage) and /models/car.glb (an animated lap on /circuit's traced
curve) are procedurally generated by scripts/generate_circuit_models.py.
sepang.glb is now a spline-swept ribbon projected from the circuit's
real 18-point apex + elevation data (the same centreline as
scripts/blender/sepang_circuit_scene.py and data/sepangCircuit.ts), so
the layout shape and its ~22 m elevation change are real (vertical
exaggerated ~6× for readability); car.glb stays box/cylinder primitives,
no scanned geometry. The car is deliberately unbranded (no livery, no sponsor
marks, no race number). Surfaced a real gotcha worth knowing if you ever
touch that script: trimesh's GLB exporter silently omits the NORMAL
accessor by default, even with vertex normals explicitly set on the
mesh — invisible (unlit black) once loaded, since three.js's
GLTFLoader doesn't compute missing normals itself. Fixed with
include_normals=True on .export(); see
frontend/public/models/README.md. An alternative, much higher-fidelity
Blender-based generator lives at scripts/blender/ — untested end to
end (no Blender in this dev environment), documented there as an
optional path rather than the current source of truth for the shipped
file.
Prediction accuracy (/accuracy) — a closed loop with no manual
input (backend/app/services/accuracy_service.py):
accuracy_predictions), from live /predict
traffic and from a scheduler that reads every session in the three
hours before it. Nothing can overwrite it once the session starts.rainfall samples), falling back to
Open-Meteo's modelled hourly rainfall if OpenF1 hasn't published 75
minutes after the flag. For the race, the winner's first stop comes
from Jolpica's pit-stop table. Stored in accuracy_outcomes..github/workflows/accuracy-loop.yml runs both steps every 10 minutes
(backend/ml/accuracy_tick.py), and GET /api/accuracy/weekend runs
them too: it re-locks sessions inside the three-hour window at most every
10 minutes and resolves on read, so the page updates within a poll of the
data landing (it polls every 30s while a session is live or pending).
GitHub delays and drops scheduled runs under load, so the loop doesn't
depend on the cron: any page view, or an uptime pinger hitting that
endpoint, keeps it moving. POST /api/outcomes is now an operator-only correction behind
OUTCOMES_ADMIN_TOKEN. Schema: backend/supabase/migrations/. This
replaced the earlier MLflow-backed version, which failed outright once
its remote tracking server stopped answering; MLflow still logs runs and
powers the /predict confidence trend when it's configured.
A Vue 3 port of this board lives in demos/vue-accuracy-board/
(open in StackBlitz).
Real telemetry (/telemetry, and /circuit's "real lap pacing"
toggle) — speed, throttle, brake, RPM, gear, and DRS from
OpenF1 (free, keyless, historical-only; see
docs/BRAND.md), replaying the same real 2026 Dutch GP at Zandvoort
already cited for the driver/team recaps. /circuit doesn't plot the
real car's actual coordinates — this app's track shape is a stylized
fictional trace, and the real corners don't line up with it — instead
it paces movement along the existing traced curve by the real lap's
distance-weighted speed profile (lib/telemetry.ts's
buildDistanceProgress), so a real braking zone visibly slows the
fictional car at whatever point in its own lap that fraction of
distance falls, even though the corner positions are unrelated. Backend
proxies OpenF1 (backend/app/services/telemetry_service.py) rather
than calling it from the browser, matching the existing Open-Meteo
pattern; the date-range filter needed a hand-built query string, since
OpenF1's date>/date< syntax glues the operator directly onto the
field name rather than using key=value.
Verification gap, stated plainly: api.openf1.org is blocked by
this dev sandbox's own network egress policy, so this integration was
built and unit-tested against a mocked HTTP transport
(backend/tests/test_telemetry_service.py) — confirming this repo's
own request-building and response-parsing logic, not that OpenF1's real
API actually matches the field names/shapes assumed here. The most
likely failure mode if something's off: the target session not
existing yet in OpenF1's archive, which the backend turns into a clean
404/502 rather than a crash (confirmed against the real, blocked host
from this sandbox — it fails exactly that way). Test this against the
live API somewhere with ordinary internet access (a normal dev machine
or the deployed Vercel function both qualify) before relying on it.
Design tokens, voice/tone rules, and component patterns are documented in
docs/BRAND.md — written against what's actually in
frontend/tailwind.config.ts, kept in sync rather than aspirational.
eslint.config.mjs imports eslint-config-next/core-web-vitals without
the .js extension, so next build skips linting with a resolution
error.og:image scenario card is rendered from a shared link's query params,
not from a fresh backend read — a shared card reflects the numbers captured
at share time (which is the intent, but worth knowing).User accounts, and a full 15-corner interactive circuit map.
cd frontend && npm install && npm run devcd backend && python -m venv venv && venv\Scripts\activate (Windows) then
pip install -r requirements.txt && uvicorn app.main:app --reload --host 0.0.0.0 --port 8000frontend/.env.example → frontend/.env.local and backend/.env.example → backend/.envcd backend && pip install -r requirements-dev.txt && pytest
Both frontend and backend deploy as separate Vercel projects from this one repo — no other platform involved.
Frontend (Next.js)
frontend/, framework preset
Next.js (auto-detected).NEXT_PUBLIC_API_URL = the backend project's deployed URL with an
/api suffix (e.g. https://jalur-apexgp-backend.vercel.app/api) — see
below for why.Backend (FastAPI, as a Vercel Python serverless function)
A second Vercel project, root directory backend/, framework preset
fastapi (auto-detected — confirmed via the Vercel API on this project).
backend/api/index.py exposes the existing FastAPI app as the ASGI
entrypoint Vercel's Python runtime wraps; pyproject.toml's
[tool.vercel] entrypoint pins it explicitly so Vercel's auto-detection
doesn't also pick app/main.py as a second candidate entrypoint (it
otherwise would — that scan independently checks app/ and src/
subdirectories for recognized filenames, and this repo's app/main.py
matches by coincidence of Python package naming, not because it's meant
to be its own function).
Routes are mounted under /api (/api/predict, /api/health) — this
matches Vercel's own zero-config FastAPI convention, which routes
/api/* straight to the function with no rewrite needed. An earlier
version of this project used a catch-all vercel.json rewrite instead;
that broke every route in production. Vercel's own build log explained
why in plain text: "Internal rewrites in backend framework projects now
route requests using the rewritten destination path" — meaning the app
received every request as literally /api/index, matching none of its
real routes, 404ing universally. Confirmed live against the deployed
backend (every path, including /, returned an identical FastAPI-shaped
404) before switching to the prefix approach and removing vercel.json
entirely.
requirements.txt uses mlflow-skinny rather than full mlflow — the
full package pulls pandas/pyarrow/sklearn and comfortably risks Vercel's
function size limit; skinny is client-only (no local UI/server) and is
all mlflow_client.py actually uses. Verified combined runtime deps
(fastapi + uvicorn + mlflow-skinny + the rest) come to ~130MB
uncompressed, well under the 250MB Hobby-tier limit.
MLflow needs a durable tracking backend — a Vercel function's
filesystem doesn't persist between invocations, so the local-dev default
(file:./ml/mlruns) silently breaks get_confidence_trend in
production (same-day trend needs multiple runs to actually persist
across requests). mlflow-skinny only supports file, http(s), and
databricks/databricks-uc tracking URI schemes — notably not a bare
SQL/Postgres URI, which needs the full package. The practical
free option is
's
hosted MLflow tracking (a REST backend, not a second thing you host):
set , ,
, and to an absolute
workspace path (Databricks requires this — see
).
Daily MLflow log — .github/workflows/daily-prediction-log.yml
(06:00 UTC) needs the same MLFLOW_TRACKING_URI /
MLFLOW_EXPERIMENT_NAME / DATABRICKS_HOST / DATABRICKS_TOKEN as repo
secrets (Settings → Secrets and variables → Actions), pointed at the same
Databricks workspace as the deployed backend — otherwise this cron logs
into its own throwaway CI-runner filesystem and contributes nothing to
same-day trend history, which is what it was doing before this was wired
up properly.
Independent, unofficial project. Not affiliated with Formula 1, the FIA, Bahrain International Circuit, or Sepang International Circuit.
mlflowMLFLOW_TRACKING_URI=databricksDATABRICKS_HOSTDATABRICKS_TOKENMLFLOW_EXPERIMENT_NAMEbackend/.env.exampleEnv: FRONTEND_ORIGIN (the frontend project's deployed URL, for CORS),
plus the MLflow vars above.
SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY back picks and the accuracy
loop. Enable them for Preview as well as Production: preview deploys
then read the real leaderboard and accuracy board, but every write
(submitting picks, locking predictions, recording outcomes) is refused
with a plain 403, because Vercel marks them VERCEL_ENV=preview. Set
SUPABASE_READ_ONLY=false on Preview only if it points at a separate
staging project.
ANTHROPIC_API_KEY powers the race-engineer assistant (the "Ask the
engineer" sidebar). It is optional: with the key unset the chat route
answers 503 with a plain "not configured" message that the panel shows
as-is, and nothing else in the app is affected. CHAT_MODEL optionally
pins a different model; the default is claude-opus-5.
Chat spend guardrails (backend/app/services/chat_guard.py). The
route is public, so cheap checks run before any token is spent:
CHAT_EXTRA_ORIGINS (comma-separated) allows more, e.g. a preview URL;CHAT_RATE_PER_MINUTE (default 6)
and CHAT_RATE_PER_DAY (60), plus a per-instance ceiling
CHAT_GLOBAL_PER_MINUTE (120), answering 429 with Retry-After;The limits are in-memory, so they hold per serverless instance. For a
hard ceiling across instances, add a Vercel Firewall rate-limit rule on
/api/chat and set a monthly spend limit on the Anthropic key.
A wedged or misconfigured tracking backend can't stall /predict itself —
mlflow_client.py bounds every MLflow call to 4s in a daemon thread.
Verified this against a genuinely unreachable host: without it,
mlflow.set_experiment() blocks indefinitely inside Databricks SDK's own
auth resolution, before any HTTP call — MLFLOW_HTTP_REQUEST_TIMEOUT
doesn't reach that stall, so a code-level bound was the only reliable
fix. Worth knowing if you ever touch that file: don't swap the daemon
thread for ThreadPoolExecutor without checking — its workers are
non-daemon, so shutdown(wait=False) stops the call from blocking but
a genuinely stuck one still keeps the whole process alive afterward.