# Path A — Fitbit Sense 2 Live Workout Companion: Architecture

## Target

**Fitbit Sense 2 (`rhea`)** running a private Gallery-distributed companion app
that **owns live workout state** during the session and **syncs the completed
workout to Hevy** via a lightweight backend.

## Architecture Diagram

```
┌──────────────────────────────┐
│        Hevy Cloud API        │
│  POST /v1/workouts (final)   │
│  GET  /v1/routines (plan)    │
└────────────▲──────┬──────────┘
             │      │
    ┌────────┴──────▼──────────────────────────────┐
    │              Pipo Backend                    │
    │                                              │
    │  • GET  /api/workout-plan/:userId            │
    │  • POST /api/live-workout/events             │
    │  • POST /api/workout/complete                │
    │  • GET  /api/workout/:id/status              │
    │                                              │
    │  State: active workout session, exercise     │
    │  pointer, set queue, RIR log, Hevy mapper    │
    └────────────▲──────┬──────────────────────────┘
                 │      │ HTTPS (fetch)
    ┌────────────┴──────▼──────────────────────────┐
    │       Fitbit Companion (phone-side)          │
    │                                              │
    │  • Bridge between watch ↔ backend            │
    │  • fetch() to API over phone network         │
    │  • Caches plan locally for offline start     │
    │  • Message relay with dedup                  │
    └────────────▲──────┬──────────────────────────┘
                 │      │ peerSocket / messaging API
    ┌────────────┴──────▼──────────────────────────┐
    │       Fitbit Device App (Sense 2, rhea)      │
    │                                              │
    │  ┌──────────────────────────────────────┐    │
    │  │         Live Workout State FSM       │    │
    │  │  IDLE → LOADING → ACTIVE → RESTING   │    │
    │  │     → DONE → SYNCING → COMPLETE       │    │
    │  └──────────────────────────────────────┘    │
    │                                              │
    │  UI: exercise name, set N/M, target weight,   │
    │  target reps, target RIR, +/- buttons,        │
    │  set-done button, rest timer, vibration       │
    └──────────────────────────────────────────────┘
```

## Component Breakdown

### 1. Device App (`app/`)

Runs on Sense 2. Responsible for:

- **Rendering** current exercise, set, targets, and controls.
- **Managing live workout state** as a finite state machine (local source of truth
  during the workout; backend mirrors it).
- **Capturing** user input: reps, weight, RIR, skip, next/prev, finish.
- **Running rest timer** with vibration alerts.
- **Sending events** to companion via `peerSocket`.
- **Offline-tolerant**: if companion/backend is unreachable, queues events
  locally until reconnect.

### 2. Companion App (`companion/`)

Runs inside the Fitbit phone app (Android). Responsible for:

- **Backend sync**: `fetch()` to Pipo backend over HTTPS.
- **Plan loading**: fetches today's Hevy routine on startup.
- **Event forwarding**: relays watch events to backend, backend confirmations
  to watch.
- **Local caching**: stores plan + partial session in `localStorage`.

### 3. Backend API (sketch in `backend-api-sketch.ts`)

Lightweight REST API. Production choice: Fastify (TypeScript, low overhead).
Deployed alongside existing Pipo services on Ubuntu VPS (`/api/` prefix).

- Authenticates via Hevy API key (user-linked).
- Endpoints documented in the sketch file.
- Maps internal state model ↔ Hevy API payload format.

### 4. Hevy API Integration

Only two operations touch Hevy during the workout flow:

- **Start**: `GET /v1/routines/{activeRoutineId}` → extract exercises, sets,
  targets.
- **Finish**: `POST /v1/workouts` with full exercise/set/RPE payload.

No live endpoint exists, so the backend accumulates state during the workout.

## State Model

See `app/state-model.ts` for full TypeScript definition.

Core state machine:

```
                  ┌──────────┐
                  │   IDLE   │  no workout loaded
                  └─────┬────┘
                        │ loadPlan()
                  ┌─────▼────┐
                  │ LOADING  │  fetching routine from backend
                  └─────┬────┘
                        │ planReady
                  ┌─────▼────┐
               ┌──│  ACTIVE  │  showing exercise, awaiting set input
               │  └─────┬────┘
               │        │ logSet() / skipExercise()
               │  ┌─────▼────┐
               │  │ RESTING  │  rest timer running
               │  └─────┬────┘
               │        │ restDone / skipRest
               │        │
               │  (loop until last set of last exercise)
               │
               └────────┘
                        │ finishWorkout()
                  ┌─────▼────┐
                  │   DONE   │  local save, prepare payload
                  └─────┬────┘
                        │ syncToHevy()
                  ┌─────▼────┐
                  │ SYNCING  │  POSTing to backend → Hevy
                  └─────┬────┘
                        │ syncSuccess / syncError(retry)
                  ┌─────▼────┐
                  │ COMPLETE │  finished + synced
                  └──────────┘
```

### Offline sub-state

If companion/backend is unreachable during ACTIVE/RESTING:
- Events queue locally in `pendingEvents[]`.
- UI shows ⚠ offline indicator.
- On reconnect, flush queue; backend applies events in order.
- If disconnect persists through workout end → DONE stores session locally;
  user can manually sync later.

## Data Flow: Set Logging (Happy Path)

```
1. User taps "Set Done" on watch
2. Watch FSM: ACTIVE → RESTING, timer starts
3. Watch → companion (peerSocket): { type: "SET_COMPLETE", exerciseIndex, setIndex, reps, weightKg, rir }
4. Companion → backend (fetch POST): /api/live-workout/events
5. Backend updates in-memory session, returns { ok: true, nextSet: {...} }
6. Backend → companion: { confirmation }
7. Companion → watch: { type: "SET_ACK", serverConfirmed: true }
8. Timer expires → vibration → watch FSM: RESTING → ACTIVE (next set)
```

## Package Choices

See `package-choices.md` for full rationale.

Quick summary:
- `@fitbit/sdk@7.2.0-pre.0` — targets `rhea` (Sense 2).
- `fitbit-sdk-build-targets` — unofficial build target definitions.
- TypeScript 5.x — Fitbit SDK standard.
- Backend: Fastify + TypeScript (matches Pipo stack).
- State: simple in-memory FSM (no Redux — overkill for 8 states).

## Distribution Path

Path A = **Private Gallery link** (GAM upload):

1. `npx fitbit-build` produces `.fba` for `rhea`.
2. Upload to Gallery App Manager as private listing.
3. User installs via private link in Fitbit mobile app.
4. No USB/DevBridge needed for end user.

Risk: GAM is flaky; if private upload fails, fallback to Path B (DevBridge sideload).
