Skip to main content

Athlora

Run the whole season from one place.

Athlora is a web app for athletics (track & field) coaches to manage a roster, plan competitions and training, log results live, derive statistics and PBs/SBs, and collaborate around a whole meet season.

Product scope​

Athlora supports a focused athletics-meet catalogue: 100m, 200m, 400m, 800m, 1500m, 100m hurdles, 400m hurdles, 4 x 100m relay, high jump, long jump, triple jump, javelin, discus, and shot put. The shipped release also covers shared fixtures, public statistics, schedules, reports, leaderboards, offline logging, club branding and the console assistant. Multi-events and automated season scheduling remain future work.

Monorepo layout​

/frontend React + Vite + TypeScript SPA (design tokens in src/styles/tokens.css)
/backend Express + TypeScript REST API (PostgreSQL, Auth0)
/docs This Docusaurus site
/e2e Playwright end-to-end tests

Design mockups live in the repository: SDP-Landing.html and SDP-Coach-Console.html at the repo root, plus the approved premium coach-console redesign 00002803-Athlora_Premium_Dashboard.html (dark aurora default with a light "Aurora Mist/Ice" variant) archived under docs/docs/sprints/sprint-1/screenshots/. All carry the placeholder brand "SDP" — the real brand everywhere is Athlora.

Key decisions​

  • Non-monolithic architecture: React frontend and Express API are separate services talking over HTTP/JSON. No fused framework (Next.js/SvelteKit) — this is a hard project requirement.
  • Timeline-first data model: everything an athlete does during an event is captured as an append-only timeline_entries log; results are derived from it, with a manual override for corrections.
  • Safe distributed writes: UUID primary keys and soft deletes everywhere, so offline logging and multi-device merge stay consistent.
  • Design tokens: the approved mockups are encoded once in frontend/src/styles/tokens.css; no hard-coded colours in components.

Current implementation​

