openapi: 3.0.3
info:
  title: xg2g API
  description: |
    API for xg2g (Enigma2 to Plex/Jellyfin Proxy).

    **Authentication**:
    All endpoints support both **Bearer Token** (`Authorization: Bearer <token>`) and **Cookie** (`xg2g_session=<token>`) authentication.
    Streaming and media requests specifically support unified authentication since v3.1.5.

    **License**: PolyForm Noncommercial License 1.0.0.
    Commercial usage is restricted. See repository documentation for details.
  version: 3.0.0
servers:
  - url: /api/v3
    description: Production API

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  parameters:
    HouseholdProfileHeader:
      name: X-Household-Profile
      in: header
      required: false
      description: |
        Optional active household profile id. If omitted, the backend resolves the unrestricted
        default household profile for backward compatibility.
      schema:
        type: string

  schemas:
    # ==================== V3 API Schemas ====================
    APIError:
      type: object
      required: [code, message, requestId]
      properties:
        code:
          type: string
          description: Machine-readable error code
          example: "UNAUTHORIZED"
        message:
          type: string
          description: Human-readable error message
          example: "Authentication required"
        requestId:
          type: string
          description: Request ID for debugging
          example: "req_abc123"
        details:
          description: Optional additional context
          example: "Invalid token format"

    DeviceAuthDeviceType:
      type: string
      enum: [android_phone, android_tablet, android_tv, browser, unknown]

    PairingStatus:
      type: string
      enum: [pending, approved, expired, consumed, revoked]

    PublishedEndpointKind:
      type: string
      enum: [public_https, local_https, local_http]

    PublishedEndpointTLSMode:
      type: string
      enum: [required, prohibited]

    PublishedEndpointSource:
      type: string
      enum: [config, env, operator]

    ConnectivityDeploymentProfile:
      type: string
      enum: [lan, reverse_proxy, tunnel, vps]

    PublishedEndpoint:
      type: object
      additionalProperties: false
      required:
        [url, kind, priority, tlsMode, allowPairing, allowStreaming, allowWeb, allowNative, advertiseReason, source]
      properties:
        url:
          type: string
          format: uri
        kind:
          $ref: "#/components/schemas/PublishedEndpointKind"
        priority:
          type: integer
          format: int32
        tlsMode:
          $ref: "#/components/schemas/PublishedEndpointTLSMode"
        allowPairing:
          type: boolean
        allowStreaming:
          type: boolean
        allowWeb:
          type: boolean
        allowNative:
          type: boolean
        advertiseReason:
          type: string
        source:
          $ref: "#/components/schemas/PublishedEndpointSource"

    ConnectivityConfig:
      type: object
      additionalProperties: false
      required: [profile, allowLocalHTTP, publishedEndpoints]
      properties:
        profile:
          $ref: "#/components/schemas/ConnectivityDeploymentProfile"
        allowLocalHTTP:
          type: boolean
        publishedEndpoints:
          type: array
          items:
            $ref: "#/components/schemas/PublishedEndpoint"

    ConnectivitySelection:
      type: object
      additionalProperties: false
      properties:
        endpoint:
          $ref: "#/components/schemas/PublishedEndpoint"
        reason:
          type: string

    ConnectivitySelections:
      type: object
      additionalProperties: false
      required: [web, webPublic, native, nativePublic, pairing, pairingPublic, streaming]
      properties:
        web:
          $ref: "#/components/schemas/ConnectivitySelection"
        webPublic:
          $ref: "#/components/schemas/ConnectivitySelection"
        native:
          $ref: "#/components/schemas/ConnectivitySelection"
        nativePublic:
          $ref: "#/components/schemas/ConnectivitySelection"
        pairing:
          $ref: "#/components/schemas/ConnectivitySelection"
        pairingPublic:
          $ref: "#/components/schemas/ConnectivitySelection"
        streaming:
          $ref: "#/components/schemas/ConnectivitySelection"

    ConnectivityFinding:
      type: object
      additionalProperties: false
      required: [code, severity, scopes, summary]
      properties:
        code:
          type: string
        severity:
          type: string
          enum: [ok, warn, degraded, fatal]
        scopes:
          type: array
          items:
            type: string
        field:
          type: string
        summary:
          type: string
        detail:
          type: string
        endpointUrl:
          type: string

    ConnectivityRequest:
      type: object
      additionalProperties: false
      required:
        [remoteIsLoopback, tlsDirect, trustedProxyMatch, effectiveHttps, schemeSource, acceptedProxyHeaders, originAllowAll]
      properties:
        remoteAddr:
          type: string
        remoteIp:
          type: string
        remoteIsLoopback:
          type: boolean
        tlsDirect:
          type: boolean
        trustedProxyMatch:
          type: boolean
        effectiveHttps:
          type: boolean
        schemeSource:
          type: string
        acceptedProxyHeaders:
          type: array
          items:
            type: string
        xForwardedProto:
          type: string
        xForwardedHost:
          type: string
        xForwardedFor:
          type: string
        origin:
          type: string
        originAllowed:
          type: boolean
        originAllowAll:
          type: boolean

    ConnectivityContract:
      type: object
      additionalProperties: false
      required:
        [profile, public, status, startupFatal, readinessBlocked, pairingBlocked, webBlocked, allowLocalHTTP, tlsEnabled, forceHTTPS, allowedOrigins, trustedProxies, publishedEndpoints, selections, findings, request]
      properties:
        profile:
          $ref: "#/components/schemas/ConnectivityDeploymentProfile"
        public:
          type: boolean
        status:
          type: string
          enum: [ok, warn, degraded, fatal]
        startupFatal:
          type: boolean
        readinessBlocked:
          type: boolean
        pairingBlocked:
          type: boolean
        webBlocked:
          type: boolean
        allowLocalHTTP:
          type: boolean
        tlsEnabled:
          type: boolean
        forceHTTPS:
          type: boolean
        allowedOrigins:
          type: array
          items:
            type: string
        trustedProxies:
          type: array
          items:
            type: string
        publishedEndpoints:
          type: array
          items:
            $ref: "#/components/schemas/PublishedEndpoint"
        selections:
          $ref: "#/components/schemas/ConnectivitySelections"
        findings:
          type: array
          items:
            $ref: "#/components/schemas/ConnectivityFinding"
        request:
          $ref: "#/components/schemas/ConnectivityRequest"

    StartPairingRequest:
      type: object
      additionalProperties: false
      properties:
        deviceName:
          type: string
        deviceType:
          $ref: "#/components/schemas/DeviceAuthDeviceType"
        requestedPolicyProfile:
          type: string

    PairingSecretRequest:
      type: object
      additionalProperties: false
      required: [pairingSecret]
      properties:
        pairingSecret:
          type: string

    ApprovePairingRequest:
      type: object
      additionalProperties: false
      properties:
        ownerId:
          type: string
        approvedPolicyProfile:
          type: string

    StartPairingResponse:
      type: object
      additionalProperties: false
      required: [pairingId, pairingSecret, userCode, qrPayload, expiresAt]
      properties:
        pairingId:
          type: string
        pairingSecret:
          type: string
        userCode:
          type: string
        qrPayload:
          type: string
        expiresAt:
          type: string
          format: date-time

    PairingStatusResponse:
      type: object
      additionalProperties: false
      required: [pairingId, status, userCode, deviceName, deviceType, expiresAt]
      properties:
        pairingId:
          type: string
        status:
          $ref: "#/components/schemas/PairingStatus"
        userCode:
          type: string
        deviceName:
          type: string
        deviceType:
          $ref: "#/components/schemas/DeviceAuthDeviceType"
        requestedPolicyProfile:
          type: string
        approvedPolicyProfile:
          type: string
        expiresAt:
          type: string
          format: date-time
        approvedAt:
          type: string
          format: date-time
        consumedAt:
          type: string
          format: date-time

    ApprovePairingResponse:
      type: object
      additionalProperties: false
      required: [pairingId, status, ownerId, expiresAt]
      properties:
        pairingId:
          type: string
        status:
          $ref: "#/components/schemas/PairingStatus"
        ownerId:
          type: string
        approvedPolicyProfile:
          type: string
        approvedAt:
          type: string
          format: date-time
        expiresAt:
          type: string
          format: date-time

    ExchangePairingResponse:
      type: object
      additionalProperties: false
      required:
        [
          pairingId,
          deviceId,
          deviceGrantId,
          deviceGrant,
          deviceGrantExpiresAt,
          accessSessionId,
          accessToken,
          accessTokenExpiresAt,
          policyVersion,
          scopes,
          endpoints,
        ]
      properties:
        pairingId:
          type: string
        deviceId:
          type: string
        deviceGrantId:
          type: string
        deviceGrant:
          type: string
        deviceGrantExpiresAt:
          type: string
          format: date-time
        accessSessionId:
          type: string
        accessToken:
          type: string
        accessTokenExpiresAt:
          type: string
          format: date-time
        policyVersion:
          type: string
        scopes:
          type: array
          items:
            type: string
        endpoints:
          type: array
          items:
            $ref: "#/components/schemas/PublishedEndpoint"

    CreateDeviceSessionRequest:
      type: object
      additionalProperties: false
      required: [deviceGrantId, deviceGrant]
      properties:
        deviceGrantId:
          type: string
        deviceGrant:
          type: string

    CreateDeviceSessionResponse:
      type: object
      additionalProperties: false
      required: [deviceId, accessSessionId, accessToken, accessTokenExpiresAt, policyVersion, scopes, endpoints]
      properties:
        deviceId:
          type: string
        rotatedDeviceGrantId:
          type: string
        rotatedDeviceGrant:
          type: string
        rotatedDeviceGrantExpiresAt:
          type: string
          format: date-time
        accessSessionId:
          type: string
        accessToken:
          type: string
        accessTokenExpiresAt:
          type: string
          format: date-time
        policyVersion:
          type: string
        scopes:
          type: array
          items:
            type: string
        endpoints:
          type: array
          items:
            $ref: "#/components/schemas/PublishedEndpoint"

    CreateWebBootstrapRequest:
      type: object
      additionalProperties: false
      required: [targetPath]
      properties:
        targetPath:
          type: string
          description: Absolute same-origin path inside the xg2g web UI.

    CreateWebBootstrapResponse:
      type: object
      additionalProperties: false
      required: [bootstrapId, bootstrapToken, completePath, targetPath, expiresAt]
      properties:
        bootstrapId:
          type: string
        bootstrapToken:
          type: string
        completePath:
          type: string
        targetPath:
          type: string
        expiresAt:
          type: string
          format: date-time

    IntentRequest:
      type: object
      additionalProperties: false
      properties:
        type:
          type: string
          enum: [stream.start, stream.stop]
          default: stream.start
        serviceRef:
          type: string
          description: Required for stream.start. Enigma2 service reference (live playback only).
          example: "1:0:1:445D:453:1:C00000:0:0:0:"
        playbackDecisionToken:
          type: string
          description: Required for stream.start. A secure cryptographically bound JWT token verifying the backend decision policy.

        hwaccel:
          type: string
          enum: [auto, force, off]
          default: auto
          description: |
            Hardware acceleration override (v3.1+).
            - auto: Server decides based on GPU availability
            - force: Force GPU encoding (fails if no GPU)
            - off: Force CPU encoding
          example: "auto"
        correlationId:
          type: string
          description: Optional correlation ID for end-to-end tracing
        startMs:
          type: integer
          format: int64
          description: Optional live playback start offset in milliseconds.
        sessionId:
          type: string
          format: uuid
          description: Required for stream.stop intent
        idempotencyKey:
          type: string
          description: Optional idempotency key for at-most-once semantics
        params:
          type: object
          additionalProperties:
            type: string
          description: Additional parameters
        client:
          $ref: "#/components/schemas/PlaybackCapabilities"

    IntentAcceptedResponse:
      type: object
      additionalProperties: false
      required: [sessionId, requestId, status]
      properties:
        sessionId:
          type: string
          format: uuid
        requestId:
          type: string
          description: Request ID for debugging/tracing.
        status:
          type: string
          enum: [accepted, idempotent_replay]
        correlationId:
          type: string

    SessionResponse:
      type: object
      additionalProperties: false
      required: [sessionId, requestId, state, heartbeatIntervalSeconds, leaseExpiresAt]
      properties:
        sessionId:
          type: string
          format: uuid
        requestId:
          type: string
          description: Request ID for debugging/tracing. Mandatory in P3-1.
          example: "req_abc123"
        serviceRef:
          type: string
        profile:
          type: string
        profileReason:
          type: string
          description: Short machine-readable hint explaining why the effective playback profile/path was selected.
          example: safari_compat_transcode
        state:
          type: string
          description: |
            Session lifecycle state for GET /sessions/{sessionID}. STARTING guarantees a
            session ticket is allocated. READY/DRAINING guarantees a playable HLS stream.
          enum:
            [
              STARTING,
              IDLE,
              PRIMING,
              READY,
              DRAINING,
              STOPPING,
              STOPPED,
              FAILED,
              CANCELLED,
            ]
        reason:
          type: string
          description: Reason code; R_LEASE_BUSY means capacity rejection (no tuner available), not a system fault.
          enum:
            - R_NONE
            - R_UNKNOWN
            - R_BAD_REQUEST
            - R_NOT_FOUND
            - R_LEASE_BUSY
            - R_TUNE_TIMEOUT
            - R_LEASE_EXPIRED
            - R_TUNE_FAILED
            - R_INVARIANT_VIOLATION
            - R_FFMPEG_START_FAILED
            - R_PROCESS_ENDED
            - R_PACKAGER_FAILED
            - R_CANCELLED
            - R_IDLE_TIMEOUT
            - R_CLIENT_STOP
        reasonDetail:
          type: string
        correlationId:
          type: string
        updatedAtMs:
          type: integer
        heartbeatIntervalSeconds:
          type: integer
          format: int32
          description: Canonical server heartbeat cadence for renewing the session lease.
        leaseExpiresAt:
          type: string
          format: date-time
          description: Current observed lease expiry for the session.
        mode:
          type: string
          description: Playback mode for the session.
          enum: [LIVE, RECORDING]
        windowKind:
          type: string
          description: Playback window topology for the session.
          enum: [live, live-dvr, vod, unknown]
        durationSeconds:
          type: number
          format: float
          description: DVR window length for live sessions, in seconds.
        seekableStartSeconds:
          type: number
          format: float
          description: Earliest seekable position in seconds.
        seekableEndSeconds:
          type: number
          format: float
          description: Latest seekable position in seconds.
        liveEdgeSeconds:
          type: number
          format: float
          description: Current live edge position in seconds (live only).
        playbackUrl:
          type: string
          description: Playback URL for the HLS playlist.
          format: uri
        trace:
          $ref: "#/components/schemas/PlaybackTrace"

    SessionHeartbeatResponse:
      type: object
      additionalProperties: false
      required: [sessionId, acknowledged, leaseExpiresAt]
      properties:
        sessionId:
          type: string
          format: uuid
        acknowledged:
          type: boolean
          description: True when the heartbeat was accepted for the addressed session.
        leaseExpiresAt:
          type: string
          format: date-time
          description: Renewed lease expiry after the heartbeat acknowledgement.

    Service:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        number:
          type: string
        group:
          type: string
        logoUrl:
          type: string
          format: uri
        enabled:
          type: boolean
        serviceRef:
          type: string
          description: Service reference for streaming (extracted from M3U URL)
        resolution:
          type: string
          description: Video resolution (e.g. 1920x1080)
        codec:
          type: string
          description: Video codec (e.g. h264)

    SessionRecord:
      type: object
      properties:
        sessionId:
          type: string
          format: uuid
        serviceRef:
          type: string
        profile:
          type: object
        state:
          type: string
          description: |
            Session lifecycle state. READY guarantees a playable HLS stream (playlist + at least one segment,
            atomically published). PRIMING means FFmpeg is running but content is not yet playable.
          enum:
            [
              NEW,
              STARTING,
              PRIMING,
              READY,
              DRAINING,
              STOPPING,
              STOPPED,
              FAILED,
              CANCELLED,
            ]
        reason:
          type: string
          description: Reason code; R_LEASE_BUSY means capacity rejection (no tuner available), not a system fault.
          enum:
            - R_NONE
            - R_UNKNOWN
            - R_BAD_REQUEST
            - R_NOT_FOUND
            - R_LEASE_BUSY
            - R_TUNE_TIMEOUT
            - R_LEASE_EXPIRED
            - R_TUNE_FAILED
            - R_INVARIANT_VIOLATION
            - R_FFMPEG_START_FAILED
            - R_PROCESS_ENDED
            - R_PACKAGER_FAILED
            - R_CANCELLED
            - R_IDLE_TIMEOUT
            - R_CLIENT_STOP
        reasonDetail:
          type: string
        createdAtUnix:
          type: integer
          format: int64
        updatedAtUnix:
          type: integer
          format: int64
        lastAccessUnix:
          type: integer
          format: int64
        tunerID:
          type: string
        correlationId:
          type: string
        contextData:
          type: object
          additionalProperties:
            type: string

    ScanStatus:
      type: object
      properties:
        state:
          type: string
          enum: [idle, running, complete, failed, cancelled]
        startedAt:
          type: integer
          format: int64
          description: Unix timestamp of when the current or last scan started
        finishedAt:
          type: integer
          format: int64
          description: Unix timestamp of when the last scan completed
        totalChannels:
          type: integer
          description: Total number of channels in the playlist
        scannedChannels:
          type: integer
          description: Number of attempts (successful/failed) to probe channels so far
        updatedCount:
          type: integer
          description: Number of capabilities successfully updated in the store
        lastError:
          type: string

    PlaybackFeedbackRequest:
      type: object
      required: [event]
      additionalProperties: false
      properties:
        event:
          type: string
          enum: [error, warning, info]
        code:
          type: integer
          description: MediaError code if applicable
        message:
          type: string
        details:
          type: object

    # ==================== Legacy Schemas ====================
    Error:
      type: object
      properties:
        type:
          type: string
          format: uri
          example: "about:blank"
        title:
          type: string
          example: "Internal Server Error"
        status:
          type: integer
          example: 500
        detail:
          type: string
          example: "Failed to connect to database"

    SystemHealth:
      type: object
      properties:
        status:
          type: string
          enum: [ok, degraded, error]
        serverTime:
          type: string
          format: date-time
        receiver:
          $ref: "#/components/schemas/ComponentStatus"
        epg:
          $ref: "#/components/schemas/EPGStatus"
        version:
          type: string
        uptimeSeconds:
          type: integer
          format: int64

    SystemInfoData:
      type: object
      properties:
        hardware:
          type: object
          properties:
            brand: { type: string }
            model: { type: string }
            boxtype: { type: string }
            chipset: { type: string }
            chipsetDescription: { type: string }
        software:
          type: object
          properties:
            oeVersion: { type: string }
            imageDistro: { type: string }
            imageVersion: { type: string }
            enigmaVersion: { type: string }
            kernelVersion: { type: string }
            driverDate: { type: string }
            webifVersion: { type: string }
        tuners:
          type: array
          items:
            type: object
            properties:
              name: { type: string }
              type: { type: string }
              status: { type: string }
        network:
          type: object
          properties:
            interfaces:
              type: array
              items:
                type: object
                properties:
                  name: { type: string }
                  type: { type: string }
                  speed: { type: string }
                  mac: { type: string }
                  ip: { type: string }
                  ipv6: { type: string }
                  dhcp: { type: boolean }
        storage:
          type: object
          properties:
            devices:
              type: array
              items:
                $ref: "#/components/schemas/StorageItem"
            locations:
              type: array
              items:
                $ref: "#/components/schemas/StorageItem"
        runtime:
          type: object
          properties:
            uptime: { type: string }
        resource:
          type: object
          properties:
            memoryTotal: { type: string }
            memoryAvailable: { type: string }
            memoryUsed: { type: string }

    StorageItem:
      type: object
      additionalProperties: false
      required: [mountStatus, healthStatus, access, isNas]
      properties:
        model: { type: string }
        capacity: { type: string }
        mount: { type: string }
        origin:
          type: string
          description: "Storage perspective: receiver or xg2g."
        pathType:
          type: string
          description: "Topology class of the path: receiver_attached, receiver_share, xg2g_local, xg2g_share, xg2g_aggregate, or unknown."
        mountStatus:
          type: string
          enum: [mounted, unmounted, unknown]
        healthStatus:
          type: string
          description: "Status of the storage device. 'skipped' indicates the monitor was too busy to evaluate."
          enum: [ok, timeout, error, unknown, skipped]
        access:
          type: string
          description: "Access level detected during probe."
          enum: [none, ro, rw]
        isNas: { type: boolean }
        fsType: { type: string }
        checkedAt: { type: string, format: date-time }

    ComponentStatus:
      type: object
      properties:
        status:
          type: string
          enum: [ok, error]
        lastCheck:
          type: string
          format: date-time

    EPGStatus:
      type: object
      properties:
        status:
          type: string
          enum: [ok, missing]
        missingChannels:
          type: integer

    CurrentServiceInfo:
      type: object
      description: Current live service and EPG information from receiver
      properties:
        status: { type: string, enum: [ok, unavailable] }
        channel:
          type: object
          properties:
            name: { type: string }
            ref: { type: string }
        now:
          type: object
          properties:
            title: { type: string }
            description: { type: string }
            beginTimestamp: { type: integer, format: int64 }
            durationSec: { type: integer }
        next:
          type: object
          properties:
            title: { type: string }

    OpenWebIFConfig:
      type: object
      properties:
        baseUrl:
          type: string
        username:
          type: string
        password:
          type: string
        streamPort:
          type: integer

    EPGConfig:
      type: object
      properties:
        enabled:
          type: boolean
        days:
          type: integer
        source:
          type: string
          enum: [bouquet, per-service]

    PiconsConfig:
      type: object
      properties:
        baseUrl:
          type: string

    StreamingConfig:
      type: object
      description: Streaming delivery policy configuration (ADR-00X)
      properties:
        deliveryPolicy:
          type: string
          enum: [universal]
          description: Streaming delivery policy (only 'universal' is supported)
          default: universal

    VerificationConfig:
      type: object
      properties:
        enabled:
          type: boolean
          default: true
        interval:
          type: string
          description: Drift verification interval (e.g. "1m")
          default: "1m"

    SeriesRuleRunReport:
      type: object
      properties:
        ruleId: { type: string }
        runId: { type: string }
        trigger: { type: string }
        startedAt: { type: string, format: date-time }
        finishedAt: { type: string, format: date-time }
        durationMs: { type: integer, format: int64 }
        windowFrom: { type: integer, format: int64 }
        windowTo: { type: integer, format: int64 }
        status: { type: string, enum: [success, partial, failed] }
        summary: { $ref: "#/components/schemas/RunSummary" }
        snapshot: { $ref: "#/components/schemas/RuleSnapshot" }
        decisions:
          type: array
          items: { $ref: "#/components/schemas/RunDecision" }
        errors:
          type: array
          items: { $ref: "#/components/schemas/RunError" }
        conflicts:
          type: array
          items: { $ref: "#/components/schemas/RunConflict" }

    RunSummary:
      type: object
      properties:
        epgItemsScanned: { type: integer }
        epgItemsMatched: { type: integer }
        timersAttempted: { type: integer }
        timersCreated: { type: integer }
        timersSkipped: { type: integer }
        timersConflicted: { type: integer }
        timersErrored: { type: integer }
        maxTimersGlobalPerRunHit: { type: boolean }
        maxMatchesScannedPerRuleHit: { type: boolean }
        receiverUnreachable: { type: boolean }

    RunDecision:
      type: object
      additionalProperties: false
      properties:
        serviceRef: { type: string }
        begin: { type: integer, format: int64 }
        end: { type: integer, format: int64 }
        title: { type: string }
        action: { type: string }
        reason: { type: string }
        matchReason:
          type: array
          items: { type: string }
        timerId: { type: string }
        details: { type: string }

    RunError:
      type: object
      properties:
        type: { type: string }
        message: { type: string }
        at: { type: string, format: date-time }
        retryable: { type: boolean }

    RunConflict:
      type: object
      properties:
        serviceRef: { type: string }
        begin: { type: integer, format: int64 }
        end: { type: integer, format: int64 }
        title: { type: string }
        blockingTimerId: { type: string }
        overlapSeconds: { type: integer, format: int64 }
        message: { type: string }

    RuleSnapshot:
      type: object
      properties:
        id: { type: string }
        enabled: { type: boolean }
        keyword: { type: string }
        channelRef: { type: string }
        days:
          type: array
          items: { type: integer }
        startWindow: { type: string }
        priority: { type: integer }
    SeriesRule:
      type: object
      properties:
        id:
          type: string
          readOnly: true
        enabled:
          type: boolean
          default: true
        keyword:
          type: string
          description: Search term or regex for event title
        channelRef:
          type: string
          description: Optional service reference to restrict rule
        days:
          type: array
          items:
            type: integer
            minimum: 0
            maximum: 6
          description: Days of week (0=Sunday)
        startWindow:
          type: string
          description: Time window HHMM-HHMM
        priority:
          type: integer
          default: 0
        lastRunAt:
          type: string
          format: date-time
        lastRunStatus:
          type: string
        lastRunSummary:
          $ref: "#/components/schemas/RunSummary"

    SeriesRuleUpdate:
      type: object
      required: [enabled, keyword, priority]
      properties:
        enabled:
          type: boolean
        keyword:
          type: string
          minLength: 1
          description: Search term or regex for event title
        channelRef:
          type: string
          description: Optional service reference to restrict rule
        days:
          type: array
          items:
            type: integer
            minimum: 0
            maximum: 6
          description: Days of week (0=Sunday)
        startWindow:
          type: string
          description: Time window HHMM-HHMM
        priority:
          type: integer

    ConfigUpdate:
      type: object
      properties:
        openWebIF:
          $ref: "#/components/schemas/OpenWebIFConfig"
        bouquets:
          type: array
          items:
            type: string
        epg:
          $ref: "#/components/schemas/EPGConfig"
        picons:
          $ref: "#/components/schemas/PiconsConfig"
        verification:
          $ref: "#/components/schemas/VerificationConfig"
        logLevel:
          type: string
          description: Log level to set (debug, info, warn, error)
        household:
          $ref: "#/components/schemas/HouseholdConfigUpdate"

    AppConfig:
      type: object
      properties:
        version:
          type: string
        dataDir:
          type: string
        logLevel:
          type: string
        openWebIF:
          $ref: "#/components/schemas/OpenWebIFConfig"
        bouquets:
          type: array
          items:
            type: string
        epg:
          $ref: "#/components/schemas/EPGConfig"
        picons:
          $ref: "#/components/schemas/PiconsConfig"
        streaming:
          $ref: "#/components/schemas/StreamingConfig"
        monetization:
          $ref: "#/components/schemas/MonetizationStatus"
        verification:
          $ref: "#/components/schemas/VerificationConfig"
        household:
          $ref: "#/components/schemas/HouseholdStatus"
        connectivity:
          $ref: "#/components/schemas/ConnectivityConfig"

    HouseholdConfigUpdate:
      type: object
      additionalProperties: false
      properties:
        pin:
          type: string
          nullable: true
          description: >
            Optional numeric household PIN update. Omit to keep the current PIN.
            Send an empty string to clear the configured PIN. The plaintext value
            is accepted only for update input; the server persists a hash only.

    HouseholdStatus:
      type: object
      additionalProperties: false
      properties:
        pinConfigured:
          type: boolean
          description: True when the server currently has a household PIN hash configured.

    HouseholdUnlockRequest:
      type: object
      required: [pin]
      additionalProperties: false
      properties:
        pin:
          type: string
          description: Plaintext household PIN used to unlock the current browser session.

    HouseholdUnlockStatus:
      type: object
      required: [pinConfigured, unlocked]
      additionalProperties: false
      properties:
        pinConfigured:
          type: boolean
        unlocked:
          type: boolean
          description: >
            True when the current authenticated browser session is unlocked for
            protected household actions. Unlock state ends on logout, relock,
            browser-session end, or server-side unlock TTL expiry.

    HouseholdProfilePermissions:
      type: object
      required: [dvrPlayback, dvrManage, settings]
      additionalProperties: false
      properties:
        dvrPlayback:
          type: boolean
        dvrManage:
          type: boolean
        settings:
          type: boolean

    HouseholdProfile:
      type: object
      required:
        - id
        - name
        - kind
        - allowedBouquets
        - allowedServiceRefs
        - favoriteServiceRefs
        - permissions
      additionalProperties: false
      properties:
        id:
          type: string
        name:
          type: string
        kind:
          type: string
          enum: [adult, child]
        maxFsk:
          type: integer
          nullable: true
          minimum: 0
        allowedBouquets:
          type: array
          items:
            type: string
        allowedServiceRefs:
          type: array
          items:
            type: string
        favoriteServiceRefs:
          type: array
          items:
            type: string
        permissions:
          $ref: "#/components/schemas/HouseholdProfilePermissions"

    MonetizationStatus:
      type: object
      properties:
        enabled:
          type: boolean
        model:
          type: string
          description: Monetization model exposed to the client bootstrap flow.
        productName:
          type: string
        requiredScopes:
          type: array
          description: Scope names that the authenticated principal must all have before the client is commercially unlocked.
          items:
            type: string
        purchaseUrl:
          type: string
          format: uri
        enforcement:
          type: string
          description: Whether the client should enforce the unlock gate.
        unlocked:
          type: boolean
          description: True when the authenticated principal is already unlocked.

    EntitlementGrant:
      type: object
      properties:
        scope:
          type: string
        source:
          type: string
        grantedAt:
          type: string
          format: date-time
        expiresAt:
          type: string
          format: date-time
        active:
          type: boolean

    EntitlementStatus:
      type: object
      properties:
        principalId:
          type: string
        model:
          type: string
        productName:
          type: string
        purchaseUrl:
          type: string
          format: uri
        enforcement:
          type: string
        requiredScopes:
          type: array
          items:
            type: string
        grantedScopes:
          type: array
          items:
            type: string
        missingScopes:
          type: array
          items:
            type: string
        unlocked:
          type: boolean
        grants:
          type: array
          items:
            $ref: "#/components/schemas/EntitlementGrant"

    EntitlementOverrideRequest:
      type: object
      required: [scopes]
      properties:
        principalId:
          type: string
        scopes:
          type: array
          minItems: 1
          items:
            type: string
        expiresAt:
          type: string
          format: date-time

    EntitlementReceiptRequest:
      type: object
      required: [provider, productId, purchaseToken]
      properties:
        principalId:
          type: string
          description: Submit a verified purchase for another principal when authenticated with admin scope.
        userId:
          type: string
          description: Provider-specific user identifier required by Amazon Appstore receipt verification.
        provider:
          type: string
          description: Receipt provider identifier.
        productId:
          type: string
        purchaseToken:
          type: string

    EntitlementReceiptResponse:
      type: object
      required:
        - provider
        - productId
        - purchaseState
        - action
        - mappedScopes
        - entitlementStatus
      properties:
        principalId:
          type: string
        provider:
          type: string
        productId:
          type: string
        source:
          type: string
        purchaseState:
          type: string
          description: Verified purchase state returned by the provider.
        action:
          type: string
          description: Entitlement mutation applied after verification.
        mappedScopes:
          type: array
          items:
            type: string
        orderId:
          type: string
        purchaseTime:
          type: string
          format: date-time
        testPurchase:
          type: boolean
        entitlementStatus:
          $ref: "#/components/schemas/EntitlementStatus"

    Bouquet:
      type: object
      properties:
        name:
          type: string
        services:
          type: integer

    NowNextRequest:
      type: object
      required: [services]
      properties:
        services:
          type: array
          minItems: 1
          items:
            type: string

    NowNextEntry:
      type: object
      required: [title, start, end]
      properties:
        title:
          type: string
        start:
          type: integer
          description: Unix timestamp (seconds)
        end:
          type: integer
          description: Unix timestamp (seconds)
        startXmltv:
          type: string
          description: Original XMLTV start timestamp including offset
        endXmltv:
          type: string
          description: Original XMLTV end timestamp including offset

    NowNextItem:
      type: object
      required: [serviceRef]
      properties:
        serviceRef:
          type: string
        now:
          $ref: "#/components/schemas/NowNextEntry"
        next:
          $ref: "#/components/schemas/NowNextEntry"

    NowNextResponse:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/NowNextItem"

    ProblemCode:
      type: string
      description: Registry-backed public machine-readable short code for RFC7807 responses.
      enum:
        - ADD_FAILED
        - ADMISSION_ENGINE_DISABLED
        - ADMISSION_NO_TUNERS
        - ADMISSION_SESSIONS_FULL
        - ADMISSION_STATE_UNKNOWN
        - ADMISSION_TRANSCODES_FULL
        - ADMISSION_UNAVAILABLE
        - BOUQUET_NOT_FOUND
        - BREAKER_OPEN
        - CLAIM_MISMATCH
        - CLIENT_UNAVAILABLE
        - CONCURRENT_BUILDS_EXCEEDED
        - CONFLICT
        - DELETE_FAILED
        - DIFF_FAILED
        - DURATION_INVALID
        - DURATION_NEGATIVE
        - DURATION_OVERFLOW
        - ENGINE_ERROR
        - FILE_NOT_FOUND
        - FORBIDDEN
        - HTTPS_REQUIRED
        - INTERNAL_ERROR
        - INTERNAL_SERVER_ERROR
        - INVALID_CAPABILITIES
        - INVALID_ID
        - INVALID_INPUT
        - INVALID_PLAYLIST_PATH
        - INVALID_SESSION_ID
        - INVALID_TIME
        - INVALID_TOKEN
        - LIBRARY_ROOT_NOT_FOUND
        - LIBRARY_SCAN_RUNNING
        - METHOD_NOT_ALLOWED
        - NOT_FOUND
        - NOT_IMPLEMENTED
        - PANIC
        - PATH_TRAVERSAL
        - PREFLIGHT_BAD_GATEWAY
        - PREFLIGHT_FORBIDDEN
        - PREFLIGHT_INTERNAL
        - PREFLIGHT_NOT_FOUND
        - PREFLIGHT_TIMEOUT
        - PREFLIGHT_UNAUTHORIZED
        - PREFLIGHT_UNREACHABLE
        - PREPARING
        - PROVIDER_ERROR
        - RATE_LIMIT_EXCEEDED
        - READ_FAILED
        - RECEIVER_ERROR
        - RECEIVER_INCONSISTENT
        - RECEIVER_UNREACHABLE
        - RECORDING_NOT_FOUND
        - RECORDING_PREPARING
        - REFRESH_FAILED
        - REFRESH_IN_PROGRESS
        - REMOTE_PROBE_UNSUPPORTED
        - R_RECORDING_NOT_READY
        - SAVE_FAILED
        - SCAN_UNAVAILABLE
        - SECURITY_UNAVAILABLE
        - SERVICE_NOT_FOUND
        - SERVICE_UNAVAILABLE
        - SESSION.EXPIRED
        - SESSION.NOT_FOUND
        - SESSION.UPDATE_ERROR
        - SESSION_NOT_FOUND
        - STOP_FAILED
        - STORE_ERROR
        - TOKEN_AUD_MISMATCH
        - TOKEN_CAP_MISMATCH
        - TOKEN_ERROR
        - TOKEN_EXPIRED
        - TOKEN_INVALID_ALG
        - TOKEN_INVALID_SIG
        - TOKEN_ISS_MISMATCH
        - TOKEN_MALFORMED
        - TOKEN_MISSING
        - TOKEN_MISSING_CLAIM
        - TOKEN_MODE_MISMATCH
        - TOKEN_NOT_ACTIVE
        - TOKEN_SUB_MISMATCH
        - TOKEN_TTL_EXCEEDED
        - TRANSCODE_CANCELED
        - TRANSCODE_FAILED
        - TRANSCODE_STALLED
        - TRANSCODE_START_TIMEOUT
        - UNAUTHORIZED
        - UNAVAILABLE
        - UPDATE_FAILED
        - UPSTREAM_AUTH
        - UPSTREAM_EMPTY
        - UPSTREAM_ERROR
        - UPSTREAM_RESULT_FALSE
        - UPSTREAM_TIMEOUT
        - UPSTREAM_UNAVAILABLE
        - V3_UNAVAILABLE
        - capabilities_invalid
        - capabilities_missing
        - decision_ambiguous
        - invariant_violation
        - session_gone

    ErrorSeverity:
      type: string
      description: Relative operational severity for this problem code.
      enum: [info, warning, error, critical]

    ErrorCatalogEntry:
      type: object
      required: [code, problemType, title, description, operatorHint, severity, retryable]
      additionalProperties: false
      properties:
        code:
          $ref: "#/components/schemas/ProblemCode"
        description:
          type: string
          description: Stable operator-facing explanation of what the code means and when it is emitted.
        operatorHint:
          type: string
          description: Recommended first remediation step or operator action for this code.
        problemType:
          type: string
          description: Canonical RFC7807 problem type emitted for this code.
        severity:
          $ref: "#/components/schemas/ErrorSeverity"
        retryable:
          type: boolean
          description: Whether an automated retry has a reasonable chance of succeeding without a code change.
        runbookUrl:
          type: string
          description: Optional URL or repository-relative runbook path with deeper remediation guidance.
        title:
          type: string
          description: Default human-readable title emitted when no more specific title is provided.

    ErrorCatalogResponse:
      type: object
      required: [items]
      additionalProperties: false
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/ErrorCatalogEntry"

    ProblemDetails:
      type: object
      required: [type, title, status, requestId]
      additionalProperties: false
      properties:
        type: { type: string }
        title: { type: string }
        status: { type: integer }
        requestId:
          type: string
          description: Correlation ID (UUID or prefixed string like req_abc123)
        code:
          $ref: "#/components/schemas/ProblemCode"
        detail: { type: string }
        instance: { type: string }
        fields:
          type: object
          additionalProperties: true
        conflicts:
          type: array
          items: { $ref: "#/components/schemas/TimerConflict" }

    Timer:
      type: object
      required: [timerId, serviceRef, begin, end, name, state]
      properties:
        timerId: { type: string }
        serviceRef: { type: string }
        begin: { type: integer, format: int64 }
        end: { type: integer, format: int64 }
        name: { type: string }
        description: { type: string }
        serviceName: { type: string }
        state:
          type: string
          enum: [scheduled, recording, completed, disabled, unknown]
        receiverState:
          type: object
          additionalProperties: true
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }

    PlaybackInfo:
      type: object
      required: [mode, requestId, sessionId, isSeekable]
      additionalProperties: false
      properties:
        requestId:
          type: string
          description: Correlation ID for the request. Mandatory in P3-1.
        sessionId:
          type: string
          description: Unique ID for the stream session. Mandatory in P3-1.
        mode:
          $ref: "#/components/schemas/PlaybackInfoMode"
        decisionReason:
          type: string
          description: Machine-readable constant explaining why this mode was chosen (e.g. 'CAPABILITY_H264_UNSUPPORTED').
        url:
          type: string
          description: Relative URL for the selected playback strategy. Optional for deny or decision-led cases.
        seekable:
          type: boolean
          description: Deprecated compatibility mirror of `isSeekable`. Omitted if not emitted.
        isSeekable:
          type: boolean
          description: Authoritative seekability flag. Always present on successful PlaybackInfo responses and must be consumed fail-closed if absent at runtime.
        dvrWindowSeconds:
          type: integer
          format: int64
          description: Absolute DVR window length in seconds. Becomes required in P3-4.
        liveEdgeUnix:
          type: integer
          format: int64
          description: wall-clock timestamp (UNIX) of the latest segment. Becomes required in P3-4.
        startUnix:
          type: integer
          format: int64
          description: wall-clock timestamp (UNIX) of the earliest segment in window. Becomes required in P3-4.
        durationSeconds:
          type: integer
          format: int64
          minimum: 1
          description: Canonical playback duration in seconds. Omitted if unknown or still preparing.
        durationSource:
          $ref: "#/components/schemas/PlaybackInfoDurationSource"
        resume:
          $ref: "#/components/schemas/ResumeSummary"
        playbackDecisionToken:
          type: string
          description: Attestation token for live playback intent creation
        container:
          type: string
          description: Truthful container name if known (e.g., ts, mp4, mkv).
        videoCodec:
          type: string
          description: Truthful video codec if known (e.g., h264, hevc, mpeg2).
        audioCodec:
          type: string
          description: Truthful audio codec if known (e.g., aac, ac3, mp2).
        reason:
          $ref: "#/components/schemas/PlaybackInfoReason"
        decision:
          $ref: "#/components/schemas/PlaybackDecision"
          description: Backend playback decision (P4-1). Required when client provides capabilities.

    PlaybackInfoMode:
      type: string
      enum: [hls, direct_mp4, deny]
      description: Selected playback strategy output mode.

    PlaybackInfoDurationSource:
      type: string
      enum: [store, cache, probe]
      description: Source of the reported duration, when durationSeconds is present.

    PlaybackInfoReason:
      type: string
      enum:
        - directplay_match
        - transcode_audio
        - transcode_video
        - transcode_all
        - container_mismatch
        - unknown
      description: Reason for the playback decision.

    # ==================== P4-1: Decision Engine Schemas ====================

    PlaybackCapabilities:
      type: object
      required: [capabilitiesVersion, container, videoCodecs, audioCodecs]
      additionalProperties: false
      description: Client capabilities for playback decision (P4-1)
      properties:
        capabilitiesVersion:
          type: integer
          description: "Capabilities contract version (current: 3)"
          example: 3
        container:
          type: array
          items:
            type: string
          description: Supported container formats
          example: ["mp4", "webm", "hls"]
        videoCodecs:
          type: array
          items:
            type: string
          description: Supported video codecs
          example: ["h264", "hevc"]
        videoCodecSignals:
          type: array
          description: Runtime decode signals per video codec from MediaCapabilities and fallback browser probes
          items:
            $ref: "#/components/schemas/PlaybackVideoCodecSignal"
        audioCodecs:
          type: array
          items:
            type: string
          description: Supported audio codecs
          example: ["aac", "ac3"]
        maxVideo:
          type: object
          description: Optional resolution/FPS constraints
          properties:
            width:
              type: integer
              example: 1920
            height:
              type: integer
              example: 1080
            fps:
              type: integer
              example: 60
        deviceContext:
          $ref: "#/components/schemas/PlaybackDeviceContext"
        networkContext:
          $ref: "#/components/schemas/PlaybackNetworkContext"
        supportsHls:
          type: boolean
          description: Whether client supports HLS playlists
          example: true
        supportsRange:
          type: boolean
          description: Whether client supports HTTP range requests
          example: true
        deviceType:
          type: string
          description: Client device category for policy decisions
          example: "desktop"
        hlsEngines:
          type: array
          items:
            type: string
          description: Supported HLS playback engines (e.g. native, hlsjs)
          example: ["native", "hlsjs"]
        preferredHlsEngine:
          type: string
          description: Preferred HLS playback engine for this client (e.g. native, hlsjs)
          example: "native"
        runtimeProbeUsed:
          type: boolean
          description: Whether the capability snapshot was gathered from runtime browser probes
          example: true
        runtimeProbeVersion:
          type: integer
          description: Version of the runtime playback probe contract
          example: 2
        clientFamilyFallback:
          type: string
          description: Browser-family fallback used when the server needs conservative capability defaults
          example: "chromium_hlsjs"
        allowTranscode:
          type: boolean
          description: Whether client allows transcoding (force bypass)
          default: true
          example: true

    PlaybackVideoCodecSignal:
      type: object
      required: [codec, supported]
      additionalProperties: false
      properties:
        codec:
          type: string
          description: Video codec identifier
          example: "av1"
        supported:
          type: boolean
          description: Whether the browser can decode this codec at all
          example: true
        smooth:
          type: boolean
          description: Whether the browser reported smooth playback for this codec
          example: true
        powerEfficient:
          type: boolean
          description: Whether the browser reported hardware-efficient decode for this codec
          example: true

    PlaybackDeviceContext:
      type: object
      additionalProperties: false
      properties:
        brand:
          type: string
        product:
          type: string
        device:
          type: string
        platform:
          type: string
        manufacturer:
          type: string
        model:
          type: string
        osName:
          type: string
        osVersion:
          type: string
        sdkInt:
          type: integer

    PlaybackClientSnapshot:
      type: object
      additionalProperties: false
      properties:
        capturedAtMs:
          type: integer
          format: int64
          nullable: true
        capHash:
          type: string
          nullable: true
        clientCapsSource:
          type: string
          nullable: true
        clientFamily:
          type: string
          nullable: true
        preferredHlsEngine:
          type: string
          nullable: true
        deviceType:
          type: string
          nullable: true
        runtimeProbeUsed:
          type: boolean
          nullable: true
        runtimeProbeVersion:
          type: integer
          nullable: true
        deviceContext:
          $ref: "#/components/schemas/PlaybackDeviceContext"
        networkContext:
          $ref: "#/components/schemas/PlaybackNetworkContext"

    PlaybackClientSummary:
      type: object
      additionalProperties: false
      properties:
        capHash:
          type: string
          nullable: true
        clientCapsSource:
          type: string
          nullable: true
        clientFamily:
          type: string
          nullable: true
        preferredHlsEngine:
          type: string
          nullable: true
        deviceType:
          type: string
          nullable: true
        platform:
          type: string
          nullable: true
        osName:
          type: string
          nullable: true
        osVersion:
          type: string
          nullable: true
        model:
          type: string
          nullable: true
        networkKind:
          type: string
          nullable: true
        runtimeProbeVersion:
          type: integer
          nullable: true

    PlaybackNetworkContext:
      type: object
      additionalProperties: false
      properties:
        kind:
          type: string
        downlinkKbps:
          type: integer
        metered:
          type: boolean
        internetValidated:
          type: boolean

    PlaybackDecision:
      type: object
      required:
        [
          mode,
          selected,
          outputs,
          constraints,
          reasons,
          trace,
          selectedOutputUrl,
          selectedOutputKind,
        ]
      additionalProperties: false
      description: Complete playback decision from backend (P4-1)
      properties:
        mode:
          type: string
          enum: [direct_play, direct_stream, transcode, deny]
          description: Playback mode decision
        selectedOutputUrl:
          type: string
          format: uri
          description: The explicitly selected playback URL (backend-driven).
        selectedOutputKind:
          type: string
          enum: [file, hls]
          description: The explicitly selected playback kind.
        selected:
          type: object
          required: [container, videoCodec, audioCodec]
          additionalProperties: false
          description: Selected output format
          properties:
            container:
              type: string
              example: "mp4"
            videoCodec:
              type: string
              example: "h264"
            audioCodec:
              type: string
              example: "aac"
        outputs:
          type: array
          items:
            $ref: "#/components/schemas/PlaybackOutput"
          description: Available output URLs/playlists
        constraints:
          type: array
          items:
            type: string
          description: Applied constraints (e.g., downscale_required)
          example: []
        reasons:
          type: array
          items:
            type: string
          description: Machine-readable decision reason codes
          example: ["source_compatible_with_client"]
        trace:
          $ref: "#/components/schemas/PlaybackTrace"
      example:
        mode: "direct_play"
        selectedOutputUrl: "/api/v3/recordings/rec:123/stream.mp4"
        selectedOutputKind: "file"
        selected:
          container: "mp4"
          videoCodec: "h264"
          audioCodec: "aac"
        outputs:
          - kind: "file"
            url: "/api/v3/recordings/rec:123/stream.mp4"
        constraints: []
        reasons: ["source_compatible_with_client"]
        trace:
          requestId: "550e8400-e29b-41d4-a716-446655440000"
    PlaybackTrace:
      type: object
      required: [requestId]
      additionalProperties: false
      description: Traceability information
      properties:
        requestId:
          type: string
          description: Correlation ID (UUID or prefixed string like req_abc123)
        requestProfile:
          type: string
          nullable: true
        requestedIntent:
          type: string
          nullable: true
        resolvedIntent:
          type: string
          nullable: true
        policyModeHint:
          type: string
          nullable: true
        effectiveRuntimeMode:
          type: string
          nullable: true
        effectiveModeSource:
          type: string
          nullable: true
        qualityRung:
          type: string
          nullable: true
        audioQualityRung:
          type: string
          nullable: true
        videoQualityRung:
          type: string
          nullable: true
        degradedFrom:
          type: string
          nullable: true
        hostPressureBand:
          type: string
          nullable: true
        hostOverrideApplied:
          type: boolean
          nullable: true
        clientCapsSource:
          type: string
          nullable: true
        clientFamily:
          type: string
          nullable: true
        client:
          $ref: "#/components/schemas/PlaybackClientSnapshot"
        sessionId:
          type: string
          nullable: true
        source:
          $ref: "#/components/schemas/PlaybackSourceProfile"
        clientPath:
          type: string
          nullable: true
        inputKind:
          type: string
          nullable: true
        preflightReason:
          type: string
          nullable: true
        preflightDetail:
          type: string
          nullable: true
        targetProfileHash:
          type: string
          nullable: true
        targetProfile:
          $ref: "#/components/schemas/PlaybackTargetProfile"
        autoCodecPolicy:
          type: string
          nullable: true
          description: Neutral selection policy identifier used for automatic codec choice.
        autoCodecRequestedCodecs:
          type: string
          nullable: true
          description: Comma-separated codec preference set considered during automatic codec choice.
        autoCodecSelectedCodec:
          type: string
          nullable: true
          description: Codec selected from the automatic codec preference set.
        autoCodecPerformanceClass:
          type: string
          nullable: true
          description: Host performance class observed during automatic codec choice.
        autoCodecBenchmarkClass:
          type: string
          nullable: true
          description: Benchmark capability class for the selected codec on the current host.
        ffmpegPlan:
          $ref: "#/components/schemas/PlaybackTraceFfmpegPlan"
        runtimeDiagnostics:
          $ref: "#/components/schemas/PlaybackTraceRuntimeDiagnostics"
        operator:
          $ref: "#/components/schemas/PlaybackTraceOperator"
        firstFrameAtMs:
          type: integer
          nullable: true
        fallbackCount:
          type: integer
          nullable: true
        lastFallbackReason:
          type: string
          nullable: true
        stopReason:
          type: string
          nullable: true
        stopClass:
          type: string
          nullable: true

    PlaybackTraceOperator:
      type: object
      additionalProperties: false
      properties:
        forcedIntent:
          type: string
          nullable: true
        maxQualityRung:
          type: string
          nullable: true
        runtimePolicyAction:
          type: string
          nullable: true
        runtimePolicyPhase:
          type: string
          nullable: true
        runtimeProbeCandidate:
          type: string
          nullable: true
        runtimePolicyReasons:
          type: array
          items:
            type: string
          nullable: true
        runtimePolicyConstraints:
          type: array
          items:
            type: string
          nullable: true
        runtimePolicyReplay:
          $ref: "#/components/schemas/PlaybackTraceRuntimeReplay"
        runtimePolicyTimeline:
          type: array
          items:
            $ref: "#/components/schemas/PlaybackTraceRuntimeTick"
          nullable: true
        runtimeProbeSuccessStreak:
          type: integer
          nullable: true
        runtimeProbeFailureStreak:
          type: integer
          nullable: true
        ruleName:
          type: string
          nullable: true
        ruleScope:
          type: string
          nullable: true
        clientFallbackDisabled:
          type: boolean
        overrideApplied:
          type: boolean

    PlaybackTraceRuntimeReplay:
      type: object
      additionalProperties: false
      properties:
        metadata:
          $ref: "#/components/schemas/PlaybackTraceRuntimeReplayMetadata"
        initialState:
          $ref: "#/components/schemas/PlaybackTraceRuntimeReplayState"
        finalState:
          $ref: "#/components/schemas/PlaybackTraceRuntimeReplayState"
        ticks:
          type: array
          items:
            $ref: "#/components/schemas/PlaybackTraceRuntimeReplayTick"
          nullable: true

    PlaybackTraceRuntimeReplayMetadata:
      type: object
      additionalProperties: false
      properties:
        sessionId:
          type: string
          nullable: true
        serviceRef:
          type: string
          nullable: true
        clientPath:
          type: string
          nullable: true
        sourceType:
          type: string
          nullable: true
        initialTarget:
          type: string
          nullable: true

    PlaybackTraceRuntimeReplayState:
      type: object
      additionalProperties: false
      properties:
        confidenceScore:
          type: integer
          nullable: true
        confidenceState:
          type: string
          nullable: true
        cooldownUntil:
          type: string
          format: date-time
          nullable: true
        currentStep:
          type: string
          nullable: true
        lastAction:
          type: string
          nullable: true
        policyConstraints:
          type: array
          items:
            type: string
          nullable: true
        probeState:
          type: string
          nullable: true
        probeStep:
          type: string
          nullable: true
        reasons:
          type: array
          items:
            type: string
          nullable: true
        targetStep:
          type: string
          nullable: true

    PlaybackTraceRuntimeReplayTick:
      type: object
      additionalProperties: false
      properties:
        input:
          $ref: "#/components/schemas/PlaybackTraceRuntimeReplayTickInput"
        expected:
          $ref: "#/components/schemas/PlaybackTraceRuntimeReplayTickExpected"

    PlaybackTraceRuntimeReplayTickInput:
      type: object
      additionalProperties: false
      required: [tickAt]
      properties:
        confidence:
          $ref: "#/components/schemas/PlaybackTraceRuntimeReplayTickInputConfidence"
        observedStep:
          type: string
          nullable: true
        targetStep:
          type: string
          nullable: true
        tickAt:
          type: string
          format: date-time

    PlaybackTraceRuntimeReplayTickInputConfidence:
      type: object
      additionalProperties: false
      properties:
        score:
          type: integer
          nullable: true
        state:
          type: string
          nullable: true
        stateSince:
          type: string
          format: date-time
          nullable: true
        cooldownUntil:
          type: string
          format: date-time
          nullable: true
        policyConstraints:
          type: array
          items:
            type: string
          nullable: true
        reasons:
          type: array
          items:
            type: string
          nullable: true
        windowCount:
          type: integer
          nullable: true

    PlaybackTraceRuntimeReplayTickExpected:
      type: object
      additionalProperties: false
      properties:
        action:
          type: string
          nullable: true
        activeStep:
          type: string
          nullable: true
        blockers:
          type: array
          items:
            type: string
          nullable: true
        plannedTransition:
          type: string
          nullable: true
        executedTransition:
          type: string
          nullable: true
        probeStep:
          type: string
          nullable: true
        probeState:
          type: string
          nullable: true
        reasons:
          type: array
          items:
            type: string
          nullable: true
        runtimePhase:
          type: string
          nullable: true

    PlaybackTraceRuntimeTick:
      type: object
      additionalProperties: false
      required: [tickAt]
      properties:
        tickAt:
          type: string
          format: date-time
        confidenceScore:
          type: integer
          nullable: true
        confidenceState:
          type: string
          nullable: true
        policyAction:
          type: string
          nullable: true
        plannedTransition:
          type: string
          nullable: true
        executedTransition:
          type: string
          nullable: true
        activeStep:
          type: string
          nullable: true
        targetStep:
          type: string
          nullable: true
        probeStep:
          type: string
          nullable: true
        probeState:
          type: string
          nullable: true
        cooldownUntil:
          type: string
          format: date-time
          nullable: true
        blockers:
          type: array
          items:
            type: string
          nullable: true
        reasons:
          type: array
          items:
            type: string
          nullable: true

    PlaybackTraceFfmpegPlan:
      type: object
      additionalProperties: false
      properties:
        inputKind:
          type: string
        container:
          type: string
        packaging:
          type: string
        hwAccel:
          type: string
        videoMode:
          type: string
        videoCodec:
          type: string
        audioMode:
          type: string
        audioCodec:
          type: string

    PlaybackTraceRuntimeDiagnostics:
      type: object
      additionalProperties: false
      properties:
        frameCount:
          type: integer
        fps:
          type: number
        dropFrames:
          type: integer
        dupFrames:
          type: integer
        speed:
          type: number
        corruptDecodedFrames:
          type: integer
        lastWarning:
          type: string
        updatedAtUnix:
          type: integer

    PlaybackSourceProfile:
      type: object
      additionalProperties: false
      properties:
        container:
          type: string
        videoCodec:
          type: string
        audioCodec:
          type: string
        bitrateKbps:
          type: integer
        width:
          type: integer
        height:
          type: integer
        fps:
          type: number
        interlaced:
          type: boolean
        audioChannels:
          type: integer
        audioBitrateKbps:
          type: integer

    PlaybackTargetProfile:
      type: object
      required: [container, packaging, video, audio, hls, hwAccel]
      additionalProperties: false
      properties:
        container:
          type: string
        packaging:
          type: string
        video:
          $ref: "#/components/schemas/PlaybackTargetVideo"
        audio:
          $ref: "#/components/schemas/PlaybackTargetAudio"
        hls:
          $ref: "#/components/schemas/PlaybackTargetHls"
        hwAccel:
          type: string

    PlaybackTargetAudio:
      type: object
      required: [mode, codec, channels, bitrateKbps, sampleRate]
      additionalProperties: false
      properties:
        mode:
          type: string
        codec:
          type: string
        channels:
          type: integer
        bitrateKbps:
          type: integer
        sampleRate:
          type: integer

    PlaybackTargetHls:
      type: object
      required: [enabled, segmentContainer, segmentSeconds]
      additionalProperties: false
      properties:
        enabled:
          type: boolean
        segmentContainer:
          type: string
        segmentSeconds:
          type: integer

    PlaybackTargetVideo:
      type: object
      required: [mode, codec, width, height, fps]
      additionalProperties: false
      properties:
        mode:
          type: string
        codec:
          type: string
        crf:
          type: integer
        preset:
          type: string
        width:
          type: integer
        height:
          type: integer
        fps:
          type: number

    PlaybackOutput:
      description: Output URL for client playback
      oneOf:
        - $ref: "#/components/schemas/PlaybackOutputFile"
        - $ref: "#/components/schemas/PlaybackOutputHls"

    PlaybackOutputFile:
      type: object
      required: [kind, url]
      additionalProperties: false
      properties:
        kind:
          type: string
          enum: [file]
          description: Static file output
        url:
          type: string
          format: uri
          description: Direct playback URL

    PlaybackOutputHls:
      type: object
      required: [kind, playlistUrl]
      additionalProperties: false
      properties:
        kind:
          type: string
          enum: [hls]
          description: HLS stream output
        playlistUrl:
          type: string
          format: uri
          description: Canonical HLS playlist URL (.m3u8)
        url:
          type: string
          format: uri
          description: Alternate playback URL (optional)
          example: "/api/v3/recordings/rec:xyz/stream.mp4"

    # ==================== RFC7807 Problem Details (P4-1) ====================

    ProblemCapabilitiesMissing:
      allOf:
        - $ref: "#/components/schemas/ProblemDetails"
        - type: object
          properties:
            code:
              type: string
              enum: [capabilities_missing]
          example:
            type: "recordings/capabilities-missing"
            title: "Capabilities Missing"
            status: 412
            code: "capabilities_missing"
            detail: "Client must provide capabilities (capabilitiesVersion required)"

    ProblemCapabilitiesInvalid:
      allOf:
        - $ref: "#/components/schemas/ProblemDetails"
        - type: object
          properties:
            code:
              type: string
              enum: [capabilities_invalid]
          example:
            type: "recordings/capabilities-invalid"
            title: "Capabilities Invalid"
            status: 400
            code: "capabilities_invalid"
            detail: "capabilitiesVersion 999 not supported (current: 1)"

    ProblemDecisionAmbiguous:
      allOf:
        - $ref: "#/components/schemas/ProblemDetails"
        - type: object
          properties:
            code:
              type: string
              enum: [decision_ambiguous]
          example:
            type: "recordings/decision-ambiguous"
            title: "Decision Ambiguous"
            status: 422
            code: "decision_ambiguous"
            detail: "No compatible playback path available"

    LivePlaybackTruthProblem:
      type: object
      required: [type, title, status, requestId, code, retryAfterSeconds, truthState, truthReason]
      additionalProperties: false
      properties:
        type:
          type: string
          enum:
            - /problems/live/scan_unavailable
            - /problems/live/missing_scan_truth
            - /problems/live/stale_truth
            - /problems/live/partial_truth
            - /problems/live/inactive_event_feed
            - /problems/live/failed_scan_truth
          description: Stable live-truth problem type. Clients MUST branch on this field, not on free-text title/detail.
        title:
          type: string
          description: Human-readable fallback title. Not for decision branching.
        status:
          type: integer
          enum: [503]
        requestId:
          type: string
          description: Correlation ID (UUID or prefixed string like req_abc123)
        code:
          $ref: "#/components/schemas/ProblemCode"
        detail:
          type: string
          description: Optional human-readable detail. Not for decision branching.
        instance:
          type: string
        retryAfterSeconds:
          type: integer
          minimum: 1
          description: JSON mirror of the Retry-After header.
        truthState:
          type: string
          enum: [unverified, partial, failed, inactive_event_feed]
          description: Stable degraded-state classifier for live truth.
        truthReason:
          type: string
          enum:
            - scanner_unavailable
            - missing_scan_truth
            - stale_scan_truth
            - partial_scan_truth
            - inactive_event_feed
            - failed_scan_truth
          description: Stable machine-readable reason for the degraded live truth state.
        truthOrigin:
          type: string
          enum: [live_unverified]
          description: Diagnostic provenance only. Clients SHOULD NOT branch on this field.
        problemFlags:
          type: array
          description: Diagnostic flags only. Clients MUST NOT use these as the sole UX branch.
          items:
            type: string
      example:
        type: /problems/live/stale_truth
        title: Live media truth stale
        status: 503
        requestId: req_live_123
        code: UNAVAILABLE
        detail: Live media truth is stale
        retryAfterSeconds: 5
        truthState: unverified
        truthReason: stale_scan_truth
        truthOrigin: live_unverified
        problemFlags:
          - live_truth_unverified
          - stale_scan_truth

    SessionTerminalProblem:
      type: object
      required: [type, title, status, requestId, session, state, reason_detail]
      additionalProperties: false
      properties:
        type: { type: string }
        title: { type: string }
        status: { type: integer }
        requestId:
          type: string
          description: Correlation ID (UUID or prefixed string like req_abc123)
        code:
          $ref: "#/components/schemas/ProblemCode"
        detail: { type: string }
        instance: { type: string }
        session:
          type: string
          format: uuid
        state:
          type: string
          enum: [STOPPED, FAILED, CANCELLED]
        reason:
          type: string
        reason_detail:
          type: string
        trace:
          $ref: "#/components/schemas/PlaybackTrace"
      example:
        type: "/problems/error/transcode_stalled"
        title: "Transcode stalled - no progress detected"
        status: 410
        requestId: "req_abc123"
        code: "TRANSCODE_STALLED"
        detail: "The session failed because the transcode process stopped producing progress."
        session: "550e8400-e29b-41d4-a716-446655440000"
        state: "FAILED"
        reason: "R_PROCESS_ENDED"
        reason_detail: "transcode stalled - no progress detected"

    TimerCreateRequest:
      type: object
      required: [serviceRef, begin, end, name]
      properties:
        serviceRef: { type: string }
        begin: { type: integer, format: int64 }
        end: { type: integer, format: int64 }
        name: { type: string }
        description: { type: string }
        enabled: { type: boolean, default: true }
        justPlay: { type: boolean, default: false }
        afterEvent:
          type: string
          enum: [default, standby, deepstandby, nothing]
          default: default
        paddingBeforeSec: { type: integer, default: 0 }
        paddingAfterSec: { type: integer, default: 0 }
        idempotencyKey: { type: string }

    TimerPatchRequest:
      type: object
      properties:
        begin: { type: integer, format: int64 }
        end: { type: integer, format: int64 }
        name: { type: string }
        description: { type: string }
        enabled: { type: boolean }
        paddingBeforeSec: { type: integer }
        paddingAfterSec: { type: integer }

    TimerConflict:
      type: object
      required: [type, blockingTimer]
      properties:
        type:
          type: string
          enum: [overlap, duplicate, tuner_limit, unknown]
        blockingTimer: { $ref: "#/components/schemas/Timer" }
        overlapSeconds: { type: integer }
        message: { type: string }

    TimerConflictPreviewRequest:
      type: object
      required: [proposed]
      properties:
        proposed:
          $ref: "#/components/schemas/TimerCreateRequest"
        mode:
          type: string
          enum: [conservative, receiverAware]
          default: conservative

    TimerConflictPreviewResponse:
      type: object
      required: [canSchedule, conflicts]
      properties:
        canSchedule: { type: boolean }
        conflicts:
          type: array
          items: { $ref: "#/components/schemas/TimerConflict" }
        suggestions:
          type: array
          items:
            type: object
            properties:
              kind:
                type: string
                enum:
                  - reduce_padding
                  - shift_start
                  - shift_end
              proposedBegin: { type: integer, format: int64 }
              proposedEnd: { type: integer, format: int64 }
              note: { type: string }

    DvrCapabilities:
      type: object
      required: [timers, conflicts, series]
      properties:
        timers:
          type: object
          properties:
            edit: { type: boolean }
            delete: { type: boolean }
            readBackVerify: { type: boolean }
        conflicts:
          type: object
          properties:
            preview: { type: boolean }
            receiverAware: { type: boolean }
        series:
          type: object
          properties:
            supported: { type: boolean }
            mode: { type: string, enum: [none, delegated, managed] }
            delegatedProvider: { type: string }

    RecordingStatus:
      type: object
      required: [isRecording]
      properties:
        isRecording: { type: boolean }
        serviceName: { type: string }

    RecordingBuildStatus:
      type: object
      required: [state, requestId]
      properties:
        requestId:
          type: string
          description: Correlation ID for the request.
        state:
          type: string
          enum: [IDLE, RUNNING, FAILED, READY]
        segmentCount: { type: integer }
        lastProgress: { type: string, format: date-time }
        startedAt: { type: string, format: date-time }
        attemptMode: { type: string, enum: [fast, robust] }
        error: { type: string }
        progressiveReady:
          type: boolean
          description: True if a progressive (timeshift) playlist is playable.

    TimerList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: "#/components/schemas/Timer" }

    LogEntry:
      type: object
      properties:
        time:
          type: string
          format: date-time
        level:
          type: string
        message:
          type: string
        fields:
          type: object
          additionalProperties: true

    StreamSession:
      type: object
      required: [sessionId, requestId, state]
      properties:
        id:
          type: string
          description: Internal database ID (deprecated).
        sessionId:
          type: string
          format: uuid
          description: Mandatory stream lifecycle ID (domain truth).
        requestId:
          type: string
          description: Correlation ID for the trace. Mandatory in P3-1.
        clientIp:
          type: string
        clientFamily:
          type: string
          description: Coarse browser/player family reported by the client (e.g. chromium_hlsjs).
        client:
          $ref: "#/components/schemas/PlaybackClientSummary"
        channelName:
          type: string
        preferredHlsEngine:
          type: string
          description: Preferred HLS playback engine reported by the client (e.g. native, hlsjs).
        deviceType:
          type: string
          description: Optional client device category reported during stream startup.
        startedAt:
          type: string
          format: date-time
        state:
          type: string
          enum: [starting, buffering, active, stalled, ending, idle, error]
        detailedState:
          type: string
          description: Fine-grained diagnostic state for running sessions.
          enum: [starting, priming, buffering, active, stalled, ending, idle, error]
        program:
          type: object
          properties:
            title: { type: string }
            description: { type: string }
            beginTimestamp: { type: integer, format: int64 }
            durationSec: { type: integer }

    RecordingRoot:
      type: object
      properties:
        id: { type: string }
        name: { type: string }

    Breadcrumb:
      type: object
      properties:
        name: { type: string }
        path: { type: string }

    DirectoryItem:
      type: object
      properties:
        name: { type: string }
        path: { type: string }

    RecordingItem:
      type: object
      required: [status]
      properties:
        serviceRef:
          type: string
          description: Legacy receiver service reference (read-only).
        recordingId:
          type: string
          description: Base64url-encoded recording ID (RFC 4648, unpadded) to use for /recordings/{recordingId}.
          pattern: "^[A-Za-z0-9_-]+$"
          minLength: 16
          maxLength: 2048
          example: "MTowOjA6MDo6L21lZGlhL2hkZC9tb3ZpZS9mb28udHM"
        title: { type: string }
        description: { type: string }
        beginUnixSeconds:
          type: integer
          format: int64
          description: Recording start time as UNIX seconds.
        durationSeconds:
          type: integer
          format: int64
          description: Recording duration in seconds, if known.
        length:
          type: string
          description: Human-readable duration string for display only.
        filename: { type: string }
        localWritable:
          type: boolean
          description: Whether the current runtime can rename this recording via a writable locally mapped filesystem path. Clients MUST fail closed when this field is absent or false.
        status:
          type: string
          description: Authoritative coarse-grained recording truth from the backend domain model. `unknown` means there is currently no confirmed recording truth; clients may react to that truth gap, but MUST NOT infer sub-causes from it.
          enum: [scheduled, recording, completed, failed, unknown]
        resume: { $ref: "#/components/schemas/ResumeSummary" }

    ResumeSummary:
      type: object
      required: [posSeconds]
      properties:
        posSeconds:
          type: integer
          format: int64
        durationSeconds:
          type: integer
          format: int64
        finished: { type: boolean }
        updatedAt: { type: string, format: date-time }

    RecordingResponse:
      type: object
      required: [requestId]
      properties:
        requestId:
          type: string
          description: Correlation ID for the request.
        roots:
          type: array
          items: { $ref: "#/components/schemas/RecordingRoot" }
        currentRoot: { type: string }
        currentPath: { type: string }
        breadcrumbs:
          type: array
          items: { $ref: "#/components/schemas/Breadcrumb" }
        directories:
          type: array
          items: { $ref: "#/components/schemas/DirectoryItem" }
        recordings:
          type: array
          items: { $ref: "#/components/schemas/RecordingItem" }

