swagger: "2.0"
info:
  title: API TenTenTen Archery - Portail Partenaires
  description: "Spécification des points d'accès REST TenTenTen : détection de blasons de tir à l'arc (World Archery, Beursault, Campagne), scoring balistique de tir sportif (C50, C25) et annuaire des structures d'archerie."
  contact:
    email: contact@tententen-archery.com
  version: "1.0.0 (Build 2026-09-10 / rev.00073)"
host: tententen-vision-3iv2bzrnda-ew.a.run.app
schemes:
  - https
produces:
  - application/json

tags:
  - name: "Tir à l'Arc"
    description: "Scoring de blasons World Archery et Beursault, détection d'impacts de flèches et colorimétrie des empennages"
  - name: "Tir Sportif"
    description: "Scoring de cibles C50, C25 et 10m, détection d'impacts de projectiles, règle du cordonnet et visualiseur interactif"
  - name: "Structures & Clubs d'Archerie"
    description: "Recherche et géolocalisation des clubs et compagnies de tir à l'arc"
  - name: "Système & Santé"
    description: "Surveillance opérationnelle et vérification de connectivité"

# Sécurité requise : clé d'API partenaire (x-api-key)
securityDefinitions:
  api_key_header:
    type: apiKey
    name: x-api-key
    in: header
    description: "Clé d'API partenaire fournie par TenTenTen (transmise via l'en-tête x-api-key)"

# Authentification requise par défaut
security:
  - api_key_header: []

