Skip to content

About

Phoenix Portal — full-stack web companion dashboard for Project Phoenix (Vitruvian Trainer community rescue project). React 19 + Vite 7 + Supabase (Postgres, Auth, Realtime, 22 Edge Functions) + Paddle billing.

Resources

Stars

3 stars

Watchers

0 watching

Forks

Latest commit

 

History

1,324 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Phoenix Portal

Beta React TypeScript Vite

Web companion dashboard for Project Phoenix, an open-source companion for Phoenix-compatible fitness machines. View workouts, build routines and training cycles, analyze biomechanics, and replay 50Hz session telemetry synced from the Kotlin Multiplatform mobile app.

Phoenix Portal Dashboard

Features

Dashboard & History

  • Real-time Dashboard — Workout stats, streak tracking, recovery score, goal progress rings
  • Workout History — Searchable/filterable workout log synced from mobile app
  • Session Detail — Deep dive into individual workouts with exercise breakdown

Analytics & Insights

  • Performance Analytics — Volume trends, PR tracking, muscle group distribution
  • Biomechanics Analysis — Velocity zones, ROM trends, asymmetry detection
  • Training Load Gauge — Acute:chronic workload ratio monitoring
  • Muscle Heatmap — Visual muscle group coverage
  • Force Curves — Per-cable forces and method-aware signed cable-work proxy power; unverified watts remain unavailable

Session Replay

  • Telemetry Playback — Rep-by-rep motion replay canvas; measured receipt timing is distinct from unverified legacy timing
  • Timeline Navigation — Scrub through workout timeline
  • Fatigue Summary — Per-set fatigue analysis
  • Quality Badges — Rep quality indicators

Power summaries accept PAIRED_CABLE_WORK_V1, UNAVAILABLE and LEGACY_UNKNOWN_V0. Only the first projects mean/peak watts; genuine zero and signed negative values are preserved. Historical collapsed curves retain known cable forces, but unknown position/velocity cannot establish phase or work. This is not muscle/body power or physical measurement validation.

Deploy 20261002100000_paired_rep_power_contract.sql with the coordinated sync handlers before new mobile uploads; regenerate types from the migrated local stack. Inferno raw-curve gating is unchanged. The performance review is at /analytics?tab=performance; software checks and synthetic localhost UI examples are not physical pilots.

Routine Builder

  • Visual Routine Editor — Drag-and-drop exercise ordering
  • Superset Support — Group exercises with rest period configuration
  • AMRAP & PR-scaling — Advanced workout modifiers
  • Community Sharing — Publish to routine library

Training Cycles

  • Multi-week Periodization — Build structured training blocks
  • Day-by-Day Planning — Assign routines to specific days
  • Cycle Activation — Single active cycle enforcement

Community Hub

  • Routine Library — Browse and clone community routines
  • Vote System — Upvote/downvote community content
  • Leaderboards — Compete on volume, PRs, streaks
  • Challenges — Time-limited competitions

Integrations

  • Strava — Import activities (pull-only)
  • Fitbit — Wearable import (coming soon; pending developer-program approval)
  • Garmin Connect — Webhook activity import (coming soon; pending Garmin developer program / GCPP approval)
  • Hevy — Workout history migration
  • Liftosaur — Program and log migration
  • Strong — CSV workout import
  • Apple Health / Google Health Connect — mobile-only cards (sync from the app, not portal OAuth)

Subscription Tiers

Single matrix matching src/lib/pricing.ts and src/lib/tierMatrix.ts (read by src/app/routes/index.tsx). Ember is not sold leaderboards.

Tier Monthly Annual Access
FREE $0 $0 Landing, pricing; signed-in users are a conversion funnel (dashboard upgrade wall), not a local-history SKU
EMBER $5 $49/yr Cloud sync, dashboard, history, session detail, goals, recovery
FLAME $15 $149/yr Analytics, community, routines, cycles, integrations (Strava/Hevy/Liftosaur), leaderboards, challenges, compare, session replay (rep-by-rep; force curves are INFERNO)
INFERNO $25 $249/yr Advanced biomechanics (force, VBT, ROM, SRA, form). Purchasable; inner INFERNO gates stay.

Route gates read their tiers from FEATURE_MIN_TIER in src/lib/tierMatrix.ts. Route gates are UX only; what the server enforces is:

  • EMBER (server-enforced): cloud sync (mobile-sync-push, mobile-sync-pull) and browser writes to workout sessions, records and local profiles (RLS).
  • FLAME (server-enforced): browser INSERT/UPDATE on shared routines/cycles, votes, comments, saved items, follows, challenge participation, integrations, the sync queue, and portal routine/cycle authoring (routines, routine_exercises, training_cycles, cycle_days) via RLS; the import_shared_routine / import_shared_cycle RPCs; initiate-oauth, the integration sync Edge Functions and leaderboards (compute-rankings). Deleting what you published or joined (comments, shares, votes, follows, saved items, challenge participation) carries no tier check in RLS, and every portal mutation that removes one of those rows is a real DELETE — including comment removal, which is a hard delete with a row check, not a soft-delete UPDATE. So a downgrade never blocks removal, at the API or in the mutations. The community and challenge screens are still FLAME-gated, so a downgraded user has no portal page to remove from (follow-up: a withdraw-only view for lapsed plans). Routines and cycles pushed from the mobile app stay EMBER (service-role push).
  • INFERNO (server-enforced): the force-curve and biomechanics data — rep_telemetry (and the telemetry_points view over it), vbt_assessments, session_phase_statistics, exercise_signatures — is readable only by an INFERNO subscriber (RLS, 20260920003800_inferno_read_policies.sql). A FLAME user reads zero rows from them, so session replay degrades to rep-by-rep playback built from rep_summaries and says "Force curves require Inferno". The GDPR export is unaffected: export-user-data reads with the service role, so any tier can still export every telemetry row they own.
  • Browser-only gates: analytics, compare and session replay itself are computed from data the user can already read, so their gates are UX only.

