openapi: 3.0.3
info:
  title: ChronoRégie — API publique
  description: >-
    API de pilotage à distance de ChronoRégie, le timer de régie événementielle.
    Philosophie « une URL = une action » : tout est pilotable en simple GET
    (compatible Stream Deck, Bitfocus Companion, curl) ; les mutations acceptent
    aussi POST avec un corps JSON. L'authentification se fait par la clé API de
    la salle, présente dans le chemin (elle est affichée dans la régie).
    Limite de débit : 100 requêtes / minute / clé (en-têtes RateLimit-*,
    429 au-delà). Toutes les durées sont en millisecondes.
  version: 1.0.0
servers:
  - url: http://localhost:4141
    description: Serveur local ChronoRégie
tags:
  - name: pilotage
    description: Commandes de transport et d'affichage (v0 et v1)
  - name: timers
    description: Gestion du conducteur (v1)
  - name: messages
    description: Messages à l'écran (v1)
paths:
  /api/v0/{apiKey}/{cmd}:
    get:
      tags: [pilotage]
      summary: API v0 (rétro-compatibilité Stream Deck)
      description: >-
        Commandes historiques, conservées telles quelles : start, pause, stop,
        reset, next, adjust?ms=, message?text=&flash=1, clear_message,
        blackout?on=, status.
      parameters:
        - $ref: '#/components/parameters/apiKey'
        - name: cmd
          in: path
          required: true
          schema:
            type: string
            enum: [start, pause, stop, reset, next, adjust, message, clear_message, blackout, status]
      responses:
        '200':
          $ref: '#/components/responses/etat'
        '401':
          $ref: '#/components/responses/cleInvalide'
        '404':
          $ref: '#/components/responses/inconnu'
  /api/v1/{apiKey}/state:
    get:
      tags: [pilotage]
      summary: État complet de la salle
      description: >-
        Instantané complet : transport (running, endAt, remainingAtPause),
        timer actif, conducteur, messages, réglages, affichage, questions.
        C'est la même vue que celle diffusée aux pupitres de contrôle.
      parameters:
        - $ref: '#/components/parameters/apiKey'
      responses:
        '200':
          $ref: '#/components/responses/etat'
        '401':
          $ref: '#/components/responses/cleInvalide'
        '429':
          $ref: '#/components/responses/limite'
  /api/v1/{apiKey}/timers:
    get:
      tags: [timers]
      summary: Liste des timers du conducteur
      parameters:
        - $ref: '#/components/parameters/apiKey'
      responses:
        '200':
          description: Conducteur complet, dans l'ordre
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean, example: true }
                  activeTimerId: { type: string }
                  timers:
                    type: array
                    items: { $ref: '#/components/schemas/Timer' }
        '401':
          $ref: '#/components/responses/cleInvalide'
        '429':
          $ref: '#/components/responses/limite'
    post:
      tags: [timers]
      summary: Créer un timer (ajouté en fin de conducteur)
      parameters:
        - $ref: '#/components/parameters/apiKey'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                label: { type: string, example: Keynote d'ouverture }
                speaker: { type: string, example: Alice Martin }
                duration: { type: integer, example: 1200000, description: Durée en ms (défaut 10 min, max 24 h) }
      responses:
        '201':
          description: Timer créé
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean, example: true }
                  timer: { $ref: '#/components/schemas/Timer' }
        '400':
          $ref: '#/components/responses/requeteInvalide'
        '401':
          $ref: '#/components/responses/cleInvalide'
        '429':
          $ref: '#/components/responses/limite'
  /api/v1/{apiKey}/timer/{id}/select:
    get:
      tags: [timers]
      summary: Sélectionner un timer (devient le timer actif, en pause)
      parameters:
        - $ref: '#/components/parameters/apiKey'
        - $ref: '#/components/parameters/timerId'
      responses:
        '200': { $ref: '#/components/responses/etat' }
        '401': { $ref: '#/components/responses/cleInvalide' }
        '404': { $ref: '#/components/responses/inconnu' }
        '429': { $ref: '#/components/responses/limite' }
    post:
      tags: [timers]
      summary: Sélectionner un timer (variante POST)
      parameters:
        - $ref: '#/components/parameters/apiKey'
        - $ref: '#/components/parameters/timerId'
      responses:
        '200': { $ref: '#/components/responses/etat' }
        '401': { $ref: '#/components/responses/cleInvalide' }
        '404': { $ref: '#/components/responses/inconnu' }
        '429': { $ref: '#/components/responses/limite' }
  /api/v1/{apiKey}/timer/{id}/update:
    get:
      tags: [timers]
      summary: Modifier un timer (paramètres en query)
      description: Au moins un champ parmi label, speaker, duration (ms), warnAt (ms), appearance (countdown | countup | clock).
      parameters:
        - $ref: '#/components/parameters/apiKey'
        - $ref: '#/components/parameters/timerId'
        - name: label
          in: query
          schema: { type: string }
        - name: speaker
          in: query
          schema: { type: string }
        - name: duration
          in: query
          schema: { type: integer, description: Durée en ms }
        - name: warnAt
          in: query
          schema: { type: integer, description: Seuil d'alerte orange en ms }
        - name: appearance
          in: query
          schema: { type: string, enum: [countdown, countup, clock] }
      responses:
        '200': { $ref: '#/components/responses/etat' }
        '400': { $ref: '#/components/responses/requeteInvalide' }
        '401': { $ref: '#/components/responses/cleInvalide' }
        '404': { $ref: '#/components/responses/inconnu' }
        '429': { $ref: '#/components/responses/limite' }
    post:
      tags: [timers]
      summary: Modifier un timer (variante POST, corps JSON)
      parameters:
        - $ref: '#/components/parameters/apiKey'
        - $ref: '#/components/parameters/timerId'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                label: { type: string }
                speaker: { type: string }
                duration: { type: integer }
                warnAt: { type: integer }
                appearance: { type: string, enum: [countdown, countup, clock] }
      responses:
        '200': { $ref: '#/components/responses/etat' }
        '400': { $ref: '#/components/responses/requeteInvalide' }
        '401': { $ref: '#/components/responses/cleInvalide' }
        '404': { $ref: '#/components/responses/inconnu' }
        '429': { $ref: '#/components/responses/limite' }
  /api/v1/{apiKey}/timer/{id}/delete:
    get:
      tags: [timers]
      summary: Supprimer un timer (le dernier timer du conducteur est conservé)
      parameters:
        - $ref: '#/components/parameters/apiKey'
        - $ref: '#/components/parameters/timerId'
      responses:
        '200': { $ref: '#/components/responses/etat' }
        '400': { $ref: '#/components/responses/requeteInvalide' }
        '401': { $ref: '#/components/responses/cleInvalide' }
        '404': { $ref: '#/components/responses/inconnu' }
        '429': { $ref: '#/components/responses/limite' }
    post:
      tags: [timers]
      summary: Supprimer un timer (variante POST)
      parameters:
        - $ref: '#/components/parameters/apiKey'
        - $ref: '#/components/parameters/timerId'
      responses:
        '200': { $ref: '#/components/responses/etat' }
        '400': { $ref: '#/components/responses/requeteInvalide' }
        '401': { $ref: '#/components/responses/cleInvalide' }
        '404': { $ref: '#/components/responses/inconnu' }
        '429': { $ref: '#/components/responses/limite' }
  /api/v1/{apiKey}/message/set:
    get:
      tags: [messages]
      summary: Afficher un message sur les écrans (paramètres en query)
      parameters:
        - $ref: '#/components/parameters/apiKey'
        - name: text
          in: query
          required: true
          schema: { type: string, example: Il reste 5 minutes }
        - name: flash
          in: query
          schema: { type: string, enum: ['0', '1'], description: '1 = clignotant' }
        - name: color
          in: query
          schema: { type: string, enum: [white, green, red] }
        - name: bold
          in: query
          schema: { type: string, enum: ['0', '1'] }
      responses:
        '200': { $ref: '#/components/responses/etat' }
        '400': { $ref: '#/components/responses/requeteInvalide' }
        '401': { $ref: '#/components/responses/cleInvalide' }
        '429': { $ref: '#/components/responses/limite' }
    post:
      tags: [messages]
      summary: Afficher un message sur les écrans (variante POST)
      parameters:
        - $ref: '#/components/parameters/apiKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [text]
              properties:
                text: { type: string }
                flash: { type: boolean }
                color: { type: string, enum: [white, green, red] }
                bold: { type: boolean }
      responses:
        '200': { $ref: '#/components/responses/etat' }
        '400': { $ref: '#/components/responses/requeteInvalide' }
        '401': { $ref: '#/components/responses/cleInvalide' }
        '429': { $ref: '#/components/responses/limite' }
  /api/v1/{apiKey}/message/clear:
    get:
      tags: [messages]
      summary: Effacer le message affiché
      parameters:
        - $ref: '#/components/parameters/apiKey'
      responses:
        '200': { $ref: '#/components/responses/etat' }
        '401': { $ref: '#/components/responses/cleInvalide' }
        '429': { $ref: '#/components/responses/limite' }
    post:
      tags: [messages]
      summary: Effacer le message affiché (variante POST)
      parameters:
        - $ref: '#/components/parameters/apiKey'
      responses:
        '200': { $ref: '#/components/responses/etat' }
        '401': { $ref: '#/components/responses/cleInvalide' }
        '429': { $ref: '#/components/responses/limite' }
  /api/v1/{apiKey}/{cmd}:
    get:
      tags: [pilotage]
      summary: Commande de pilotage (transport et affichage)
      description: >-
        start, pause, stop (= pause), reset, next, adjust?ms= (±1 h max par appel),
        message?text=&flash=&color=&bold=, clear_message, blackout?on=,
        onair?on=, status (lecture seule).
      parameters:
        - $ref: '#/components/parameters/apiKey'
        - name: cmd
          in: path
          required: true
          schema:
            type: string
            enum: [start, pause, stop, reset, next, adjust, message, clear_message, blackout, onair, status]
        - name: ms
          in: query
          schema: { type: integer, example: 60000, description: Pour adjust — décalage en ms }
        - name: text
          in: query
          schema: { type: string, description: Pour message }
        - name: flash
          in: query
          schema: { type: string, enum: ['0', '1'], description: Pour message }
        - name: color
          in: query
          schema: { type: string, enum: [white, green, red], description: Pour message }
        - name: bold
          in: query
          schema: { type: string, enum: ['0', '1'], description: Pour message }
        - name: 'on'
          in: query
          schema: { type: string, enum: ['0', '1'], description: 'Pour blackout / onair (0 = éteindre, tout le reste = allumer)' }
      responses:
        '200': { $ref: '#/components/responses/etat' }
        '401': { $ref: '#/components/responses/cleInvalide' }
        '404': { $ref: '#/components/responses/inconnu' }
        '429': { $ref: '#/components/responses/limite' }
    post:
      tags: [pilotage]
      summary: Commande de pilotage (variante POST, corps JSON)
      parameters:
        - $ref: '#/components/parameters/apiKey'
        - name: cmd
          in: path
          required: true
          schema:
            type: string
            enum: [start, pause, stop, reset, next, adjust, message, clear_message, blackout, onair, status]
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                ms: { type: integer }
                text: { type: string }
                flash: { type: boolean }
                color: { type: string }
                bold: { type: boolean }
                on: { type: string }
      responses:
        '200': { $ref: '#/components/responses/etat' }
        '401': { $ref: '#/components/responses/cleInvalide' }
        '404': { $ref: '#/components/responses/inconnu' }
        '429': { $ref: '#/components/responses/limite' }
components:
  parameters:
    apiKey:
      name: apiKey
      in: path
      required: true
      description: Clé API de la salle (affichée dans la régie, carte « Lien & QR »)
      schema: { type: string }
    timerId:
      name: id
      in: path
      required: true
      description: Identifiant du timer (champ id de la liste /timers)
      schema: { type: string }
  schemas:
    Timer:
      type: object
      properties:
        id: { type: string, example: k7x2m9 }
        label: { type: string, example: Keynote d'ouverture }
        speaker: { type: string, example: Alice Martin }
        duration: { type: integer, example: 1200000, description: Durée en ms }
        warnAt: { type: integer, example: 120000, description: Seuil d'alerte orange en ms }
        appearance: { type: string, enum: [countdown, countup, clock] }
        tagId: { type: [string, 'null'] }
        scheduledAt: { type: [integer, 'null'], description: Horodatage de démarrage programmé (ms epoch) }
    Etat:
      type: object
      description: >-
        Instantané de la salle. Le serveur est l'horloge de référence : pour
        afficher le restant, projeter `endAt - serverNow` (si running) sinon
        `remainingAtPause` — ne jamais compter localement.
      properties:
        ok: { type: boolean, example: true }
        state:
          type: object
          properties:
            seq: { type: integer }
            serverNow: { type: integer, description: Horloge serveur (ms epoch) }
            running: { type: boolean }
            endAt: { type: [integer, 'null'] }
            remainingAtPause: { type: [integer, 'null'] }
            remaining: { type: integer, description: Restant calculé côté serveur (ms, négatif = dépassement) }
            zeroBehavior: { type: string, enum: [continue, stop, hide] }
            autoAdvance: { type: boolean }
            blackout: { type: boolean }
            onAir: { type: boolean }
            activeTimerId: { type: string }
            activeLabel: { type: string }
            activeSpeaker: { type: string }
            warnAt: { type: integer }
            duration: { type: integer }
            message: { type: [object, 'null'] }
            timers:
              type: array
              items: { type: object }
    Erreur:
      type: object
      properties:
        ok: { type: boolean, example: false }
        error: { type: string, example: clé API invalide }
  responses:
    etat:
      description: Commande appliquée — état complet de la salle
      headers:
        RateLimit-Limit:
          schema: { type: integer }
          description: Quota par minute et par clé (100)
        RateLimit-Remaining:
          schema: { type: integer }
          description: Requêtes restantes dans la fenêtre courante
        RateLimit-Reset:
          schema: { type: integer }
          description: Secondes avant réinitialisation du quota
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Etat' }
    cleInvalide:
      description: Clé API invalide
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Erreur' }
    inconnu:
      description: Commande, opération ou timer inconnu
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Erreur' }
    requeteInvalide:
      description: Paramètres invalides (durée hors bornes, texte vide, dernier timer…)
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Erreur' }
    limite:
      description: Limite de débit dépassée (100 requêtes / minute / clé)
      headers:
        RateLimit-Limit:
          schema: { type: integer }
        RateLimit-Remaining:
          schema: { type: integer }
        RateLimit-Reset:
          schema: { type: integer }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Erreur' }