The monorepo is scaffolded, committed, and all automated checks pass locally. What is done today (release 0.5.0):

  • Frontend shell and roster — Vite + React + TypeScript (strict), design tokens from the mockups, shared components (Button, Input, Select, Card, Badge, Modal, Toast, EmptyState), feature folders, a typed API client with structured errors, and Auth0 Universal Login integration that synchronizes the application user before authenticated content renders. The roster has active/inactive/archived filters, state counts and badges, reversible lifecycle actions, archived-profile read-only behavior, and complete loading/error/empty feedback.
  • Landing page — a lazy React Three Fiber stage now opens in a first-person Athlora side-stadium tunnel: dark architectural panels, alternating cyan/ice ribs, branded wall treatments, a shader-lit reflective floor, and a bright nearby stadium portal lead directly into the existing athletics track. Scroll drives the tunnel exit, lane approach, brief first-person run, restrained head bob/FOV acceleration, and a single-camera rise/sweep that arrives at the original track-camera transform without a cut. The environment stays deliberately open after the portal (infield and floodlights, without stands that could obstruct the camera), reuses the existing track, uses no new GLTF asset, pauses while hidden, and has compact/reduced-motion rendering profiles.
  • Backend shell — Express + TypeScript, /api/v1 resource routers, standard error shape, Auth0 JWT protection that resolves every resource request to a typed application-user context, Neon PostgreSQL with checksum-tracked migrations, and pure result-derivation services.
  • 100m data/API contract — the first implemented discipline is defined in docs/api-reference/contract, encoded in the aligned backend/frontend TypeScript domain types, and backed by the forward-only migration 0002_contract_100m.sql. It is a current contract, not the product limit: the database keeps a discipline/unit foundation for the later track and field rollout. The current result model distinguishes no result, a valid finish, DQ, DNF and DNS.
  • Ownership foundation — verified Auth0 subjects resolve to users.id and role before protected resource handlers run. Unsynchronized identities receive a structured recovery error, while reusable athlete/event/timeline/participant/result checks scope access to the current user and return the same generic not-found response for missing and cross-coach resources.
  • Account lifecycle — public sign-up/password-help controls use Auth0 Universal Login and the reachable Account console supports provider-aware password management, sign-out and typed-confirmation permanent deletion. The backend uses a least-privilege Auth0 Management API client, durable deletion tombstones, transactional workspace purge and scheduled idempotent reconciliation so stale tokens and partial failures cannot restore access.
  • Athlete lifecycle — the /api/v1/athletes routes provide workspace-authorized active/inactive/archived transitions with current actor/timestamp metadata and a durable transition audit. Existing archive/restore actions remain available; archived athletes are read-only, while inactive and archived athletes cannot receive new event assignments. Status changes preserve history and flag each existing event assignment for independent coach acknowledgment.
  • Event CRUD & lifecycle — the /api/v1/events routes are live against PostgreSQL: coach-scoped list with type/status/date-range filters and stable ordering, create, detail, full replacement with forward-only status transitions (cancelled is terminal), and cancellation-as-delete (DELETE sets status = 'cancelled', preserving timeline entries and results). POST /events/:id/archive and /unarchive soft-hide or restore an event through archivedAt without changing its status: archived events leave the default list, dashboard upcoming counts, reminders, and the public schedule, appear only under the status=archived filter, and reject participant/fixture mutations with 409 EVENT_ARCHIVED (archiving an in_progress event returns 409 EVENT_IN_PROGRESS). The same event lifecycle underpins every catalogue discipline session. Timeline routes reject logging against any event that is not in_progress (409 EVENT_NOT_IN_PROGRESS).
  • Event management UI — the coach console event view now consumes the typed event API with responsive list/calendar views, date/type/status filters, async and empty states, strict create/edit forms, detail participant counts, and confirmed start/complete/history-preserving cancel actions. An Archived option in the status filter is the only place archived events appear, each card and detail shows an Archived badge, and hosts archive or restore an event with a single click. API-wrapper and RTL tests cover filters, payloads, validation, details and lifecycle failures.
  • Venue search and map preview — authenticated GET /venues/search?q= proxies an explicit (not autocomplete) Nominatim lookup through strict validation, an identifiable server User-Agent, timeout/error mapping, a five-minute memory cache, and one-second public-provider throttle. Coaches may select a keyboard-accessible result or manually enter/adjust the persisted location and coordinates; event detail shows an attributed responsive read-only OSM preview, saved-coordinate fallback, and external map link. Weather continues to use those saved coordinates.
  • Weather — current conditions and event-day forecasts use GraySky through a shared ten-minute server cache. The live keyless endpoint returns a US-customary forecast envelope, which the backend validates and normalizes to nullable Celsius/mm/h/km/h metrics for the existing console/card visuals. Forecasts cover up to ten days; no hourly data is generated by Athlora. Provider errors remain isolated.
  • Event athlete assignments — authenticated event routes can list assigned athletes with logger-ready summaries, assign active owned athletes, idempotently update RSVP status, acknowledge per-athlete lifecycle review items, and remove assignments without deleting timeline/results history. Duplicate, inactive, and archived new assignments return explicit conflicts; all ownership failures remain non-enumerating.
  • Event assignment UI — event detail lists assigned athletes and RSVP state, keeps archived historical participants visible, loads active roster candidates, assigns athletes, replaces RSVP status, and confirms relationship removal with preserved-history messaging. Independent loading/retry states and mutation focus/locking behavior are covered by API-wrapper and RTL tests.
  • Event results and corrections UI — event detail and the live logger share an authoritative outcome board for the current 100m contract. Competition finishers are ordered by effective time while preserving backend tied placings, training omits misleading places, penalties/incidents and partial results remain distinct, and PB/SB plus archived history stay visible. The same presentation model will expand with discipline-specific ranking and measurement rules.
  • Live timeline API — authenticated coaches can read the active log, create normalized 100m entries, correct finish/incident/note content with optimistic expectedVersion checks, and undo through versioned tombstones. Stale requests conflict instead of overwriting newer state, exact repeated undo is a no-op, and tombstones are hidden from normal timeline/results views. Mutations enforce ownership, parent IDs and lifecycle under transaction locks while atomically refreshing outcomes, placings and PB/SB flags.
  • Track-side live logger — the mobile-first logger records 100m decimal finishes and incidents through serialized controls for the legacy timeline slice, while whole-meet logging handles timed, measured, vertical, relay, guest, and incident observations per catalogue session. Its versioned entries, corrections, undo, lifecycle guards, and responsive controls are the shared foundation for every discipline interface.
  • Statistics and dashboard aggregates — GET /api/v1/athletes/:id/statistics returns owner-scoped PB, calendar-year SB, effective-result counts, latest result and separate recent competition/training history. GET /api/v1/dashboard/summary returns one stable summary/live shape with deterministic active-event selection, progress and latest entries, active/inactive/archived roster counts, pending lifecycle-review count, active roster snapshot, scheduled upcoming events, recent results and PBs. A personal or season best is derived from one shared predicate, COMPLETED_COMPETITION_FILTER in disciplineStatistics.ts, so every surface that reports a PB or SB — roster discipline PBs, the athlete statistics detail, comparison rows, the public statistics and report, the leaderboard, analytics, vertical PB/SB and the live is_pb/is_sb flags — counts only results from an event whose status is completed and whose type is competition, direction-aware (fastest time for lower disciplines, highest mark for higher); training sessions, cancelled events and events still in progress never become a best mark. The row set behind those queries still counts every non-cancelled event, so result counts, latest results and averages keep in-progress results, while each best-mark aggregate reads a counts_for_best flag (or the shared predicate directly) and stays empty until a completed competition supplies a mark. Cancelled events do not score, archived athletes remain named in history but not in the active roster, and empty accounts receive zero counts and empty arrays. Progression charts obey the same rule: progression.ts, disciplineProgression.ts, getPublicAthleteProgression and both comparison progression arrays restrict every plotted point (and the allTimePb, personalBest, totalResults and comparison pb summaries that come with them) to that completed-competition set, so a chart stays empty while a meet is still running and fills in as soon as the event is completed, while comparison rows keep their non-cancelled result counts, averages and improvement on the full row set and filter only the points they plot through counts_for_best. The performance log behind GET /api/v1/athletes/:id/statistics now covers every discipline the athlete has results in: its history query dropped the r.discipline = '100m' predicate (and the $3 discipline parameter) so recentResults.competitions and recentResults.training return cross-discipline rows, and getDisciplineProgression filters through its $3 discipline join instead of a hard-coded r.discipline = '100m', so a raw results row in any discipline plots on that discipline's chart. The PB/SB/resultCounts block on the same statistics endpoint stays the 100m-scoped AthleteStatistics block, which no surface displays — every PB the UI shows comes from the per-discipline statistics endpoints.
  • Athlete performance detail UI — every real roster athlete opens a responsive profile and cross-discipline history view with editable shared profile fields, DOB/current age, and recent competition/training outcomes, with PB and calendar-year SB read from the per-discipline performance summary, while profile and statistics failures retry independently. The performance log is a four-column table (Date / Event / Type / Result) split into one tab per discipline assigned to the athlete, plus any further discipline that appears in their results: there is no combined "All disciplines" tab and no Competitions/Training tab strip — a Type select narrows the same table to competitions or training instead. Every assigned discipline keeps its tab, so a discipline with no results yet simply shows "No results yet.", and the log now loads multi-discipline history at all: the row mapper used to reject any discipline code that was not 100m and any unit that was not seconds, which made the whole performance log fail for athletes with results in other disciplines. Each Result cell carries the effective mark, the outcome itself for DQ, DNF or DNS, or a plain "No result" when nothing was recorded; those non-scoring outcomes stay out of the statistics and out of the progression charts, while the override and non-scoring audit notes are no longer printed under results and Personal best / Season best tags are gone from the rows — best marks belong to the discipline summary and the progression chart. Cancelled rows keep their text note. The view includes an all-time progression chart with PB milestones and chart/table toggle.
  • API-backed coach dashboard — the authenticated entry point now consumes the dashboard aggregate directly. Active events receive a dominant progress/latest-entry panel and an exact resume-logging action; otherwise coaches see factual roster and upcoming-event summaries with onboarding states and direct athlete/event navigation. Returning after event lifecycle changes refetches the aggregate so live and summary modes stay current.
  • Premium console redesign — the coach console shell and every authenticated view now mirror the approved premium mockup (docs/docs/sprints/sprint-1/screenshots/00002803-Athlora_Premium_Dashboard.html): a dark aurora shell (light "Aurora Mist/Ice" toggle) with sidebar brand/nav/readiness, topbar clock, a live weather readout (device geolocation with timezone-city fallback, proxied through GET /api/v1/weather/current), weather effects toggle and animated scene, plus mockup-matched Dashboard, Athletes, Events, Live Logger and Account views. All console colors live in one --console-* token namespace in the shell (theme-aware light overrides included); the Dashboard hero, metric row and roster/upcoming panels render real aggregate data — no fabricated figures. Weather endpoint contract is §4.4 in docs/api-reference/contract.
  • Fitness and injury mapping UI — Roster cards and athlete performance views first show a lightweight, accessible SVG summary of active injury count, highest severity and mapped body regions without loading Three.js. Each summary links to the shell-contained Fitness & Injury Map sub-view for progressive anatomical selections, persistent records, active-injury resolution, and history. The on-demand viewer uses the supplied static anatomical mesh on a dark medical stage in both themes, automatic framing, rotate/zoom/reset controls, and topology-bound surface heat maps rather than floating markers.
  • Backend deployment — the Render service is live at https://athlora-deploy.onrender.com and its /health check is verified.
  • Frontend deployment — the Vercel SPA is live at https://athlora-deploy.vercel.app with production Auth0 sign-in verified.
  • End-to-end suite — a deterministic Playwright run (desktop and mobile Chromium) proves the product end to end against a dedicated local PostgreSQL: authenticated roster → catalogue event creation → athlete assignment → live logging of finishes, incidents and session results → timeline correction and undo → result overrides and clearing → event completion → statistics, PBs/SBs and the returning dashboard summary, plus the unauthenticated public logger, statistics, schedule and report surfaces. The suite signs in through real Auth0 Universal Login once (auth-setup project), keeps an unauthenticated smoke project, seeds unique per-project data so desktop and mobile runs never collide, and runs an axe accessibility audit across key coach views.
  • Cross-coach authorization integration test — a TEST_DATABASE_URL-gated backend suite proves every owned resource (athletes, events, participants, timeline entries, result overrides and athlete statistics) is scoped to the owning coach with the generic non-enumerating 404, and that list endpoints never leak foreign rows.
  • Workspace tenancy — workspaces and memberships now form the shared authorization boundary. Existing records migrate losslessly into default UTC workspaces; the console selects an accessible workspace and scopes every request, aggregate and local view reset through X-Workspace-Id. Creator and result actors remain durable attribution, so an account departure removes memberships without destroying shared history.
  • Club onboarding — Clubs are the product-facing organization layer over the existing authorization boundary. Every existing workspace backfills to one Club; new users create a Club as coach or search all Clubs and request access. Club coaches review pending requests and choose coach or assistant access.
  • Offline-first PWA — the app is installable as a Progressive Web App with a service worker that caches the app shell and API responses. An offline queue (IndexedDB via Dexie) stores create/edit/undo actions when the network is unavailable, with a designated offline logger per event. On reconnect, the queue drains deterministically through a batch sync endpoint with idempotent action processing and optimistic version conflict detection.
  • Global Athlora assistant and discipline analytics — the fixed console assistant uses Google Gemini's BidiGenerateContentConstrained API for text and voice, catalogue-aware athlete/discipline analysis, opt-in weather, and direct PDF reports. Gemini can only prepare a validated local athlete draft; a coach must explicitly confirm it before the existing athlete API is called. Analytics normalize legacy 100m and finalized generic-session results without copying data, preserve result sources/placings, rank by direction-aware PB only, and reuse cached models for Tell me/PDF actions.
  • Workspace switching — coaches can belong to multiple workspaces and switch between them via the sidebar dropdown. Every request, aggregate, and view reset scopes to the selected workspace through X-Workspace-Id.
  • Role enforcement — coach and assistant roles are enforced through requireOperationalAccess and requireCoach middleware. Assistants can create/edit athletes and log events; coaches manage members, join requests, participant rosters, and fixture withdrawals. The final coach cannot be demoted or removed.
  • Athlete lifecycle — active/inactive/archived states with transition audit, status reviews, and read-only archived profiles.
  • Injury and fitness mapping — persistent injury records with body region, severity, and resolution. 3D anatomy viewer using React Three Fiber with topology-bound surface heat maps.
  • Cross-workspace fixtures — host/guest workspace invitation flow with revision reacceptance and withdrawals. Hosts can invite registered clubs directly from both 100m and multi-discipline event details; accepted guests use the same discipline tabs but can create and register only their own active athletes. A session stays empty when a club has no athletes for that discipline.
  • Event helpers — invitation-based event helper grants with offline logger designation and transfer protocol.
  • Realtime — Socket.IO event subscriptions with Auth0 token verification and workspace-scoped authorization.
  • Event reminders — in-app event reminders with mute preferences and notification delivery.
  • Fixture notifications — notification bell with unread counts and mark-as-read for fixture lifecycle events.
  • Dashboard preferences API — per-user dashboard preferences (card order, visibility, saved-filter presets) remain stored per (user, workspace) at GET|PUT /api/v1/preferences. The dashboard simplification in release 0.5.0 removed the "Customize dashboard" dialog and the optional dashboard panels from the UI, so the coach overview now uses a fixed layout while the API contract remains available to clients.
  • Public logger links — shareable unauthenticated links managed from the event detail by the event's own club or an accepted fixture-guest coach (any other workspace gets the generic 404). Legacy 100m links retain their per-athlete timeline console; generic whole-meet links let officials select an active discipline session and safely record timed, field, vertical, relay, guest, and incident observations. Officials may correct or undo only their own session entries; notes, lifecycle actions, roster controls, and coach overrides remain authenticated-only.
  • Athlete and club comparison — pair and multi-athlete/club side-by-side metrics with per-discipline tabs, an interactive SVG chart and URL-persisted state, plus two independent coach publication toggles for public results and the public upcoming schedule.
  • Independent club publication flags — clubs.public_results_enabled and clubs.public_schedule_enabled are separate coach-controlled settings on GET|PUT /api/v1/clubs/publication (full-replacement body requires both booleans). Unauthenticated GET /api/v1/public/schedule/clubs and /clubs/:clubId expose upcoming meet metadata only for clubs that opted into schedule publication; results and schedule visibility never share a gate.
  • Public club schedule experience — unauthenticated /schedule (club index with explicit search) and /schedule/:clubId (upcoming meets with date, time, venue, and selected disciplines) render outside the Auth0 provider so they work without Auth0 variables. Landing navigation, the public stats header, and the stats club card link in; disabled and unknown clubs share one generic unavailable state.
  • Club branding — coach-managed description, WCAG-checked primary color, logo, and cover on GET|PUT /api/v1/clubs/branding plus multipart logo/cover replace/delete. Assets are sniffed (PNG/JPEG/WebP ≤5 MB) and stored in S3-compatible object storage; public DTOs include branding only when the matching publication flag is on. The Account page hosts the settings UI; ClubBadge applies branding across the console, fixture teams, public stats, and comparison surfaces with an initials fallback.
  • Athlete progression — chronological 100m result history with PB milestones and interactive chart.
  • Expanded E2E suite — 23 spec files covering workspace, roles, athlete lifecycle, injuries, event helpers, realtime, reminders, public logger, fixture notifications, fixtures, public schedule, public statistics report, comparison, offline logging, authorization, migration, accessibility, routing, analytics, vertical events, relay session logging, the core vertical flow, and the anonymous smoke test.
  • Multi-discipline relay foundation — the 4x100m catalogue session, shared athlete/guest/relay entrants with ordered legs, coach-only roster edits while the meet is scheduled, session-scoped timed logging with offline enqueue, coach-selected official entry (selected_entry_id), read-time team standings, athlete relay history that never writes PB/SB, and public session/team places on the Stats page using safe member summaries only. Every athlete in a relay session's pool — selected members and unchosen candidates — has a pending/attending/not-attending RSVP control with a session RSVP summary; a team can be named and added only when exactly four athletes are selected and all are attending, legs are numbered in selection order, and a declined selection asks for a replacement athlete, so pending pool RSVPs no longer block starting the meet. Relay results are recorded as per-athlete splits in the coach and public loggers (one Relay splits - athlete input and Make official control per leg, with official selection — relay legs and individual attempts alike — limited to athletes of the coach's own club, the host included, while session finalization stays host-only), each official split shows its own PB/SB marker against the athlete's relay-discipline history once the session is final (the summed team total is never a personal best, and a leg still counts after the team's result becomes DQ), the coach logger's standings and CSV export and the event's final results show each leg with the summed team total only once the session is final (the public logger shows each entrant's current result inline, with no standings table or CSV export), and a session cannot be finalized while any leg is missing an official split (409 RELAY_RESULTS_INCOMPLETE). Official relay splits also feed the surfaces athletes actually watch: each roster entry's discipline PBs, the per-discipline progression graph, and the athlete/club comparison rows and graphs (which now render value ticks and axis titles like the progression charts), while the public Stats page again renders published session/team rows for the selected club discipline — the public report and leaderboard list each finalized relay team's name, club, and summed result (team rows never match gender or age filters), public club statistics count team results for the relay discipline's row, and the public athlete cards show relay leg PBs. Retired catalogue codes (4x400m, hammer throw) are hidden from every preference read and their stale rows deleted by migration 0044_prune_unsupported_athlete_disciplines.sql.
  • Event discipline selection and roster tabs — Add Event opens directly to grouped, Athlora-themed discipline cards with loading/retry feedback; 100m is a normal selectable session alongside the other catalogue disciplines. Generic event rosters use keyboard-accessible session tabs and session-specific registrations. An athlete appears in a tab's themed picker only when their preferred disciplines include that tab's catalogue definition, and the backend rejects a mismatched registration. Existing 100m events retain their participant/RVSP controls inside a single 100m tab.
  • Quality gate — lint, typecheck, Vitest/RTL, Supertest, production builds, an informational Vitest V8 coverage report, the Playwright E2E suite (smoke + desktop/mobile full suite + axe audit), and the Docusaurus build are configured in CI. The Gitea coverage job prints a short Markdown summary; the e2e job provisions a local PostgreSQL cluster (apt + initdb on port 55432) and skips with a clear message until the Auth0/E2E credentials are configured as repository secrets.
  • Docs deployment — the Docusaurus site is live at https://athlora-deploy.pages.dev.

Keeping these docs current​

These pages are a living record that agents maintain as part of every task. If you are an agent working in this repo: the sections marked Current status / Current state and the check-status table in getting-started/scripts.md must be updated in the same session as the code they describe, documentation changes are committed with the feature (Conventional Commits, Assisted-by: footer), the docs build must pass before you finish, and this site's source-of-truth process docs must never be edited for progress. The full mandatory rules are in the build spec, Section 14.

Getting around​

AI declaration​

This document was created or updated with the assistance of OpenCode[openai/gpt-5.6-terra].