Fitbit and Garmin Connect stay comingSoon in the UI until developer-program approval. Do not treat README as a Connect un-block.

Tech Stack

Layer Technology
Build Vite 7
Framework React 19, TypeScript 5.7
Styling Tailwind CSS v4
Components shadcn/ui (33 component files, 20 Radix imports)
State Zustand 5 (client), TanStack Query 5 (server)
Visualization Recharts 3, @visx, ECharts 6
Animation Motion (formerly Framer Motion, reduced-motion)
Validation Zod 4
Backend Supabase (PostgreSQL, Auth, Realtime, Storage)
Edge Functions Deno (ls supabase/functions; each has a [functions.*] block in supabase/config.toml)
Payments Paddle (Merchant of Record)
Monitoring Sentry (cookie-consent-gated)
Testing Vitest 4, Playwright 1.58
Linting Biome 2.4

Development

# Install dependencies
npm install

# Start dev server (localhost:5173)
npm run dev

# Production build
npm run build

# Run tests
npm test

# Sync tests (mocked Edge Functions)
npm run test:sync

# Edge Function type-check and Deno handler tests
npm run check:edge-functions
npm run test:edge

# Type checking (scripts/typecheck.mjs runs tsc -p --noEmit on the app, node,
# test, and e2e tsconfigs; fails on errors not in typecheck-baseline.json)
npm run typecheck

# Re-record the pre-existing type errors after fixing some (review the diff)
npm run typecheck:baseline

# E2E tests
npm run test:e2e

# Regenerate Supabase types from the migrated local DB (what CI checks)
npm run supabase -- start
npm run supabase -- db reset --no-seed
npm run gen:types:local

# Database tests (pgTAP) against the local stack
npm run test:db

Symphony Orchestration

This repo includes WORKFLOW.md for OpenAI Symphony, which can poll Linear and launch isolated Codex implementation runs for active issues. Before running it, set LINEAR_API_KEY and confirm that tracker.project_slug in WORKFLOW.md (already set) points at your Linear project. See docs/runbooks/symphony.md for the full setup and operating checklist.

Environment Variables

Copy .env.example to .env.local and confirm every value in it — that file is the list, and it carries a comment per variable.

VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_ANON_KEY=your-anon-key
VITE_PADDLE_CLIENT_TOKEN=your-paddle-token
VITE_PADDLE_ENVIRONMENT=sandbox
VITE_SENTRY_DSN=
# ... Paddle price IDs for each tier

Only VITE_-prefixed values reach the browser bundle. Edge Function secrets (PADDLE_API_KEY, CRON_SECRET, SYNC_LWW_ENABLED, SUPABASE_PUBLIC_URL, APP_URL, the provider client secrets, …) are set in Supabase Dashboard → Edge Functions → Secrets. CLAUDE.md → "Environment Variables" explains what each one is for.

Mobile Sync Architecture

Bidirectional sync with Project Phoenix mobile app (Kotlin Multiplatform):

Mobile (SQLite) → POST mobile-sync-push → Supabase DB
                                            ↓
                              Broadcast sync_complete
                                            ↓
Portal ← useRealtimeSync hook ← Channel sync:{userId}
  • Push: Workouts → mobile-sync-push Edge upsert (realtime broadcast on private sync:{userId} invalidates portal cache)
  • Pull: Parity-based — the device sends the ids it already holds and the server returns the rest, plus rows changed since lastSync - 2 min. Cursor-based, 75 entities/page (max 300). There is no timestamp-only pull mode. rep_telemetry is not pulled — session replay restores telemetry in the portal only.
  • Conflict Resolution: client_updated_at is the last-write-wins key and updated_at is the server-owned pull cursor; they are not interchangeable. Sessions and routines are last-push-wins unless SYNC_LWW_ENABLED is set (a hosted, cold-start flag); training cycles always go through merge_training_cycles_from_push, which preserves portal-only configuration and refuses a stale structure. Routine deletes (deletedRoutineIds) are tombstoned, so a stale device cannot resurrect them. Cycle resurrection is stopped by clocked deletedCycles and the tombstone gate; legacy deletedCycleIds do not tombstone. See CLAUDE.md → "The mobile sync contract".

Phoenix Theme

Dark theme by default. Color tokens live in src/styles/theme.css (:root for dark, :root[data-theme='light'] for light):

Token Dark Usage
--background #06060a App background
--primary / --phoenix-ember #ff6b35 Primary accent
--destructive #ff5252 Alerts
--accent #f59e0b Highlights
--success #00e676 Success states

The light theme overrides those tokens: --background #fbfbfc, --primary #c2410c, --destructive #b91c1c, --accent #a16207, --success #047857. --phoenix-ember stays #ff6b35 in both themes.

Deployment

Hosted as a Cloudflare Worker with static assets (wrangler.toml: the phoenix-portal Worker serves dist/ with single-page-application fallback). Workers Builds runs npx wrangler deploy on every push to main; npm run deploy does the same by hand. Other static hosts are possible but not the supported path.

Related Projects

License

MIT


This project exists because a community of lifters wanted their training data and tools to keep working, no matter what.

About

Phoenix Portal — full-stack web companion dashboard for Project Phoenix (Vitruvian Trainer community rescue project). React 19 + Vite 7 + Supabase (Postgres, Auth, Realtime, 22 Edge Functions) + Paddle billing.

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages