openapi: 3.1.0
info:
  title: River Workout API
  version: 1.0.0
  description: API for reading and creating River Workout profile, exercises, workouts, workout sessions, and workout analytics.
servers:
  - url: https://workout.the-still-river.com
    description: Production
security:
  - bearerAuth: []
paths:
  /api/v1/profile:
    get:
      summary: Get the authenticated user's fitness profile
      operationId: getFitnessProfile
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Fitness profile
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FitnessProfile"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
  /api/v1/exercises:
    get:
      summary: Search visible exercises
      operationId: searchExercises
      security:
        - bearerAuth: []
      parameters:
        - name: q
          in: query
          required: false
          schema:
            type: string
        - name: category
          in: query
          required: false
          schema:
            type: string
        - name: primaryMuscleGroup
          in: query
          required: false
          schema:
            type: string
        - name: movementPattern
          in: query
          required: false
          schema:
            type: string
        - name: equipment
          in: query
          required: false
          schema:
            type: string
        - name: difficulty
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Exercise results
          content:
            application/json:
              schema:
                type: object
                properties:
                  exercises:
                    type: array
                    items:
                      $ref: "#/components/schemas/Exercise"
                required: [exercises]
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
  /api/v1/workouts:
    get:
      summary: List user's workouts
      operationId: listWorkouts
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Workout list
          content:
            application/json:
              schema:
                type: object
                properties:
                  workouts:
                    type: array
                    items:
                      $ref: "#/components/schemas/WorkoutSummary"
                required: [workouts]
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
    post:
      summary: Create a workout
      operationId: createWorkout
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WorkoutInput"
      responses:
        "201":
          description: Workout created
          content:
            application/json:
              schema:
                type: object
                properties:
                  workout:
                    $ref: "#/components/schemas/WorkoutDetail"
                required: [workout]
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
  /api/v1/workouts/{id}:
    get:
      summary: Get workout details
      operationId: getWorkout
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Workout detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  workout:
                    $ref: "#/components/schemas/WorkoutDetail"
                required: [workout]
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      summary: Update workout
      operationId: updateWorkout
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WorkoutInput"
      responses:
        "200":
          description: Workout updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  workout:
                    $ref: "#/components/schemas/WorkoutDetail"
                required: [workout]
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
  /api/v1/workout-plan:
    get:
      summary: Get the authenticated user's active workout plan
      operationId: getWorkoutPlan
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Active workout plan, or null when none exists
          content:
            application/json:
              schema:
                type: object
                properties:
                  plan:
                    anyOf:
                      - $ref: "#/components/schemas/WorkoutPlan"
                      - type: "null"
                required: [plan]
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
    put:
      summary: Create or replace the authenticated user's active workout plan
      operationId: saveWorkoutPlan
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WorkoutPlanInput"
      responses:
        "200":
          description: Saved workout plan
          content:
            application/json:
              schema:
                type: object
                properties:
                  plan:
                    $ref: "#/components/schemas/WorkoutPlan"
                required: [plan]
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
  /api/v1/workout-plan/advance:
    post:
      summary: Advance the active workout plan to the next cycle row
      operationId: advanceWorkoutPlan
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Advanced workout plan
          content:
            application/json:
              schema:
                type: object
                properties:
                  plan:
                    $ref: "#/components/schemas/WorkoutPlan"
                required: [plan]
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/workout-plan/reset:
    post:
      summary: Reset the active workout plan to day 1
      operationId: resetWorkoutPlan
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Reset workout plan
          content:
            application/json:
              schema:
                type: object
                properties:
                  plan:
                    $ref: "#/components/schemas/WorkoutPlan"
                required: [plan]
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/workout-sessions:
    get:
      summary: List past workout sessions
      operationId: listWorkoutSessions
      security:
        - bearerAuth: []
      parameters:
        - name: limit
          in: query
          required: false
          description: Number of sessions to return. Defaults to 20 and is capped at 100.
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: from
          in: query
          required: false
          description: Include sessions started at or after this ISO date.
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          required: false
          description: Include sessions started at or before this ISO date.
          schema:
            type: string
            format: date-time
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum: [completed, active, abandoned]
            default: completed
      responses:
        "200":
          description: Past workout sessions
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/WorkoutSessionSummary"
                required: [items]
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
  /api/v1/workout-sessions/{sessionId}:
    get:
      summary: Get workout session detail
      operationId: getWorkoutSession
      security:
        - bearerAuth: []
      parameters:
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Workout session detail
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkoutSessionDetail"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/analytics/exercises:
    get:
      summary: List exercises the user has performed
      operationId: listPerformedExercises
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Performed exercises sorted by most recent performance
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/PerformedExercise"
                required: [items]
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
  /api/v1/analytics/exercises/{exerciseId}/trend:
    get:
      summary: Get set-level progress trend for one exercise
      operationId: getExerciseTrend
      security:
        - bearerAuth: []
      parameters:
        - name: exerciseId
          in: path
          required: true
          schema:
            type: string
        - name: from
          in: query
          required: false
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          required: false
          schema:
            type: string
            format: date-time
        - name: metric
          in: query
          required: false
          schema:
            type: string
            enum: [weight, reps, volume, estimated1rm, duration, distance, pace]
      responses:
        "200":
          description: Exercise progress trend
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ExerciseTrend"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/analytics/summary:
    get:
      summary: Get compact training summary
      operationId: getAnalyticsSummary
      security:
        - bearerAuth: []
      parameters:
        - name: from
          in: query
          required: false
          description: Start of the summary range. Defaults to 30 days before the request.
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          required: false
          description: End of the summary range. Defaults to the request time.
          schema:
            type: string
            format: date-time
      responses:
        "200":
          description: Training summary
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AnalyticsSummary"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
  /api/v1/analytics/training-balance:
    get:
      summary: Get muscle group and movement pattern balance
      operationId: getTrainingBalance
      security:
        - bearerAuth: []
      parameters:
        - name: from
          in: query
          required: false
          description: Start of the balance range. Defaults to 7 days before the request.
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          required: false
          description: End of the balance range. Defaults to the request time.
          schema:
            type: string
            format: date-time
      responses:
        "200":
          description: Training balance
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TrainingBalance"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
  /api/v1/workout-sessions/retroactive:
    post:
      summary: Create a completed workout session from a past workout
      operationId: createRetroactiveWorkoutSession
      description: Requires the sessions:write scope.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RetroactiveWorkoutInput"
      responses:
        "201":
          description: Retroactive workout session created
          content:
            application/json:
              schema:
                type: object
                properties:
                  sessionId:
                    type: string
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
  responses:
    BadRequest:
      description: Invalid request
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    Unauthorized:
      description: Unauthorized
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    Forbidden:
      description: Missing required scope or forbidden
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    NotFound:
      description: Not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
  schemas:
    ErrorResponse:
      type: object
      properties:
        error:
          type: string
        issues:
          type: object
      required: [error]
    Error:
      $ref: "#/components/schemas/ErrorResponse"
    FitnessProfile:
      type: object
      properties:
        displayName:
          type: [string, "null"]
        dateOfBirth:
          type: [string, "null"]
          format: date-time
        sex:
          type: [string, "null"]
        heightCm:
          type: [number, "null"]
        weightKg:
          type: [number, "null"]
        targetWeightKg:
          type: [number, "null"]
        desiredWeightChangePerWeekKg:
          type: [number, "null"]
        activityLevel:
          type: [string, "null"]
        experienceLevel:
          type: [string, "null"]
        primaryGoal:
          type: [string, "null"]
        trainingDaysPerWeek:
          type: [integer, "null"]
        averageSessionMinutes:
          type: [integer, "null"]
        equipmentAccess:
          type: array
          items:
            type: string
        injuries:
          type: [string, "null"]
        limitations:
          type: [string, "null"]
        notes:
          type: [string, "null"]
    Exercise:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        slug:
          type: string
        category:
          type: string
        modality:
          type: [string, "null"]
        movementPattern:
          type: [string, "null"]
        primaryMuscleGroup:
          type: [string, "null"]
        secondaryMuscleGroups:
          type: array
          items:
            type: string
        equipment:
          type: array
          items:
            type: string
        difficulty:
          type: [string, "null"]
        isTimeBased:
          type: boolean
        isDistanceBased:
          type: boolean
        isRepsBased:
          type: boolean
        isWeightBased:
          type: boolean
    WorkoutSummary:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        description:
          type: [string, "null"]
        visibility:
          type: string
          enum: [private, shared_by_link, public]
        exerciseCount:
          type: integer
        setCount:
          type: integer
        updatedAt:
          type: string
          format: date-time
    WorkoutInput:
      type: object
      required: [name, exercises]
      properties:
        name:
          type: string
        description:
          type: string
        visibility:
          type: string
          enum: [private, shared_by_link, public]
          default: private
        exercises:
          type: array
          items:
            $ref: "#/components/schemas/WorkoutExerciseInput"
    WorkoutExerciseInput:
      type: object
      required: [exerciseId, orderIndex, targetSets]
      properties:
        exerciseId:
          type: string
        orderIndex:
          type: integer
        notes:
          type: string
        targetSets:
          type: array
          items:
            $ref: "#/components/schemas/WorkoutSetInput"
    WorkoutSetInput:
      type: object
      required: [setNumber]
      properties:
        setNumber:
          type: integer
        targetWeight:
          type: number
        targetReps:
          type: integer
        targetDurationSeconds:
          type: integer
        targetDistanceMeters:
          type: number
        targetRpe:
          type: number
        restSeconds:
          type: integer
        notes:
          type: string
    WorkoutDetail:
      allOf:
        - $ref: "#/components/schemas/WorkoutSummary"
        - type: object
          properties:
            exercises:
              type: array
              items:
                type: object
    WorkoutPlan:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        description:
          type: [string, "null"]
        isActive:
          type: boolean
        currentCycleIndex:
          type: integer
        lastAdvancedAt:
          type: [string, "null"]
          format: date-time
        lastStartedAt:
          type: [string, "null"]
          format: date-time
        rows:
          type: array
          items:
            $ref: "#/components/schemas/WorkoutPlanRow"
        currentRow:
          anyOf:
            - $ref: "#/components/schemas/WorkoutPlanRow"
            - type: "null"
        nextWorkoutRow:
          anyOf:
            - $ref: "#/components/schemas/WorkoutPlanRow"
            - type: "null"
    WorkoutPlanRow:
      type: object
      properties:
        id:
          type: string
        orderIndex:
          type: integer
        rowType:
          type: string
          enum: [workout, rest, skip]
        workoutTemplateId:
          type: [string, "null"]
        label:
          type: [string, "null"]
        notes:
          type: [string, "null"]
        workoutTemplate:
          anyOf:
            - $ref: "#/components/schemas/WorkoutTemplateSummary"
            - type: "null"
    WorkoutTemplateSummary:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        description:
          type: [string, "null"]
        isArchived:
          type: boolean
    WorkoutPlanInput:
      type: object
      required: [name, rows]
      properties:
        name:
          type: string
          maxLength: 100
        description:
          type: string
          maxLength: 1000
        currentCycleIndex:
          type: integer
          minimum: 0
        rows:
          type: array
          minItems: 1
          items:
            $ref: "#/components/schemas/WorkoutPlanRowInput"
    WorkoutPlanRowInput:
      type: object
      required: [orderIndex, rowType]
      properties:
        id:
          type: string
        orderIndex:
          type: integer
          minimum: 0
        rowType:
          type: string
          enum: [workout, rest, skip]
        workoutTemplateId:
          type: string
          description: Required when rowType is workout.
        label:
          type: string
          maxLength: 100
        notes:
          type: string
          maxLength: 1000
    WorkoutSessionSummary:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        startedAt:
          type: string
          format: date-time
        completedAt:
          type: [string, "null"]
          format: date-time
        durationSeconds:
          type: [integer, "null"]
        status:
          type: string
          enum: [active, completed, abandoned]
        exerciseCount:
          type: integer
        completedSetCount:
          type: integer
        totalVolume:
          type: [number, "null"]
        notes:
          type: [string, "null"]
        exercises:
          type: array
          items:
            $ref: "#/components/schemas/SessionExercise"
      required: [id, name, startedAt, completedAt, durationSeconds, status, exerciseCount, completedSetCount, totalVolume, notes, exercises]
    RetroactiveWorkoutInput:
      type: object
      required: [name, performedAt, exercises]
      properties:
        workoutTemplateId:
          type: string
        name:
          type: string
        description:
          type: string
        performedAt:
          type: string
          format: date-time
        startedAt:
          type: string
          format: date-time
        completedAt:
          type: string
          format: date-time
        durationSeconds:
          type: integer
          minimum: 0
        notes:
          type: string
          maxLength: 3000
        exercises:
          type: array
          items:
            $ref: "#/components/schemas/RetroactiveWorkoutExerciseInput"
    RetroactiveWorkoutExerciseInput:
      type: object
      required: [exerciseId, orderIndex, sets]
      properties:
        exerciseId:
          type: string
        orderIndex:
          type: integer
          minimum: 0
        nameSnapshot:
          type: string
        notes:
          type: string
          maxLength: 1000
        sets:
          type: array
          items:
            $ref: "#/components/schemas/RetroactiveWorkoutSetInput"
    RetroactiveWorkoutSetInput:
      type: object
      required: [setNumber]
      properties:
        setNumber:
          type: integer
          minimum: 1
        setType:
          type: string
          enum: [warmup, working, drop, failure, cooldown]
          default: working
        status:
          type: string
          enum: [completed, skipped]
          default: completed
        actualWeight:
          type: number
          minimum: 0
        actualReps:
          type: integer
          minimum: 0
        actualDurationSeconds:
          type: integer
          minimum: 0
        actualDistanceMeters:
          type: number
          minimum: 0
        actualRpe:
          type: number
          minimum: 1
          maximum: 10
        actualRestSeconds:
          type: integer
          minimum: 0
        notes:
          type: string
          maxLength: 1000
      required: [id, name, startedAt, completedAt, durationSeconds, status, exerciseCount, completedSetCount, totalVolume, notes]
    WorkoutSessionDetail:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        startedAt:
          type: string
          format: date-time
        completedAt:
          type: [string, "null"]
          format: date-time
        durationSeconds:
          type: [integer, "null"]
        status:
          type: string
          enum: [active, completed, abandoned]
        notes:
          type: [string, "null"]
        exercises:
          type: array
          items:
            $ref: "#/components/schemas/SessionExercise"
      required: [id, name, startedAt, completedAt, durationSeconds, status, notes, exercises]
    SessionExercise:
      type: object
      properties:
        id:
          type: string
        exerciseId:
          type: string
        name:
          type: string
        orderIndex:
          type: integer
        notes:
          type: [string, "null"]
        exercise:
          type: object
          properties:
            id:
              type: string
            name:
              type: string
            category:
              type: string
            primaryMuscleGroup:
              type: [string, "null"]
            movementPattern:
              type: [string, "null"]
            equipment:
              type: array
              items:
                type: string
          required: [id, name, category, primaryMuscleGroup, movementPattern, equipment]
        sets:
          type: array
          items:
            $ref: "#/components/schemas/SessionSet"
      required: [id, exerciseId, name, orderIndex, notes, exercise, sets]
    SessionSet:
      type: object
      properties:
        id:
          type: string
        setNumber:
          type: integer
        setType:
          type: string
        status:
          type: string
          enum: [planned, completed, skipped]
        isCompleted:
          type: boolean
        targetWeight:
          type: [number, "null"]
        targetReps:
          type: [integer, "null"]
        targetDurationSeconds:
          type: [integer, "null"]
        targetDistanceMeters:
          type: [number, "null"]
        targetRpe:
          type: [number, "null"]
        targetRestSeconds:
          type: [integer, "null"]
        actualWeight:
          type: [number, "null"]
        actualReps:
          type: [integer, "null"]
        actualDurationSeconds:
          type: [integer, "null"]
        actualDistanceMeters:
          type: [number, "null"]
        actualRpe:
          type: [number, "null"]
        actualRestSeconds:
          type: [integer, "null"]
        completedAt:
          type: [string, "null"]
          format: date-time
        skippedAt:
          type: [string, "null"]
          format: date-time
        notes:
          type: [string, "null"]
      required: [id, setNumber, setType, status, isCompleted, targetWeight, targetReps, targetDurationSeconds, targetDistanceMeters, targetRpe, targetRestSeconds, actualWeight, actualReps, actualDurationSeconds, actualDistanceMeters, actualRpe, actualRestSeconds, completedAt, skippedAt, notes]
    PerformedExercise:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        category:
          type: string
        primaryMuscleGroup:
          type: [string, "null"]
        movementPattern:
          type: [string, "null"]
        sessionCount:
          type: integer
        completedSetCount:
          type: integer
        lastPerformedAt:
          type: [string, "null"]
          format: date-time
      required: [id, name, category, primaryMuscleGroup, movementPattern, sessionCount, completedSetCount, lastPerformedAt]
    ExerciseTrend:
      type: object
      properties:
        exercise:
          type: object
          properties:
            id:
              type: string
            name:
              type: string
            category:
              type: string
            primaryMuscleGroup:
              type: [string, "null"]
            movementPattern:
              type: [string, "null"]
          required: [id, name, category, primaryMuscleGroup, movementPattern]
        summary:
          type: object
          properties:
            firstPerformedAt:
              type: [string, "null"]
              format: date-time
            lastPerformedAt:
              type: [string, "null"]
              format: date-time
            totalSessions:
              type: integer
            totalCompletedSets:
              type: integer
            bestWeight:
              type: [number, "null"]
            bestReps:
              type: [integer, "null"]
            bestVolume:
              type: [number, "null"]
            bestEstimated1RM:
              type: [number, "null"]
            bestDurationSeconds:
              type: [integer, "null"]
            bestDistanceMeters:
              type: [number, "null"]
            bestPaceSecondsPerKm:
              type: [number, "null"]
          required: [firstPerformedAt, lastPerformedAt, totalSessions, totalCompletedSets, bestWeight, bestReps, bestVolume, bestEstimated1RM, bestDurationSeconds, bestDistanceMeters, bestPaceSecondsPerKm]
        points:
          type: array
          items:
            $ref: "#/components/schemas/ExerciseTrendPoint"
      required: [exercise, summary, points]
    ExerciseTrendPoint:
      type: object
      properties:
        date:
          type: string
          format: date-time
        sessionId:
          type: string
        setId:
          type: string
        setNumber:
          type: integer
        weight:
          type: [number, "null"]
        reps:
          type: [integer, "null"]
        volume:
          type: [number, "null"]
        estimated1RM:
          type: [number, "null"]
        durationSeconds:
          type: [integer, "null"]
        distanceMeters:
          type: [number, "null"]
        paceSecondsPerKm:
          type: [number, "null"]
        rpe:
          type: [number, "null"]
      required: [date, sessionId, setId, setNumber, weight, reps, volume, estimated1RM, durationSeconds, distanceMeters, paceSecondsPerKm, rpe]
    AnalyticsSummary:
      type: object
      properties:
        range:
          type: object
          properties:
            from:
              type: string
              format: date-time
            to:
              type: string
              format: date-time
          required: [from, to]
        completedWorkoutCount:
          type: integer
        completedSetCount:
          type: integer
        totalVolume:
          type: [number, "null"]
        uniqueExerciseCount:
          type: integer
        lastWorkoutAt:
          type: [string, "null"]
          format: date-time
        workoutsPerWeek:
          type: [number, "null"]
      required: [range, completedWorkoutCount, completedSetCount, totalVolume, uniqueExerciseCount, lastWorkoutAt, workoutsPerWeek]
    TrainingBalance:
      type: object
      properties:
        range:
          type: object
          properties:
            from:
              type: string
              format: date-time
            to:
              type: string
              format: date-time
          required: [from, to]
        muscleGroups:
          type: array
          items:
            $ref: "#/components/schemas/MuscleGroupBalance"
        movementPatterns:
          type: array
          items:
            $ref: "#/components/schemas/MovementPatternBalance"
      required: [range, muscleGroups, movementPatterns]
    MuscleGroupBalance:
      type: object
      properties:
        muscleGroup:
          type: string
        completedSets:
          type: integer
        totalVolume:
          type: [number, "null"]
      required: [muscleGroup, completedSets, totalVolume]
    MovementPatternBalance:
      type: object
      properties:
        movementPattern:
          type: string
        completedSets:
          type: integer
        totalVolume:
          type: [number, "null"]
      required: [movementPattern, completedSets, totalVolume]