paths:
  /v1/health:
    get:
      tags:
        - "Système & Santé"
      summary: Vérification de la disponibilité du service
      description: Permet de s'assurer que les serveurs de TenTenTen répondent correctement.
      operationId: healthCheck
      security: [] # Public, pas besoin de clé
      responses:
        200:
          description: Le service est opérationnel.
          schema:
            type: object
            properties:
              status:
                type: string
                example: ok

  /v1/clubs:
    get:
      tags:
        - "Structures & Clubs d'Archerie"
      summary: Recherche et listage des clubs d'archerie
      description: |
        Récupère la liste des structures d'archerie issues des répertoires publics, filtrées selon les paramètres géographiques et le type de club.
        
        > **⚠️ Note de test (Navigateurs) :** L'API est protégée par la passerelle de sécurité Google Cloud. Si vous testez une requête depuis votre navigateur (ou ce Swagger UI) **sans fournir de clé d'API valide**, la sécurité bloquera l'appel avant même d'atteindre nos serveurs. En conséquence, votre navigateur affichera une erreur **CORS policy** (et non une simple erreur 401). C'est le comportement attendu. N'oubliez pas d'utiliser le bouton **Authorize** pour éviter cela !
      operationId: getClubs
      parameters:
        - name: dep
          in: query
          description: "Numéro de département (ex: 93)"
          type: string
          required: false
        - name: region
          in: query
          description: "Code géographique INSEE de la région (ex: 11)"
          type: string
          required: false
        - name: type
          in: query
          description: Type de structure (compagnie ou club)
          type: string
          required: false
        - name: profile
          in: query
          description: Profil de club (elite, sportive, traditional)
          type: string
          required: false
        - name: city
          in: query
          description: Nom de la ville (recherche floue)
          type: string
          required: false
      responses:
        200:
          description: Liste des clubs récupérée avec succès.
        401:
          description: Clé d'API manquante ou invalide.
        500:
          description: Erreur interne du serveur.

  /v1/score:
    post:
      tags:
        - "Tir à l'Arc"
      summary: Scoring automatique de blason de tir à l'arc
      description: Analyse une image de blason, extrait la position des flèches et renvoie les scores et coordonnées.
      operationId: scoreTarget
      consumes:
        - multipart/form-data
      parameters:
        - name: file
          in: formData
          description: Fichier image de la cible (.jpg ou .png)
          type: file
          required: true
        - name: arrows_count
          in: formData
          description: Nombre de flèches attendues à détecter
          type: integer
          default: 6
          required: false
        - name: target_size
          in: formData
          description: "Taille réglementaire du blason en cm (ex: 122, 80, 40)"
          type: integer
          default: 122
          required: false
        - name: cock_fletching_color
          in: formData
          description: Couleur de la plume coq
          type: string
          default: orange
          required: false
        - name: hen_fletching_color
          in: formData
          description: Couleur des plumes poules
          type: string
          default: vert
          required: false
        - name: nock_color
          in: formData
          description: Couleur de l'encoche
          type: string
          default: orange
          required: false
        - name: shaft_color
          in: formData
          description: Couleur du tube de la flèche
          type: string
          default: "#212121"
          required: false
      responses:
        200:
          description: Scoring calculé avec succès.
        401:
          description: Clé d'API manquante ou invalide.
        500:
          description: Échec du traitement de l'image.

  /v1/score/stream:
    post:
      tags:
        - "Tir à l'Arc"
      summary: Scoring automatique en streaming temps réel (NDJSON)
      description: |
        Analyse une image de blason en diffusant la progression du traitement en temps réel (NDJSON).
        Chaque ligne envoyée correspond à un événement JSON avec le pourcentage d'avancement et l'étape courante
        (redressement du blason, centrage des anneaux, détection des flèches, calcul final des scores).
      operationId: scoreTargetStream
      consumes:
        - multipart/form-data
      produces:
        - application/x-ndjson
      parameters:
        - name: file
          in: formData
          description: Fichier image de la cible (.jpg ou .png)
          type: file
          required: true
        - name: arrows_count
          in: formData
          description: Nombre de flèches attendues à détecter
          type: integer
          default: 6
          required: false
        - name: target_size
          in: formData
          description: "Taille réglementaire du blason en cm (ex: 122, 80, 40)"
          type: integer
          default: 122
          required: false
        - name: cock_fletching_color
          in: formData
          description: Couleur de la plume coq
          type: string
          default: orange
          required: false
        - name: hen_fletching_color
          in: formData
          description: Couleur des plumes poules
          type: string
          default: vert
          required: false
        - name: nock_color
          in: formData
          description: Couleur de l'encoche
          type: string
          default: orange
          required: false
        - name: shaft_color
          in: formData
          description: Couleur du tube de la flèche
          type: string
          default: "#212121"
          required: false
      responses:
        200:
          description: Flux d'événements NDJSON de progression et résultat final.
        401:
          description: Clé d'API manquante ou invalide.
        500:
          description: Échec du traitement de l'image.

  /v1/analyze-arrow-colors:
    post:
      tags:
        - "Tir à l'Arc"
      summary: Analyse colorimétrique de flèche étalon
      description: |
        Analyse une photo d'une flèche (empennage et encoche) sur fond neutre et extrait automatiquement
        les signatures colorimétriques (codes hexadécimaux et noms standardisés) de l'encoche, de la plume coq,
        des plumes poules et du tube de la flèche.
      operationId: analyzeArrowColors
      consumes:
        - multipart/form-data
      parameters:
        - name: file
          in: formData
          description: Fichier image de la flèche (.jpg ou .png)
          type: file
          required: true
      responses:
        200:
          description: Couleurs de la flèche extraites avec succès.
          schema:
            type: object
            properties:
              success:
                type: boolean
                example: true
              nockColor:
                type: string
                example: "#C43222"
              nockColorName:
                type: string
                example: "rouge"
              cockFletchingColor:
                type: string
                example: "#6C8A44"
              cockFletchingColorName:
                type: string
                example: "vert"
              henFletchingColor:
                type: string
                example: "#C43222"
              henFletchingColorName:
                type: string
                example: "rouge"
              shaftColor:
                type: string
                example: "#272A2E"
              shaftColorName:
                type: string
                example: "noir"
        400:
          description: Le fichier fourni n'est pas une image valide (.jpg ou .png).
        401:
          description: Clé d'API manquante ou invalide.
        422:
          description: Aucune flèche détectée sur l'image ou contraste insuffisant avec le fond.
        500:
          description: Erreur interne lors du traitement de l'image.

  /v1/shooting/score:
    post:
      tags:
        - "Tir Sportif"
      summary: "Scoring complet d'une cible de tir sportif (C50, C25, 10m)"
      description: |
        Détecte les impacts de balles ou plombs, suit fidèlement les traits réels imprimés sans forcer d'ellipse, applique la règle réglementaire du cordonnet, filtre automatiquement les impacts suspects et restitue le calque vectoriel SVG superposable ainsi que les URLs directes vers l'image annotée et le visualiseur interactif.
      operationId: scoreShootingTarget
      consumes:
        - multipart/form-data
      parameters:
        - name: file
          in: formData
          description: "Image de la cible après les tirs (JPG, JPEG, PNG, WEBP)"
          type: file
          required: true
        - name: target_type
          in: formData
          description: "Code réglementaire de la cible (C50, C25, 10m_air_rifle, 10m_air_pistol)"
          type: string
          default: "C50"
          required: false
        - name: bullet_diameter_mm
          in: formData
          description: "Diamètre réel du projectile en millimètres (ex: 9.0 pour 9x19 / .38, 4.5 pour plomb, 5.56)"
          type: number
          default: 9.0
          required: false
        - name: shots_count
          in: formData
          description: "Nombre de cartouches tirées attendues (optionnel)"
          type: integer
          required: false
      responses:
        200:
          description: "Scoring calculé avec succès, métriques balistiques et URLs interactives."
          schema:
            $ref: "#/definitions/ShootingScoreResponse"
          examples:
            application/json:
              status: "ok"
              target_type: "C50"
              total_score: 93
              clean_score: 93
              raw_score: 95
              inner_tens_count: 3
              impacts_count: 10
              raw_impacts_count: 11
              suspect_impacts_count: 1
              overall_confidence: 0.98
              target_center:
                x: 1512.4
                y: 1508.2
              target_visual_radius_px: 604.8
              pixels_per_mm: 6.048
              viewer_url: "https://tententen-vision-3iv2bzrnda-ew.a.run.app/v1/shooting/results/62a6f92628d2/view"
              annotated_image_url: "https://tententen-vision-3iv2bzrnda-ew.a.run.app/v1/shooting/results/62a6f92628d2.jpg"
              viewer_url_markdown: "[Page d'inspection interactive](https://tententen-vision-3iv2bzrnda-ew.a.run.app/v1/shooting/results/62a6f92628d2/view)"
              annotated_image_url_markdown: "[Image annotée haute définition](https://tententen-vision-3iv2bzrnda-ew.a.run.app/v1/shooting/results/62a6f92628d2.jpg)"
              viewer_url_html: "<a href=\"https://tententen-vision-3iv2bzrnda-ew.a.run.app/v1/shooting/results/62a6f92628d2/view\" target=\"_blank\" rel=\"noopener noreferrer\">Page d'inspection interactive</a>"
              annotated_image_url_html: "<a href=\"https://tententen-vision-3iv2bzrnda-ew.a.run.app/v1/shooting/results/62a6f92628d2.jpg\" target=\"_blank\" rel=\"noopener noreferrer\">Image annotée haute définition</a>"
              links:
                viewer:
                  label: "Page d'inspection interactive"
                  url: "https://tententen-vision-3iv2bzrnda-ew.a.run.app/v1/shooting/results/62a6f92628d2/view"
                  markdown: "[Page d'inspection interactive](https://tententen-vision-3iv2bzrnda-ew.a.run.app/v1/shooting/results/62a6f92628d2/view)"
                  html: "<a href=\"https://tententen-vision-3iv2bzrnda-ew.a.run.app/v1/shooting/results/62a6f92628d2/view\" target=\"_blank\">Page d'inspection interactive</a>"
                annotated_image:
                  label: "Image annotée haute définition"
                  url: "https://tententen-vision-3iv2bzrnda-ew.a.run.app/v1/shooting/results/62a6f92628d2.jpg"
                  markdown: "[Image annotée haute définition](https://tententen-vision-3iv2bzrnda-ew.a.run.app/v1/shooting/results/62a6f92628d2.jpg)"
                  html: "<a href=\"https://tententen-vision-3iv2bzrnda-ew.a.run.app/v1/shooting/results/62a6f92628d2.jpg\" target=\"_blank\">Image annotée haute définition</a>"
              svg_overlay: "<svg viewBox=\"0 0 3024 3024\" xmlns=\"http://www.w3.org/2000/svg\">...</svg>"
              execution_time_ms: 412.5
        400:
          description: "Image invalide ou paramètres de tir non reconnus."
        401:
          description: "Clé d'API manquante ou invalide."
        500:
          description: "Erreur lors du traitement de la cible."

  /v1/shooting/zones:
    post:
      tags:
        - "Tir Sportif"
      summary: "Vectorisation exacte des zones de score (SVG et polygones)"
      description: |
        Extrait les contours réels des zones d'une cible en suivant fidèlement les traits d'encre imprimés (programmation dynamique 360°, sans forcer d'ellipse) et génère le calque SVG superposable.
      operationId: extractShootingZones
      consumes:
        - multipart/form-data
      parameters:
        - name: file
          in: formData
          description: "Image de la cible (JPG, PNG)"
          type: file
          required: true
        - name: target_type
          in: formData
          description: "Type de cible (C50, C25, 10m_air_rifle, 10m_air_pistol)"
          type: string
          default: "C50"
          required: false
      responses:
        200:
          description: "Zones et calque SVG extraits avec succès."
        400:
          description: "Image non valide ou cible non détectée."
        401:
          description: "Clé d'API manquante ou invalide."
        500:
          description: "Erreur lors de l'extraction des zones."

  /v1/shooting/preview:
    post:
      tags:
        - "Tir Sportif"
      summary: "Aperçu visuel direct de la cible annotée (JPEG)"
      description: |
        Analyse la cible et renvoie directement l'image JPEG avec les contours des zones déformées, le centre, les impacts et les scores incrustés pour affichage immédiat dans le navigateur ou Swagger.
      operationId: previewShootingTarget
      consumes:
        - multipart/form-data
      produces:
        - image/jpeg
      parameters:
        - name: file
          in: formData
          description: "Image de la cible après les tirs"
          type: file
          required: true
        - name: target_type
          in: formData
          description: "Code réglementaire de la cible (C50, C25)"
          type: string
          default: "C50"
          required: false
        - name: bullet_diameter_mm
          in: formData
          description: "Diamètre du projectile en mm (défaut 9.0)"
          type: number
          default: 9.0
          required: false
      responses:
        200:
          description: "Image JPEG annotée retournée avec succès."
        400:
          description: "Image invalide."
        401:
          description: "Clé d'API manquante ou invalide."
        500:
          description: "Erreur lors de la génération de l'aperçu."

  /v1/shooting/results/{result_id}:
    delete:
      tags:
        - "Tir Sportif"
      summary: "Suppression définitive d'une session de tir (cache local et Cloud Storage)"
      description: |
        Supprime définitivement du cache local et de Google Cloud Storage tous les artefacts d'une session (image annotée, image brute originale, métadonnées JSON).
      operationId: deleteShootingResult
      security:
        - api_key_header: []
        - bearer_auth: []
      produces:
        - application/json
      parameters:
        - name: result_id
          in: path
          description: "Identifiant de session de tir à supprimer (ex: 62a6f92628d2)"
          type: string
          required: true
      responses:
        200:
          description: "Session supprimée avec succès du cache local et de Cloud Storage."
        400:
          description: "Identifiant de session invalide."
        403:
          description: "Clé d'API manquante ou non autorisée."

  /v1/shooting/results/{result_id}/view:
    get:
      tags:
        - "Tir Sportif"
      summary: "Visualiseur interactif avec zoom centré, vignette HD et tableau de tirs"
      description: |
        Affiche une interface web complète avec l'image originale haute résolution, la superposition vectorielle SVG, le zoom dynamique 3.5x au clic sur les impacts, le réticule de visée et la vignette HD de contrôle (avec bascule carton brut / détection).
      operationId: viewShootingResult
      produces:
        - text/html
      parameters:
        - name: result_id
          in: path
          description: "Identifiant de la session d'analyse (ex: 62a6f92628d2)"
          type: string
          required: true
      responses:
        200:
          description: "Interface web du visualiseur HTML retournée avec succès."
        400:
          description: "Identifiant de résultat invalide."
        404:
          description: "Résultat introuvable ou expiré."

  /v1/shooting/results/{result_id}.json:
    get:
      tags:
        - "Tir Sportif"
      summary: "Export JSON des métadonnées balistiques et du contrôle qualité"
      description: |
        Renvoie l'intégralité des métadonnées de la session au format JSON structuré : score calculé, score épuré, impacts détaillés, polygone du carton, rapport de qualité et statistiques de groupement.
      operationId: getShootingResultJson
      produces:
        - application/json
      parameters:
        - name: result_id
          in: path
          description: "Identifiant de la session de tir (ex: 62a6f92628d2)"
          type: string
          required: true
      responses:
        200:
          description: "Document JSON balistique complet."
        400:
          description: "Identifiant de session invalide."
        404:
          description: "Session introuvable."

  /v1/shooting/results/{result_id}/edit:
    post:
      tags:
        - "Tir Sportif"
      summary: "Arbitrage manuel des impacts et recalcul balistique complet"
      description: |
        Permet à l'opérateur humain d'ajuster le statut suspect d'un impact, de pointer un tir manquant ou d'en supprimer un. Recalcule instantanément les scores, le PMI, la dispersion, le SVG et sauvegarde la session.
      operationId: editShootingResult
      consumes:
        - application/json
      produces:
        - application/json
      parameters:
        - name: result_id
          in: path
          description: "Identifiant de la session de tir à modifier"
          type: string
          required: true
        - name: payload
          in: body
          description: "Liste des modifications manuelles (actions update, add, delete)"
          required: true
          schema:
            type: object
            properties:
              impacts:
                type: array
                items:
                  type: object
                  properties:
                    id:
                      type: integer
                    x:
                      type: number
                    y:
                      type: number
                    is_suspect:
                      type: boolean
                    action:
                      type: string
                      enum: [update, add, delete]
      responses:
        200:
          description: "Résultat balistique complet mis à jour après arbitrage."
        400:
          description: "Requête ou identifiant invalide."
        404:
          description: "Session introuvable."

  /v1/shooting/results/{result_id}.jpg:
    get:
      tags:
        - "Tir Sportif"
      summary: "Image annotée haute résolution d'une session de tir"
      description: |
        Renvoie le fichier JPEG haute résolution avec incrustation des zones réelles, du réticule et des impacts.
      operationId: getShootingResultImage
      produces:
        - image/jpeg
      parameters:
        - name: result_id
          in: path
          description: "Identifiant de session d'analyse"
          type: string
          required: true
      responses:
        200:
          description: "Fichier image JPEG annoté."
        400:
          description: "Identifiant de session invalide."
        404:
          description: "Image introuvable."

  /v1/shooting/results/{result_id}_raw.jpg:
    get:
      tags:
        - "Tir Sportif"
      summary: "Image originale brute (non retouchée) d'une session de tir"
      description: |
        Renvoie la photo originale brute recadrée sans aucune annotation ni filtre, utilisée pour le contrôle visuel par vignette HD.
      operationId: getShootingRawResultImage
      produces:
        - image/jpeg
      parameters:
        - name: result_id
          in: path
          description: "Identifiant de session d'analyse"
          type: string
          required: true
      responses:
        200:
          description: "Fichier image JPEG brut non retouché."
        400:
          description: "Identifiant de session invalide."
        404:
          description: "Image introuvable."

definitions:
  ShootingScoreResponse:
    type: object
    description: "Réponse complète du scoring d'une cible de tir sportif avec métriques balistiques, calque SVG et URLs directes."
    required:
      - status
      - target_type
      - total_score
      - impacts_count
      - overall_confidence
      - target_center
      - target_visual_radius_px
      - pixels_per_mm
      - svg_overlay
      - viewer_url
      - annotated_image_url
      - execution_time_ms
    properties:
      status:
        type: string
        example: "ok"
        description: "Statut de l'opération ('ok' ou 'error')"
      session_id:
        type: string
        example: "62a6f92628d2"
        description: "Identifiant unique de la session de tir (archivé 30 jours)"
      target_type:
        type: string
        example: "C50"
        description: "Type de cible analysée ('C50', 'C25', '10m_air_rifle', '10m_air_pistol')"
      total_score:
        type: integer
        example: 93
        description: "Score total calculé selon le barème de la discipline"
      clean_score:
        type: integer
        example: 93
        description: "Score total épuré des faux positifs et impacts suspects hors calibre"
      raw_score:
        type: integer
        example: 95
        description: "Score brut total avant épuration des impacts suspects"
      inner_tens_count:
        type: integer
        example: 3
        description: "Nombre total de mouches (10+) validées"
      impacts_count:
        type: integer
        example: 10
        description: "Nombre d'impacts retenus dans le calcul du score"
      raw_impacts_count:
        type: integer
        example: 11
        description: "Nombre total brut de perforations détectées sur la cible"
      suspect_impacts_count:
        type: integer
        example: 1
        description: "Nombre d'impacts qualifiés de suspects ou débris hors calibre"
      overall_confidence:
        type: number
        example: 0.98
        description: "Indice de confiance global de l'analyse (0.0 à 1.0)"
      target_center:
        type: object
        description: "Coordonnées absolues du centre du visuel en pixels"
        properties:
          x:
            type: number
            example: 1512.4
          y:
            type: number
            example: 1508.2
      target_visual_radius_px:
        type: number
        example: 604.8
        description: "Rayon moyen du visuel noir central en pixels"
      pixels_per_mm:
        type: number
        example: 6.048
        description: "Échelle de calibration optique calculée (pixels par millimètre)"
      viewer_url:
        type: string
        example: "https://tententen-vision-3iv2bzrnda-ew.a.run.app/v1/shooting/results/62a6f92628d2/view"
        description: "Lien direct vers la page web interactive d'inspection (zoom dynamique, SVG superposable, tableau de scores)"
      annotated_image_url:
        type: string
        example: "https://tententen-vision-3iv2bzrnda-ew.a.run.app/v1/shooting/results/62a6f92628d2.jpg"
        description: "Lien direct vers l'image JPEG annotée haute résolution"
      viewer_url_markdown:
        type: string
        example: "[Page d'inspection interactive](https://tententen-vision-3iv2bzrnda-ew.a.run.app/v1/shooting/results/62a6f92628d2/view)"
        description: "Lien Markdown cliquable directement utilisable dans les messageries ou assistants"
      annotated_image_url_markdown:
        type: string
        example: "[Image annotée haute définition](https://tententen-vision-3iv2bzrnda-ew.a.run.app/v1/shooting/results/62a6f92628d2.jpg)"
        description: "Lien Markdown cliquable vers l'image annotée HD"
      viewer_url_html:
        type: string
        example: "<a href=\"https://tententen-vision-3iv2bzrnda-ew.a.run.app/v1/shooting/results/62a6f92628d2/view\" target=\"_blank\" rel=\"noopener noreferrer\">Page d'inspection interactive</a>"
        description: "Balise HTML d'ancrage prête à intégrer dans un webview ou une page web"
      annotated_image_url_html:
        type: string
        example: "<a href=\"https://tententen-vision-3iv2bzrnda-ew.a.run.app/v1/shooting/results/62a6f92628d2.jpg\" target=\"_blank\" rel=\"noopener noreferrer\">Image annotée haute définition</a>"
        description: "Balise HTML vers l'image annotée HD"
      links:
        $ref: "#/definitions/ShootingLinks"
      group_stats:
        $ref: "#/definitions/ShootingGroupStats"
      impacts:
        type: array
        items:
          $ref: "#/definitions/ShootingImpact"
        description: "Liste détaillée de chaque perforation détectée et scorée"
      zones:
        type: array
        items:
          $ref: "#/definitions/ShootingZonePolygon"
        description: "Polygones vectoriels des 10 zones épousant fidèlement les traits réels"
      svg_overlay:
        type: string
        example: "<svg viewBox=\"0 0 3024 3024\" xmlns=\"http://www.w3.org/2000/svg\">...</svg>"
        description: "Calque vectoriel SVG superposable pixel pour pixel sur la photo originale"
      execution_time_ms:
        type: number
        example: 412.5
        description: "Temps total de traitement de l'image en millisecondes"

  ShootingLinks:
    type: object
    description: "Ensemble de liens d'accès direct au visualiseur et aux images annotées"
    properties:
      viewer:
        $ref: "#/definitions/ShootingLinkItem"
      annotated_image:
        $ref: "#/definitions/ShootingLinkItem"

  ShootingLinkItem:
    type: object
    description: "Lien de consultation avec libellé, URL directe, Markdown et HTML"
    properties:
      label:
        type: string
        example: "Page d'inspection interactive"
      url:
        type: string
        example: "https://tententen-vision-3iv2bzrnda-ew.a.run.app/v1/shooting/results/62a6f92628d2/view"
      markdown:
        type: string
        example: "[Page d'inspection interactive](https://tententen-vision-3iv2bzrnda-ew.a.run.app/v1/shooting/results/62a6f92628d2/view)"
      html:
        type: string
        example: "<a href=\"https://tententen-vision-3iv2bzrnda-ew.a.run.app/v1/shooting/results/62a6f92628d2/view\" target=\"_blank\">Page d'inspection interactive</a>"

  ShootingGroupStats:
    type: object
    description: "Statistiques balistiques du groupement d'impacts"
    properties:
      center_x:
        type: number
        example: 1515.2
      center_y:
        type: number
        example: 1510.8
      dispersion_radius_mm:
        type: number
        example: 24.6
        description: "Rayon de dispersion moyen en millimètres"
      group_size_mm:
        type: number
        example: 48.2
        description: "Écart maximal entre les deux impacts les plus distants (centre à centre)"
      group_size_moa:
        type: number
        example: 3.31
        description: "Taille angulaire du groupement en Minutes d'Angle (MOA)"

  ShootingImpact:
    type: object
    description: "Détail balistique d'un impact de projectile"
    properties:
      id:
        type: integer
        example: 1
      x:
        type: number
        example: 1520.5
      y:
        type: number
        example: 1500.2
      radius_px:
        type: number
        example: 27.2
      score:
        type: integer
        example: 10
      is_inner_ten:
        type: boolean
        example: true
        description: "Indique si l'impact touche ou englobe la mouche (10+)"
      is_cordon:
        type: boolean
        example: false
        description: "Indique si l'impact coupe le cordonnet de la zone supérieure"
      is_suspect:
        type: boolean
        example: false
        description: "Indique si l'impact est suspect (micro-déchirure, éclat de carton hors calibre)"
      confidence:
        type: number
        example: 0.99

  ShootingZonePolygon:
    type: object
    description: "Polygone délimitant une zone de points suivant les traits réels imprimés"
    properties:
      score:
        type: integer
        example: 10
      observed_ratio:
        type: number
        example: 0.25
      points:
        type: array
        items:
          type: array
          items:
            type: number
        description: "Coordonnées [x, y] des sommets du contour de zone"