security:
  - BearerAuth: [v3:read]

paths:
  /errors:
    get:
      summary: List documented problem codes
      operationId: getErrors
      tags:
        - v3
      responses:
        "200":
          description: Registry-backed public error catalog
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorCatalogResponse"

  /system/health:
    get:
      summary: Get system health
      operationId: getSystemHealth
      tags:
        - system
      responses:
        "200":
          description: Health status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SystemHealth"

  /system/healthz:
    get:
      summary: Get minimal system health
      operationId: getSystemHealthz
      tags:
        - system
      security: []
      responses:
        "200":
          description: Health status
          content:
            application/json:
              schema:
                type: object
                required: [status]
                additionalProperties: false
                properties:
                  status:
                    type: string
                    enum: [ok]

  /system/connectivity:
    get:
      summary: Get effective public deployment contract diagnostics
      operationId: getSystemConnectivity
      tags:
        - system
      responses:
        "200":
          description: Connectivity contract diagnostics
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ConnectivityContract"

  /household/profiles:
    get:
      summary: List household profiles
      operationId: getHouseholdProfiles
      tags:
        - household
      parameters:
        - $ref: "#/components/parameters/HouseholdProfileHeader"
      responses:
        "200":
          description: Household profiles
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/HouseholdProfile"
    post:
      summary: Create household profile
      operationId: postHouseholdProfiles
      tags:
        - household
      security:
        - BearerAuth: [v3:admin]
      parameters:
        - $ref: "#/components/parameters/HouseholdProfileHeader"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/HouseholdProfile"
      responses:
        "201":
          description: Household profile created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HouseholdProfile"
        "400":
          description: Invalid household profile
        "403":
          description: Forbidden
        "409":
          description: Profile already exists

  /household/unlock:
    get:
      summary: Get household unlock status
      operationId: getHouseholdUnlock
      tags:
        - household
      parameters:
        - $ref: "#/components/parameters/HouseholdProfileHeader"
      responses:
        "200":
          description: Household unlock status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HouseholdUnlockStatus"
    post:
      summary: Unlock protected household profiles
      operationId: postHouseholdUnlock
      tags:
        - household
      security:
        - BearerAuth: [v3:read]
      parameters:
        - $ref: "#/components/parameters/HouseholdProfileHeader"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/HouseholdUnlockRequest"
      responses:
        "200":
          description: Household unlock granted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HouseholdUnlockStatus"
        "400":
          description: Invalid PIN payload
        "403":
          description: Invalid PIN
        "409":
          description: No household PIN configured
    delete:
      summary: Clear household unlock state
      operationId: deleteHouseholdUnlock
      tags:
        - household
      security:
        - BearerAuth: [v3:read]
      parameters:
        - $ref: "#/components/parameters/HouseholdProfileHeader"
      responses:
        "204":
          description: Household unlock cleared

  /household/profiles/{profileId}:
    parameters:
      - name: profileId
        in: path
        required: true
        schema:
          type: string
    put:
      summary: Update household profile
      operationId: putHouseholdProfile
      tags:
        - household
      security:
        - BearerAuth: [v3:admin]
      parameters:
        - $ref: "#/components/parameters/HouseholdProfileHeader"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/HouseholdProfile"
      responses:
        "200":
          description: Household profile updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HouseholdProfile"
        "400":
          description: Invalid household profile
        "403":
          description: Forbidden
        "404":
          description: Profile not found
    delete:
      summary: Delete household profile
      operationId: deleteHouseholdProfile
      tags:
        - household
      security:
        - BearerAuth: [v3:admin]
      parameters:
        - $ref: "#/components/parameters/HouseholdProfileHeader"
      responses:
        "204":
          description: Household profile deleted
        "403":
          description: Forbidden
        "404":
          description: Profile not found
        "409":
          description: Cannot delete the last profile

  /system/config:
    get:
      summary: Get system configuration
      operationId: getSystemConfig
      tags:
        - system
      security:
        - BearerAuth: [v3:admin]
      responses:
        "200":
          description: Configuration
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AppConfig"
    put:
      summary: Update system configuration
      operationId: putSystemConfig
      tags:
        - system
      security:
        - BearerAuth: [v3:admin]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ConfigUpdate"
      responses:
        "200":
          description: Configuration updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  restartRequired:
                    type: boolean
        "400":
          description: Invalid configuration
        "500":
          description: Failed to save configuration

  /system/entitlements:
    get:
      summary: Get current principal entitlement status
      operationId: getSystemEntitlements
      tags:
        - system
      security:
        - BearerAuth: [v3:read]
      parameters:
        - in: query
          name: principalId
          required: false
          description: Inspect another principal when authenticated with admin scope.
          schema:
            type: string
      responses:
        "200":
          description: Current entitlement status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EntitlementStatus"

  /system/entitlements/receipts:
    post:
      summary: Verify a purchase receipt and apply matching entitlements
      operationId: postSystemEntitlementReceipt
      tags:
        - system
      security:
        - BearerAuth: [v3:read]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EntitlementReceiptRequest"
      responses:
        "200":
          description: Receipt verified and entitlements updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EntitlementReceiptResponse"
        "400":
          description: Invalid receipt submission
        "502":
          description: Upstream verification failed
        "503":
          description: Receipt verification unavailable

  /system/entitlements/overrides:
    post:
      summary: Grant admin entitlement overrides
      operationId: postSystemEntitlementOverride
      tags:
        - system
      security:
        - BearerAuth: [v3:admin]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EntitlementOverrideRequest"
      responses:
        "204":
          description: Override granted
        "400":
          description: Invalid override request

  /system/entitlements/overrides/{principalId}/{scope}:
    delete:
      summary: Revoke an admin entitlement override
      operationId: deleteSystemEntitlementOverride
      tags:
        - system
      security:
        - BearerAuth: [v3:admin]
      parameters:
        - in: path
          name: principalId
          required: true
          schema:
            type: string
        - in: path
          name: scope
          required: true
          schema:
            type: string
      responses:
        "204":
          description: Override revoked
        "400":
          description: Invalid revoke request

  /receiver/current:
    get:
      summary: Get current service and EPG
      operationId: getReceiverCurrent
      description: Returns the currently playing service on the receiver with EPG information
      tags:
        - receiver
      security:
        - BearerAuth: [v3:read]
      responses:
        "200":
          description: Current service information
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CurrentServiceInfo"
        "502":
          description: Failed to query receiver
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "503":
          description: Receiver client unavailable
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"

  /system/info:
    get:
      summary: Get comprehensive system information
      operationId: getSystemInfo
      tags:
        - system
      security:
        - BearerAuth: [v3:read]
      responses:
        "200":
          description: System information
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SystemInfoData"

  /system/refresh:
    post:
      summary: Trigger data refresh (EPG/Channels)
      operationId: postSystemRefresh
      tags:
        - system
      security:
        - BearerAuth: [v3:write]
      responses:
        "202":
          description: Refresh started
        "409":
          description: Refresh already in progress

  /services/bouquets:
    get:
      summary: List all bouquets
      operationId: getServicesBouquets
      tags:
        - services
      responses:
        "200":
          description: List of bouquets
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Bouquet"

  /services/now-next:
    post:
      summary: Get now/next EPG for a list of services
      operationId: postServicesNowNext
      tags:
        - services
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/NowNextRequest"
      responses:
        "200":
          description: Now/next EPG entries per service
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NowNextResponse"
        "400":
          description: Invalid request

  /epg:
    get:
      summary: Get EPG data
      operationId: getEpg
      tags:
        - epg
      parameters:
        - $ref: "#/components/parameters/HouseholdProfileHeader"
        - name: from
          in: query
          schema:
            type: integer
          description: Start timestamp (unix seconds)
        - name: to
          in: query
          schema:
            type: integer
          description: End timestamp (unix seconds)
        - name: bouquet
          in: query
          schema:
            type: string
          description: Filter by bouquet name
        - name: q
          in: query
          schema:
            type: string
          description: Filter by search query
      responses:
        "200":
          description: EPG data
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  additionalProperties: false
                  properties:
                    id:
                      type: string
                    serviceRef:
                      type: string
                    title:
                      type: string
                    desc:
                      type: string
                    start:
                      type: integer
                    end:
                      type: integer
                    duration:
                      type: integer
                    startXmltv:
                      type: string
                      description: Original XMLTV start timestamp including offset
                    endXmltv:
                      type: string
                      description: Original XMLTV end timestamp including offset

  /services:
    get:
      summary: List all services (channels)
      operationId: getServices
      tags:
        - services
      parameters:
        - $ref: "#/components/parameters/HouseholdProfileHeader"
        - in: query
          name: bouquet
          schema:
            type: string
          description: Filter by bouquet name
      responses:
        "200":
          description: List of services
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Service"

  /services/{id}/toggle:
    post:
      summary: Toggle service enabled state
      operationId: postServicesIdToggle
      tags:
        - services
      security:
        - BearerAuth: [v3:write]
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                enabled:
                  type: boolean
      responses:
        "200":
          description: Status updated
        "404":
          description: Service not found

  /streams:
    get:
      summary: List active streams
      operationId: getStreams
      tags:
        - streams
      security:
        - BearerAuth: [v3:admin]
      responses:
        "200":
          description: Active sessions
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/StreamSession"

  /live/stream-info:
    post:
      summary: Get playback decision and token for a live stream
      operationId: postLivePlaybackInfo
      tags: [streams]
      security:
        - BearerAuth: [v3:read]
      requestBody:
        description: Client capabilities and service reference for decision making
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [serviceRef, capabilities]
              properties:
                serviceRef:
                  type: string
                  description: The Enigma2 service reference
                capabilities:
                  $ref: "#/components/schemas/PlaybackCapabilities"
      responses:
        "200":
          description: Playback decision and info
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlaybackInfo"
        "400":
          description: Invalid capabilities or service reference
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "401":
          description: Unauthorized
        "403":
          description: Forbidden
        "503":
          description: Live playback unavailable. Responses with `/problems/live/*` indicate unverified or degraded live media truth and MUST NOT be treated as confirmed playback readiness.
          headers:
            Retry-After:
              description: Seconds to wait before retrying a degraded or unverified live truth response.
              schema:
                type: string
                example: "5"
          content:
            application/problem+json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/LivePlaybackTruthProblem"
                  - $ref: "#/components/schemas/ProblemDetails"

  /recordings:
    get:
      summary: Browse recordings
      operationId: getRecordings
      tags:
        - recordings
      parameters:
        - $ref: "#/components/parameters/HouseholdProfileHeader"
        - name: root
          in: query
          schema:
            type: string
          description: Root location ID
        - name: path
          in: query
          schema:
            type: string
          description: Relative path
      responses:
        "200":
          description: Recordings list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RecordingResponse"

  /recordings/{recordingId}:
    delete:
      summary: Delete a recording
      description: Deletes the recording via OpenWebIF on the receiver.
      security:
        - BearerAuth: [v3:write]
      operationId: deleteRecording
      tags:
        - recordings
      parameters:
        - name: recordingId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]+$"
            minLength: 16
            maxLength: 2048
            example: "MTowOjA6MDo6L21lZGlhL2hkZC9tb3ZpZS9mb28udHM"
          description: Base64url-encoded recording ID (RFC 4648, unpadded) from RecordingItem.recordingId
      responses:
        "204":
          description: Recording deleted
        "400":
          description: Invalid recording reference
        "403":
          description: Access denied
        "404":
          description: Recording not found
        "500":
          description: Failed to delete recording

  /recordings/{recordingId}/delete:
    post:
      summary: Delete a recording
      description: Compatibility POST endpoint for clients that cannot issue DELETE requests. Deletes the recording via OpenWebIF on the receiver or via the local recording mapping when available.
      security:
        - BearerAuth: [v3:write]
      operationId: postRecordingDelete
      tags:
        - recordings
      parameters:
        - name: recordingId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]+$"
            minLength: 16
            maxLength: 2048
          description: Base64url-encoded recording ID (RFC 4648, unpadded) from RecordingItem.recordingId
      responses:
        "204":
          description: Recording deleted
        "400":
          description: Invalid recording reference
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "403":
          description: Access denied
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "404":
          description: Recording not found
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "500":
          description: Failed to delete recording
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"

  /recordings/{recordingId}/rename:
    post:
      summary: Rename a recording
      description: Renames a locally mapped recording and its sidecar artifacts.
      security:
        - BearerAuth: [v3:write]
      operationId: postRecordingRename
      tags:
        - recordings
      parameters:
        - name: recordingId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]+$"
            minLength: 16
            maxLength: 2048
          description: Base64url-encoded recording ID (RFC 4648, unpadded) from RecordingItem.recordingId
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - title
              properties:
                title:
                  type: string
                  minLength: 1
                  maxLength: 255
      responses:
        "204":
          description: Recording renamed
        "400":
          description: Invalid request
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "403":
          description: Access denied
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "404":
          description: Recording not found
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "422":
          description: Rename unsupported for this recording
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "500":
          description: Failed to rename recording
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"

  /recordings/{recordingId}/thumbnail.jpg:
    get:
      summary: Get a recording thumbnail
      description: Returns a cached JPEG thumbnail for a locally mapped recording, generating it on demand when possible.
      security:
        - BearerAuth: [v3:read]
      operationId: getRecordingThumbnail
      tags:
        - recordings
      parameters:
        - name: recordingId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]+$"
            minLength: 16
            maxLength: 2048
          description: Base64url-encoded recording ID (RFC 4648, unpadded) from RecordingItem.recordingId
      responses:
        "200":
          description: JPEG thumbnail
          headers:
            Cache-Control:
              schema:
                type: string
              description: Private thumbnail cache policy
          content:
            image/jpeg:
              schema:
                type: string
                format: binary
        "400":
          description: Invalid recording reference
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "403":
          description: Access denied
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "404":
          description: Recording thumbnail not found
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "500":
          description: Thumbnail cache unavailable
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"

  /recordings/{recordingId}/status:
    get:
      summary: Get recording build status
      operationId: getRecordingsRecordingIdStatus
      tags:
        - recordings
      parameters:
        - in: path
          name: recordingId
          schema:
            type: string
          required: true
      responses:
        "200":
          description: Build status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RecordingBuildStatus"
        "400":
          description: Invalid recording ID
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/APIError"

  /recordings/{recordingId}/stream-info:
    get:
      summary: Get playback strategy for a recording (Legacy/Anonymous)
      operationId: getRecordingPlaybackInfo
      tags: [recordings]
      parameters:
        - name: recordingId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Playback strategy info
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlaybackInfo"
        "404":
          description: Recording not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/APIError"
        "503":
          description: Preparing (media truth unknown, retryable)
          headers:
            Retry-After:
              description: Seconds to wait before retry
              schema:
                type: string
                example: "5"
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
    post:
      summary: Get playback decision with client capabilities (v3.1)
      operationId: postRecordingPlaybackInfo
      tags: [recordings]
      parameters:
        - name: recordingId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        description: Client capabilities for decision making
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PlaybackCapabilities"
      responses:
        "200":
          description: Playback decision and info
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlaybackInfo"
        "400":
          description: Invalid capabilities
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemCapabilitiesInvalid"
        "412":
          description: Capabilities missing (required for v3.1+)
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemCapabilitiesMissing"
        "422":
          description: Decision ambiguous (media truth unknown)
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDecisionAmbiguous"
        "404":
          description: Recording not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/APIError"
        "503":
          description: Preparing (media truth unknown, retryable)
          headers:
            Retry-After:
              description: Seconds to wait before retry
              schema:
                type: string
                example: "5"
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"

  /recordings/{recordingId}/stream.mp4:
    get:
      summary: Stream recording as MP4 (Direct VOD)
      operationId: streamRecordingDirect
      tags: [recordings]
      parameters:
        - name: recordingId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Video stream
          content:
            video/mp4:
              schema:
                type: string
                format: binary
        "404":
          description: Recording not found
        "503":
          description: Preparing (retryable)
          headers:
            Retry-After:
              description: Seconds to wait before retry
              schema:
                type: string
                example: "5"
    head:
      summary: Probe recording availability (Direct VOD)
      operationId: probeRecordingMp4
      tags: [recordings]
      parameters:
        - name: recordingId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Available
        "404":
          description: Not found
        "503":
          description: Preparing (retryable)
          headers:
            Retry-After:
              description: Seconds to wait before retry
              schema:
                type: string
                example: "5"

  /recordings/{recordingId}/playlist.m3u8:
    get:
      summary: Get VOD HLS playlist for a recording
      operationId: getRecordingHLSPlaylist
      tags:
        - recordings
      parameters:
        - name: recordingId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]+$"
            minLength: 16
            maxLength: 2048
            example: "MTowOjA6MDo6L21lZGlhL2hkZC9tb3ZpZS9mb28udHM"
          description: Base64url-encoded recording ID (RFC 4648, unpadded) from RecordingItem.recordingId
      responses:
        "200":
          description: HLS Playlist
          content:
            application/vnd.apple.mpegurl:
              schema:
                type: string
            application/x-mpegURL:
              schema:
                type: string
        "400":
          description: Invalid recording ID
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/APIError"
        "404":
          description: Recording not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/APIError"
        "503":
          description: Recording not ready for VOD
          headers:
            Retry-After:
              description: Seconds to wait before retry
              schema:
                type: string
                example: "5"
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
    head:
      summary: Get VOD HLS playlist metadata (Safari compatibility)
      operationId: getRecordingHLSPlaylistHead
      tags:
        - recordings
      parameters:
        - name: recordingId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]+$"
            minLength: 16
            maxLength: 2048
      responses:
        "200":
          description: HLS playlist metadata
          headers:
            Content-Type:
              schema:
                type: string
            Content-Length:
              schema:
                type: integer
        "404":
          description: Recording not found

  /recordings/{recordingId}/timeshift.m3u8:
    get:
      summary: Get timeshift HLS playlist for a recording
      operationId: getRecordingHLSTimeshift
      tags:
        - recordings
      parameters:
        - name: recordingId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]+$"
            minLength: 16
            maxLength: 2048
            example: "MTowOjA6MDo6L21lZGlhL2hkZC9tb3ZpZS9mb28udHM"
          description: Base64url-encoded recording ID (RFC 4648, unpadded) from RecordingItem.recordingId
      responses:
        "200":
          description: HLS Playlist
          content:
            application/vnd.apple.mpegurl:
              schema:
                type: string
            application/x-mpegURL:
              schema:
                type: string
        "400":
          description: Invalid recording ID
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/APIError"
        "404":
          description: Recording not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/APIError"
        "503":
          description: Recording not ready for timeshift
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/APIError"
    head:
      summary: Get timeshift HLS playlist metadata (Safari compatibility)
      operationId: getRecordingHLSTimeshiftHead
      tags:
        - recordings
      parameters:
        - name: recordingId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]+$"
            minLength: 16
            maxLength: 2048
      responses:
        "200":
          description: HLS playlist metadata
          headers:
            Content-Type:
              schema:
                type: string
            Content-Length:
              schema:
                type: integer
        "404":
          description: Recording not found

  /recordings/{recordingId}/{segment}:
    get:
      summary: Get HLS segment for a recording
      operationId: getRecordingHLSCustomSegment
      tags:
        - recordings
      parameters:
        - name: recordingId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]+$"
            minLength: 16
            maxLength: 2048
            example: "MTowOjA6MDo6L21lZGlhL2hkZC9tb3ZpZS9mb28udHM"
          description: Base64url-encoded recording ID (RFC 4648, unpadded) from RecordingItem.recordingId
        - name: segment
          in: path
          required: true
          schema:
            type: string
            pattern: '.*\.(ts|mp4|m4s|cmfv)$'
      responses:
        "200":
          description: Media Segment
          content:
            video/mp2t:
              schema:
                type: string
                format: binary
            video/MP2T:
              schema:
                type: string
                format: binary
            video/iso.segment:
              schema:
                type: string
                format: binary
            video/mp4:
              schema:
                type: string
                format: binary
            application/octet-stream:
              schema:
                type: string
                format: binary
        "400":
          description: Invalid recording ID
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/APIError"
        "404":
          description: Recording not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/APIError"
        "409":
          description: Recording not ready
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/APIError"
    head:
      summary: Get HLS segment metadata (Safari compatibility)
      operationId: getRecordingHLSCustomSegmentHead
      tags:
        - recordings
      parameters:
        - name: recordingId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]+$"
            minLength: 16
            maxLength: 2048
        - name: segment
          in: path
          required: true
          schema:
            type: string
            pattern: '.*\.(ts|mp4|m4s|cmfv)$'
      responses:
        "200":
          description: Segment metadata
          headers:
            Content-Type:
              schema:
                type: string
            Content-Length:
              schema:
                type: integer
        "404":
          description: Segment not found

  /auth/session:
    post:
      summary: Create session cookie
      description: Exchanges the Bearer token for a secure HttpOnly session cookie, enabling native playback (HLS) without token in URL. The exchange requires HTTPS or a trusted HTTPS proxy; plain HTTP is accepted only from loopback clients.
      operationId: createSession
      tags:
        - auth
      security:
        - BearerAuth: [v3:read]
      responses:
        "200":
          description: Session created
        "400":
          description: HTTPS required for session exchange
        "401":
          description: Unauthorized
    delete:
      summary: Delete auth session cookie
      operationId: deleteSession
      tags:
        - auth
      security:
        - BearerAuth: [v3:read]
      parameters:
        - $ref: "#/components/parameters/HouseholdProfileHeader"
      responses:
        "204":
          description: Session deleted
        "403":
          description: Household PIN required for child-profile logout

  /pairing/start:
    post:
      summary: Start device pairing enrollment
      operationId: StartPairing
      tags:
        - auth
      security: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/StartPairingRequest"
      responses:
        "201":
          description: Pairing enrollment created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StartPairingResponse"
        "400":
          description: Invalid pairing request
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "503":
          description: Pairing state store unavailable
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"

  /pairing/{pairingId}/status:
    post:
      summary: Read pairing enrollment status
      operationId: GetPairingStatus
      tags:
        - auth
      security: []
      parameters:
        - name: pairingId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PairingSecretRequest"
      responses:
        "200":
          description: Pairing enrollment status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PairingStatusResponse"
        "400":
          description: Invalid pairing request
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "403":
          description: Pairing secret mismatch
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "404":
          description: Pairing not found
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "410":
          description: Pairing has expired or was revoked
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "503":
          description: Pairing state store unavailable
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"

  /pairing/{pairingId}/approve:
    post:
      summary: Approve a pending pairing enrollment
      operationId: ApprovePairing
      tags:
        - auth
      security:
        - BearerAuth: [v3:admin]
      parameters:
        - name: pairingId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ApprovePairingRequest"
      responses:
        "200":
          description: Pairing enrollment approved
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApprovePairingResponse"
        "400":
          description: Invalid pairing approval request
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "401":
          description: Authentication required
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "404":
          description: Pairing not found
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "409":
          description: Pairing is still pending or otherwise cannot transition
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "410":
          description: Pairing has expired, been revoked, or was already exchanged
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "503":
          description: Pairing state store unavailable
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"

  /pairing/{pairingId}/exchange:
    post:
      summary: Exchange an approved pairing for device credentials
      operationId: ExchangePairing
      tags:
        - auth
      security: []
      parameters:
        - name: pairingId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PairingSecretRequest"
      responses:
        "200":
          description: Pairing exchanged for device grant and access session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ExchangePairingResponse"
        "400":
          description: Invalid pairing exchange request
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "403":
          description: Pairing secret mismatch
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "404":
          description: Pairing not found
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "409":
          description: Pairing is pending and not yet approved
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "410":
          description: Pairing has expired, been revoked, or was already exchanged
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "503":
          description: Pairing state store unavailable
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"

  /auth/device/session:
    post:
      summary: Refresh a device access session from a device grant
      operationId: CreateDeviceSession
      tags:
        - auth
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateDeviceSessionRequest"
      responses:
        "200":
          description: Device access session issued
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateDeviceSessionResponse"
        "400":
          description: Invalid device session request
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "401":
          description: Invalid or missing device grant secret
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "403":
          description: Device grant forbidden
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "404":
          description: Device grant not found
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "409":
          description: Device session conflict
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "410":
          description: Device grant expired or revoked
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "503":
          description: Device session store unavailable
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"

  /auth/web-bootstrap:
    post:
      summary: Create a one-time web bootstrap for an authenticated device session
      operationId: CreateWebBootstrap
      tags:
        - auth
      security:
        - BearerAuth: [v3:read]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateWebBootstrapRequest"
      responses:
        "201":
          description: Web bootstrap created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateWebBootstrapResponse"
        "400":
          description: Invalid web bootstrap request
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "401":
          description: Unauthorized
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "403":
          description: Web bootstrap forbidden
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "404":
          description: Source access session not found
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "409":
          description: Web bootstrap conflict
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "410":
          description: Source session expired or revoked
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "503":
          description: Web bootstrap store unavailable
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"

  /auth/web-bootstrap/{bootstrapId}:
    get:
      summary: Complete a one-time web bootstrap and mint a normal session cookie
      operationId: CompleteWebBootstrap
      tags:
        - auth
      security: []
      parameters:
        - name: bootstrapId
          in: path
          required: true
          schema:
            type: string
        - name: X-XG2G-Web-Bootstrap
          in: header
          required: true
          schema:
            type: string
      responses:
        "303":
          description: Web bootstrap completed; the caller is redirected to the target path with a session cookie set
          headers:
            Location:
              schema:
                type: string
            Set-Cookie:
              schema:
                type: string
        "400":
          description: Invalid bootstrap completion request or HTTPS required
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "403":
          description: Web bootstrap token mismatch
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "404":
          description: Web bootstrap not found
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "410":
          description: Web bootstrap expired, consumed, or revoked
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "503":
          description: Web bootstrap store unavailable
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"

  /timers:
    get:
      summary: List all timers
      operationId: getTimers
      tags:
        - timers
      parameters:
        - name: state
          in: query
          schema:
            type: string
            default: scheduled
        - name: from
          in: query
          schema:
            type: integer
      responses:
        "200":
          description: List of timers
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimerList"
    post:
      summary: Create a timer
      security:
        - BearerAuth: [v3:write]
      operationId: addTimer
      tags:
        - timers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TimerCreateRequest"
      responses:
        "201":
          description: Timer created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Timer"
        "409":
          description: Duplicate timer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "422":
          description: Conflict or validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "502":
          description: Receiver inconsistent (verification failed)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"

  /timers/{timerId}:
    get:
      summary: Get timer
      operationId: getTimer
      tags:
        - timers
      parameters:
        - name: timerId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Timer details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Timer"
        "404":
          description: Timer not found
    patch:
      summary: Edit timer
      security:
        - BearerAuth: [v3:write]
      operationId: updateTimer
      tags:
        - timers
      parameters:
        - name: timerId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TimerPatchRequest"
      responses:
        "200":
          description: Timer updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Timer"
        "404":
          description: Timer not found
        "409":
          description: Duplicate resulting from edit
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "422":
          description: Conflict
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "502":
          description: Receiver fail / Rollback
    delete:
      summary: Delete timer
      security:
        - BearerAuth: [v3:write]
      operationId: deleteTimer
      tags:
        - timers
      parameters:
        - name: timerId
          in: path
          required: true
          schema:
            type: string
      responses:
        "204":
          description: Deleted
        "404":
          description: Not found

  /timers/conflicts:preview:
    post:
      summary: Preview conflicts
      operationId: previewConflicts
      tags:
        - timers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TimerConflictPreviewRequest"
      responses:
        "200":
          description: Preview result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TimerConflictPreviewResponse"

  /dvr/capabilities:
    get:
      summary: Get DVR capabilities
      operationId: getDvrCapabilities
      tags:
        - dvr
      responses:
        "200":
          description: Capabilities
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DvrCapabilities"

  /dvr/status:
    get:
      summary: Get DVR recording status
      operationId: getDvrStatus
      tags:
        - dvr
      responses:
        "200":
          description: Recording status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RecordingStatus"

  /streams/{id}:
    delete:
      summary: Terminate a stream session
      security:
        - BearerAuth: [v3:write]
      operationId: deleteStreamsId
      tags:
        - streams
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        "204":
          description: Session terminated
        "404":
          description: Session not found

  /sessions/{sessionId}/feedback:
    post:
      summary: Report playback feedback (e.g. valid errors)
      operationId: reportPlaybackFeedback
      tags: [streams]
      parameters:
        - in: path
          name: sessionId
          schema:
            type: string
            format: uuid
          required: true
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PlaybackFeedbackRequest"
      responses:
        "202":
          description: Feedback accepted
        "404":
          description: Session not found

  /logs:
    get:
      summary: Get recent logs
      security:
        - BearerAuth: [v3:admin]
      operationId: getLogs
      tags:
        - system
      parameters:
        - in: query
          name: limit
          required: false
          schema:
            type: integer
            minimum: 1
          description: Maximum number of most-recent log entries to return.
      responses:
        "200":
          description: Recent log entries
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/LogEntry"

  /series-rules:
    get:
      summary: List all series recording rules
      operationId: getSeriesRules
      tags:
        - series
      responses:
        "200":
          description: List of rules
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/SeriesRule"
    post:
      summary: Create a new series rule
      security:
        - BearerAuth: [v3:write]
      operationId: createSeriesRule
      tags:
        - series
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SeriesRule"
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SeriesRule"

  /series-rules/{id}/run:
    post:
      summary: Run a specific series rule immediately
      security:
        - BearerAuth: [v3:write]
      operationId: runSeriesRule
      tags:
        - series
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: trigger
          in: query
          schema:
            type: string
            default: manual
      responses:
        "200":
          description: Run report
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SeriesRuleRunReport"
        "404":
          description: Rule not found

  /series-rules/run:
    post:
      summary: Run all enabled series rules immediately
      security:
        - BearerAuth: [v3:write]
      operationId: runAllSeriesRules
      tags:
        - series
      parameters:
        - name: trigger
          in: query
          schema:
            type: string
            default: manual
      responses:
        "200":
          description: List of run reports
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/SeriesRuleRunReport"

  /series-rules/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
    put:
      summary: Update an existing series rule
      security:
        - BearerAuth: [v3:write]
      operationId: updateSeriesRule
      tags:
        - series
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SeriesRuleUpdate"
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SeriesRule"
        "404":
          description: Rule not found
        "400":
          description: Invalid request
    delete:
      summary: Delete a series rule
      security:
        - BearerAuth: [v3:write]
      operationId: deleteSeriesRule
      tags:
        - series
      responses:
        "204":
          description: Deleted
        "404":
          description: Rule not found

  # ==================== V3 Control Plane API ====================
  /system/scan:
    post:
      summary: Trigger a background scan of all channels for capabilities
      operationId: TriggerSystemScan
      tags:
        - system
      security:
        - BearerAuth: []
      responses:
        "202":
          description: Scan initiated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: "started" }
        "200":
          description: Scan already running
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: "already_running" }
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/APIError"
        "500":
          description: Internal Server Error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/APIError"
    get:
      summary: Get status of the capability scan
      operationId: GetSystemScanStatus
      tags:
        - system
      security:
        - BearerAuth: []
      responses:
        "200":
          description: Current scan status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ScanStatus"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/APIError"
        "503":
          description: Scanner not initialized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/APIError"
  /intents:
    post:
      summary: Create stream intent (start or stop session)
      operationId: createIntent
      tags:
        - v3
      security:
        - BearerAuth: [v3:write]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/IntentRequest"
      responses:
        "202":
          description: Intent accepted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IntentAcceptedResponse"
        "400":
          description: Invalid request
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "401":
          description: Missing or invalid decision token
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "403":
          description: Decision token does not authorize the requested action
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "503":
          description: V3 control plane unavailable
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds to wait before retry when capacity or admission backoff is known
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"

  /sessions:
    get:
      summary: List all sessions (admin only)
      operationId: listSessions
      tags:
        - v3
      security:
        - BearerAuth: [v3:admin]
      parameters:
        - name: offset
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
          description: Pagination offset
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 1000
            default: 100
          description: Pagination limit (max 1000)
      responses:
        "200":
          description: Sessions list with pagination metadata
          content:
            application/json:
              schema:
                type: object
                properties:
                  sessions:
                    type: array
                    items:
                      $ref: "#/components/schemas/SessionRecord"
                  pagination:
                    type: object
                    properties:
                      offset:
                        type: integer
                      limit:
                        type: integer
                      total:
                        type: integer
                      count:
                        type: integer
        "503":
          description: V3 control plane unavailable
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/APIError"

  /sessions/{sessionID}:
    parameters:
      - name: sessionID
        in: path
        required: true
        schema:
          type: string
          format: uuid
    get:
      summary: Get session state
      operationId: getSessionState
      tags:
        - v3
      security:
        - BearerAuth: [v3:read]
      responses:
        "200":
          description: Session state
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionResponse"
        "400":
          description: Invalid session ID
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/APIError"
        "404":
          description: Session not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/APIError"
        "410":
          description: Terminal session state. `code` can be `TRANSCODE_STALLED` when the transcode watchdog detected no progress.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/SessionTerminalProblem"

  /sessions/{sessionID}/heartbeat:
    parameters:
      - name: sessionID
        in: path
        required: true
        schema:
          type: string
          format: uuid
    post:
      summary: Renew session lease
      operationId: postSessionHeartbeat
      tags:
        - v3
      security:
        - BearerAuth: [v3:read]
      responses:
        "200":
          description: Session heartbeat acknowledged
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionHeartbeatResponse"
        "400":
          description: Invalid session ID
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "404":
          description: Session not found
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "410":
          description: Session cannot be renewed because it is terminal or its lease already expired.
          content:
            application/problem+json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/ProblemDetails"
                  - $ref: "#/components/schemas/SessionTerminalProblem"
        "500":
          description: Session lease update failed
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "503":
          description: Session subsystem unavailable
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"

  /sessions/{sessionID}/hls/{filename}:
    parameters:
      - name: sessionID
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: filename
        in: path
        required: true
        schema:
          type: string
          pattern: '^[a-zA-Z0-9_.-]+\.(m3u8|ts|m4s|mp4)$'
    get:
      summary: Serve HLS playlist or segment
      operationId: serveHLS
      tags:
        - v3
      security:
        - BearerAuth: [v3:read]
      responses:
        "200":
          description: HLS content
          content:
            application/vnd.apple.mpegurl:
              schema:
                type: string
            video/MP2T:
              schema:
                type: string
                format: binary
            video/iso.segment:
              schema:
                type: string
                format: binary
            video/mp4:
              schema:
                type: string
                format: binary
        "400":
          description: Invalid request
        "404":
          description: File not found
        "503":
          description: V3 control plane unavailable
    head:
      summary: Get HLS content metadata (Safari compatibility)
      operationId: serveHLSHead
      tags:
        - v3
      security:
        - BearerAuth: [v3:read]
      responses:
        "200":
          description: HLS content metadata (headers only)
          headers:
            Content-Type:
              schema:
                type: string
            Content-Length:
              schema:
                type: integer
            Cache-Control:
              schema:
                type: string
        "404":
          description: File not found
        "503":
          description: V3 control plane unavailable
