{
 "openapi": "3.1.0",
 "info": {
  "title": "Reglova · API",
  "version": "1.0.0",
  "description": "API REST de Reglova. Authentification : `Authorization: Bearer <jeton>`, soit un jeton de session (POST /api/auth/login), soit une **clé d'API** `gmao_…` créée par un administrateur pour une intégration (ERP, supervision, BI) ; la clé agit au nom de son compte (rôle et usines).\n\n**Webhooks** : chaque événement du journal d'audit (`work_order.create`, `work_order.transition`, `purchase.receive`, `inspection.result`…) peut être poussé en POST JSON vers une URL. En-têtes `X-GMAO-Event`, `X-GMAO-Delivery` et `X-GMAO-Signature: sha256=<HMAC-SHA256 du corps avec le secret du webhook>`. Trois tentatives (immédiate, +5 s, +30 s). Corps : schéma `WebhookPayload`.\n\n**Rapports et Power BI** : `GET /api/reports` renvoie les indicateurs calculés côté serveur sur tout l'historique, pour une période en jours UTC entiers. Pour Power BI ou un entrepôt de données, interroger cette route (JSON) avec une clé d'API dans l'en-tête `Authorization: Bearer gmao_…` ; éviter `?token=`, qui finit dans les journaux des serveurs. Les exports CSV préfixent d'une apostrophe les textes qui ressemblent à des formules : préférer le JSON pour un traitement automatique.\n\n**Temps réel** : WebSocket `GET /api/live?token=…` (deltas et KPI).\n\n**Hors ligne** : l'application terrain met chaque geste en file sur l'appareil et le rejoue au retour du réseau. Les commandes de terrain acceptent `at`, l'heure du geste : 48 h au plus dans le passé, un léger futur est ramené à l'heure du serveur, au-delà d'un jour dans le futur refus (code clock). Les contrôles de sécurité (consignation, permis, habilitations, surveillance incendie) sont évalués à cette heure. Rejouer un même geste est sans effet ; les créations portent une `client_ref`. Chaque refus porte un `code` stable. L'audit garde `at`, `received_at` et `deferred`."
 },
 "servers": [
  {
   "url": "/"
  }
 ],
 "security": [
  {
   "bearer": []
  }
 ],
 "components": {
  "securitySchemes": {
   "bearer": {
    "type": "http",
    "scheme": "bearer"
   },
   "platform": {
    "type": "http",
    "scheme": "bearer",
    "description": "Jeton de l'éditeur (PLATFORM_TOKEN)"
   }
  },
  "schemas": {
   "Error": {
    "type": "object",
    "properties": {
     "error": {
      "type": "string"
     },
     "code": {
      "type": "string",
      "description": "Code stable du refus (closed, children_open, too_old, clock, permit, lockout, lockout_active, lockout_missing (clôture d'un OT jamais consigné ni vérifié), qualification, procedure_changed, notes_changed, fire_watch, order, timer_running, no_timer, not_planned, codes (mode ou cause de défaillance manquant), mode_class (mode non admis pour la classe de l'équipement), hygiene_checks, signature, close_with_report, machine_stopped (micro-arrêt pendant un arrêt déjà compté), not_stopped (remise en service d'une machine non arrêtée), in_progress (remise en service pendant une intervention), hygiene_pending (remise en service avant la levée d'hygiène), shortage, nothing_planned, nothing_to_issue, tech_busy, tech_absent, no_technician (affectation : technicien occupé, absent, aucun de libre), followup_limit, followup_too_soon, rating_open, rating_done, rating_expired (portail QR : relance et note), escalated (étape déjà escaladée dont la réparation n'est plus en mémoire), calc_version (calcul joint fait avec une autre version des formules), out_of_range (entrée de calcul hors bornes : `field`, `min`, `max`), rate_limited, not_found, forbidden, invalid, too_large, rejected…). shortage : `error` liste les manques « not enough stock: CODE missing 4 kg; … »"
     }
    },
    "required": [
     "error",
     "code"
    ]
   },
   "FailureMode": {
    "enum": [
     "breakdown",
     "leak",
     "noise",
     "overheating",
     "electrical",
     "sensor",
     "blockage",
     "wear",
     "adjustment",
     "other"
    ],
    "description": "Mode de défaillance ; les modes admis dépendent de la classe d'équipement"
   },
   "DetectionMethod": {
    "enum": [
     "periodic_maintenance",
     "functional_test",
     "inspection",
     "periodic_condition_monitoring",
     "continuous_condition_monitoring",
     "production_interference",
     "casual_observation",
     "corrective_maintenance",
     "on_demand",
     "other"
    ],
    "description": "Méthode de détection ISO 14224 (tableau B.4, codes 1 à 10)"
   },
   "Codes": {
    "type": "object",
    "properties": {
     "failure_mode": {
      "$ref": "#/components/schemas/FailureMode"
     },
     "mechanism": {
      "type": "string",
      "description": "Mécanisme ISO 14224 (tableau B.2)"
     },
     "cause": {
      "type": "string",
      "description": "Cause ISO 14224 (tableau B.3)"
     },
     "activity": {
      "type": "string",
      "description": "Activité ISO 14224 (tableau B.5)"
     },
     "detection": {
      "description": "Déduite de l'origine de l'OT (réserve de contrôle, mesure, portail QR, panne, plan), jamais d'une fréquence ; null si inconnue",
      "oneOf": [
       {
        "$ref": "#/components/schemas/DetectionMethod"
       },
       {
        "type": "null"
       }
      ]
     },
     "inherited": {
      "type": "boolean",
      "description": "Réparation définitive : codes complets repris de la réparation temporaire"
     },
     "allowed_modes": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/FailureMode"
      },
      "description": "Modes admis pour la classe de l'équipement, du plus au moins probable"
     },
     "typical": {
      "type": "object",
      "description": "Par mode admis : mécanisme et activité complétés par le serveur à la clôture",
      "additionalProperties": {
       "type": "object",
       "properties": {
        "mechanism": {
         "type": "string"
        },
        "activity": {
         "type": "string"
        }
       }
      }
     }
    }
   },
   "WorkOrder": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string"
     },
     "code": {
      "type": "string"
     },
     "site_id": {
      "type": "string"
     },
     "asset_id": {
      "type": "string"
     },
     "kind": {
      "type": "string"
     },
     "priority": {
      "enum": [
       "low",
       "medium",
       "high",
       "critical"
      ]
     },
     "status": {
      "enum": [
       "requested",
       "approved",
       "planned",
       "in_progress",
       "done"
      ]
     },
     "title": {
      "type": "string"
     },
     "requested_by": {
      "type": "string"
     },
     "assignee_id": {
      "type": "string"
     },
     "history": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "status": {
         "type": "string"
        },
        "at": {
         "type": "integer",
         "format": "int64",
         "description": "Horodatage en millisecondes (epoch)"
        }
       }
      }
     },
     "report": {
      "type": "object"
     },
     "parts": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/PartUse"
      }
     },
     "operations": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Operation"
      }
     },
     "parent_id": {
      "type": [
       "string",
       "null"
      ],
      "description": "OT parent (arborescence)"
     },
     "kits": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "kit_id": {
         "type": "string"
        },
        "qty": {
         "type": "number",
         "description": "Kits prévus ou réservés (cumul, unité de gestion du kit)"
        },
        "issued": {
         "type": "number",
         "description": "Kits sortis (cumul) : chaque sortie déstocke la différence entre deux cumuls de composants"
        }
       }
      }
     },
     "time_entries": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "who": {
         "type": "string"
        },
        "started_at": {
         "type": "integer",
         "format": "int64",
         "description": "Horodatage en millisecondes (epoch)"
        },
        "ended_at": {
         "type": "integer",
         "format": "int64",
         "description": "Horodatage en millisecondes (epoch)"
        },
        "operation": {
         "type": "integer",
         "description": "Numéro de l'opération visée (facultatif)"
        }
       }
      }
     },
     "subcontract": {
      "type": "object"
     },
     "lockout": {
      "type": "object"
     },
     "hygiene": {
      "type": "object"
     },
     "permits": {
      "type": "array",
      "items": {
       "type": "object"
      }
     },
     "checklist": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/ChecklistItem"
      }
     },
     "escalated_from": {
      "description": "Réparation urgente créée depuis une étape de checklist en échec : OT et rang de l'étape (0 pour la première)",
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "work_order_id": {
          "type": "string"
         },
         "step": {
          "type": "integer"
         }
        }
       },
       {
        "type": "null"
       }
      ]
     },
     "followups": {
      "type": "array",
      "items": {
       "type": "integer",
       "format": "int64",
       "description": "Horodatage en millisecondes (epoch)"
      },
      "description": "Relances du demandeur sans compte (portail QR), heure de chacune ; absent si aucune"
     },
     "calculations": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/CalcRecord"
      },
      "description": "Calculs joints (20 au plus), recalculés par le serveur ; absent si aucun"
     },
     "satisfaction": {
      "description": "Note du demandeur sans compte, donnée après la clôture (absente sinon)",
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "score": {
          "type": "integer",
          "minimum": 1,
          "maximum": 5
         },
         "comment": {
          "type": "string"
         },
         "at": {
          "type": "integer",
          "format": "int64",
          "description": "Horodatage en millisecondes (epoch)"
         }
        }
       },
       {
        "type": "null"
       }
      ]
     },
     "stop": {
      "description": "Panne déclarée « machine arrêtée » : l'OT tient la machine « En panne » tant qu'il est ouvert et que `until` est null. `minutes` : part de l'arrêt de la machine imputée à cet OT. Les arrêts des OT d'une machine qui se chevauchent forment un seul arrêt, mesuré une fois ; sa durée va à l'OT qui le termine (ou à l'ouvreur en cas de remise en service), moins ce que les autres OT en comptent déjà. Reprise au compte rendu si la clôture ne déclare pas d'arrêt",
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "since": {
          "type": "integer",
          "format": "int64",
          "description": "Horodatage en millisecondes (epoch)"
         },
         "until": {
          "oneOf": [
           {
            "type": "integer",
            "format": "int64",
            "description": "Horodatage en millisecondes (epoch)"
           },
           {
            "type": "null"
           }
          ]
         },
         "minutes": {
          "type": "integer"
         }
        }
       },
       {
        "type": "null"
       }
      ]
     }
    }
   },
   "ChecklistItem": {
    "type": "object",
    "properties": {
     "label": {
      "type": "string"
     },
     "done": {
      "type": "boolean"
     },
     "failed": {
      "type": "boolean",
      "description": "Étape faite mais non conforme (absent : conforme)"
     },
     "escalated_to": {
      "type": [
       "string",
       "null"
      ],
      "description": "Réparation urgente créée depuis l'étape"
     },
     "key": {
      "type": "string",
      "description": "Clé stable de l'étape (`calc:<id>` pour l'étape d'un calcul joint) ; absente sinon"
     }
    },
    "required": [
     "label",
     "done"
    ]
   },
   "CalcRecord": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string"
     },
     "calc_id": {
      "type": "string"
     },
     "version": {
      "type": "integer"
     },
     "inputs": {
      "type": "object",
      "description": "Entrées retenues (seules les clés du catalogue)"
     },
     "values": {
      "type": "array",
      "items": {
       "type": "array",
       "minItems": 2,
       "maxItems": 2
      },
      "description": "Sorties recalculées par le serveur, [clé, valeur] dans l'ordre du catalogue"
     },
     "warnings": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "step": {
      "type": "string",
      "description": "Libellé de l'étape créée, dans la langue de l'usine (absent sans étape)"
     },
     "by": {
      "type": "string"
     },
     "at": {
      "type": "integer",
      "format": "int64",
      "description": "Horodatage en millisecondes (epoch)"
     },
     "client_ref": {
      "type": "string"
     }
    },
    "required": [
     "id",
     "calc_id",
     "version",
     "inputs",
     "values",
     "warnings",
     "by",
     "at"
    ]
   },
   "CalcOutcome": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string"
     },
     "version": {
      "type": "integer"
     },
     "values": {
      "type": "object",
      "description": "Sorties, dans l'ordre du catalogue"
     },
     "warnings": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "steps": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "key": {
         "type": "string"
        },
        "value": {
         "type": "number"
        },
        "unit": {
         "type": "string"
        }
       }
      }
     }
    }
   },
   "ReportSuggestion": {
    "type": "object",
    "properties": {
     "text": {
      "type": "string"
     },
     "count": {
      "type": "integer",
      "description": "Pannes passées qui ont employé cette description (0 : symptôme du catalogue de la classe)"
     }
    },
    "required": [
     "text",
     "count"
    ]
   },
   "PublicAsset": {
    "type": "object",
    "properties": {
     "code": {
      "type": "string"
     },
     "name": {
      "type": "string"
     },
     "site": {
      "type": "string"
     },
     "locale": {
      "type": "string"
     },
     "open_requests": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "suggestions": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "description": "Descriptions fréquentes pour la classe de l'équipement (6 au plus), apprises de sa seule usine, dans sa langue"
     }
    }
   },
   "PublicTracking": {
    "type": "object",
    "properties": {
     "code": {
      "type": "string"
     },
     "title": {
      "type": "string"
     },
     "asset": {
      "type": "string"
     },
     "status": {
      "type": "string"
     },
     "history": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "status": {
         "type": "string"
        },
        "at": {
         "type": "integer",
         "format": "int64",
         "description": "Horodatage en millisecondes (epoch)"
        }
       }
      }
     },
     "remedy": {
      "type": [
       "string",
       "null"
      ]
     },
     "followups": {
      "type": "integer",
      "description": "Relances déjà faites"
     },
     "followups_left": {
      "type": "integer",
      "description": "Relances restantes (0 si la demande est close)"
     },
     "next_followup_at": {
      "description": "Heure à partir de laquelle une relance est admise (null : plus de relance possible)",
      "oneOf": [
       {
        "type": "integer",
        "format": "int64",
        "description": "Horodatage en millisecondes (epoch)"
       },
       {
        "type": "null"
       }
      ]
     },
     "can_rate": {
      "type": "boolean",
      "description": "Demande close, pas encore notée, dans les 30 jours"
     },
     "rating": {
      "type": [
       "integer",
       "null"
      ],
      "description": "Note déjà donnée (1 à 5)"
     }
    }
   },
   "Asset": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string"
     },
     "site_id": {
      "type": "string"
     },
     "line_id": {
      "type": "string"
     },
     "code": {
      "type": "string"
     },
     "name": {
      "type": "string"
     },
     "kind": {
      "type": "string"
     },
     "status": {
      "enum": [
       "running",
       "idle",
       "degraded",
       "down",
       "maintenance"
      ],
      "description": "Déduit des faits : « maintenance » (intervention démarrée, machine consignée pour un OT ouvert ou levée d'hygiène en attente) > « down » (un OT ouvert tient la machine arrêtée) > « degraded » (point de mesure en alarme ou OT conditionnel ouvert) > « running ». Chaque changement est diffusé sur /api/live (restreint aux usines du compte). Il est journalisé (`asset.status`) quand il vient d'une commande, du portail QR, de l'IoT (acteur : « IoT » pour MQTT et OPC UA, « <compte> (IoT) » pour HTTP) ou du démarrage ; les pas du simulateur (démo) ne sont pas journalisés"
     },
     "stopped_since": {
      "description": "Début de l'arrêt en cours (null : machine non arrêtée)",
      "oneOf": [
       {
        "type": "integer",
        "format": "int64",
        "description": "Horodatage en millisecondes (epoch)"
       },
       {
        "type": "null"
       }
      ]
     },
     "restarted_at": {
      "description": "Fin du dernier arrêt (clôture ou remise en service)",
      "oneOf": [
       {
        "type": "integer",
        "format": "int64",
        "description": "Horodatage en millisecondes (epoch)"
       },
       {
        "type": "null"
       }
      ]
     },
     "unassigned_requests": {
      "type": "integer",
      "description": "OT « Demande » sans technicien (ni affecté ni planifié) : signe distinct, sans effet sur l'état"
     },
     "specs": {
      "type": "object",
      "additionalProperties": {
       "type": "number"
      },
      "description": "Caractéristiques (clés fermées de calc::specs : power_kw, speed_rpm, voltage_v, current_a, cos_phi, efficiency, poles, frequency_hz, ratio, bore_mm, rod_mm, flow_m3h, head_m, rotor_mass_kg, bearing_d_mm, bearing_od_mm, bearing_width_mm, bearing_c_kn, bearing_c0_kn, bearing_f0), bornées ; absent si vide"
     },
     "criticality": {
      "enum": [
       "A",
       "B",
       "C"
      ]
     },
     "health": {
      "type": "number"
     },
     "oee": {
      "type": "number"
     },
     "mtbf_hours": {
      "type": "number"
     },
     "mttr_minutes": {
      "type": "number"
     },
     "bom": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "part_id": {
         "type": "string"
        },
        "qty": {
         "type": "number"
        }
       }
      }
     },
     "isolation_points": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/IsolationPoint"
      }
     },
     "hazards": {
      "type": "array",
      "items": {
       "enum": [
        "atex",
        "confined_space",
        "height"
       ]
      }
     },
     "model3d": {
      "description": "Modèle 3D propre (null : modèle procédural du type)",
      "oneOf": [
       {
        "$ref": "#/components/schemas/AssetModel"
       },
       {
        "type": "null"
       }
      ]
     }
    }
   },
   "IsolationPoint": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string"
     },
     "energy": {
      "enum": [
       "electrical",
       "pneumatic",
       "hydraulic",
       "mechanical",
       "thermal",
       "chemical"
      ]
     },
     "label": {
      "type": "string"
     },
     "anchor": {
      "description": "[x, y, z] en m dans le repère du modèle 3D, ou null",
      "oneOf": [
       {
        "type": "array",
        "items": {
         "type": "number"
        },
        "minItems": 3,
        "maxItems": 3
       },
       {
        "type": "null"
       }
      ]
     }
    }
   },
   "ModelPart": {
    "type": "object",
    "properties": {
     "shape": {
      "enum": [
       "box",
       "cylinder",
       "sphere",
       "lathe"
      ]
     },
     "size": {
      "type": "array",
      "items": {
       "type": "number"
      },
      "minItems": 3,
      "maxItems": 3,
      "description": "box : [largeur x, hauteur y, profondeur z] en m"
     },
     "radius": {
      "type": "number",
      "description": "box : rayon des arêtes (facultatif) ; cylinder : rayon du bas ; sphere : rayon"
     },
     "radius_top": {
      "type": "number",
      "description": "cylinder : rayon du haut (facultatif, = radius)"
     },
     "height": {
      "type": "number",
      "description": "cylinder : hauteur selon y local"
     },
     "profile": {
      "type": "array",
      "items": {
       "type": "array",
       "items": {
        "type": "number"
       },
       "minItems": 2,
       "maxItems": 2
      },
      "description": "lathe : 2 à 32 points [rayon, y], révolution autour de y, origine en `at`"
     },
     "at": {
      "type": "array",
      "items": {
       "type": "number"
      },
      "minItems": 3,
      "maxItems": 3,
      "description": "Centre de la pièce (lathe : origine du profil), en m"
     },
     "rotation": {
      "type": "array",
      "items": {
       "type": "number"
      },
      "minItems": 3,
      "maxItems": 3,
      "description": "Euler XYZ en radians (0 par défaut)"
     },
     "color": {
      "type": "string",
      "description": "body | body_alt | steel | dark | accent | glass | glow | warning | wood | cardboard | #rrggbb"
     },
     "part": {
      "type": "string",
      "description": "Sous-ensemble maintenable (motor, gearbox, hmi…), 40 caractères max"
     }
    },
    "required": [
     "shape",
     "at",
     "color"
    ]
   },
   "Recipe": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string"
     },
     "parts": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/ModelPart"
      },
      "minItems": 1,
      "maxItems": 250
     }
    },
    "required": [
     "name",
     "parts"
    ]
   },
   "AssetModel": {
    "description": "Repère de la machine : origine au centre de l'emprise au sol, y vers le haut, flux produit +x, côté opérateur +z (m)",
    "oneOf": [
     {
      "type": "object",
      "properties": {
       "source": {
        "const": "recipe"
       },
       "recipe": {
        "$ref": "#/components/schemas/Recipe"
       },
       "source_url": {
        "type": [
         "string",
         "null"
        ]
       },
       "generated_at": {
        "type": "integer",
        "format": "int64",
        "description": "Horodatage en millisecondes (epoch)"
       },
       "footprint": {
        "type": "array",
        "items": {
         "type": "number"
        },
        "minItems": 3,
        "maxItems": 3,
        "description": "Boîte englobante [w, h, d] en m"
       }
      },
      "required": [
       "source",
       "recipe",
       "source_url",
       "generated_at",
       "footprint"
      ]
     },
     {
      "type": "object",
      "properties": {
       "source": {
        "const": "gltf"
       },
       "attachment_id": {
        "type": "string",
        "description": "Pièce jointe glTF/GLB : GET /api/files/{id}"
       },
       "source_url": {
        "type": [
         "string",
         "null"
        ]
       },
       "scale": {
        "type": "number"
       },
       "rotation": {
        "type": "number",
        "description": "Radians autour de y"
       },
       "footprint": {
        "type": "array",
        "items": {
         "type": "number"
        },
        "minItems": 3,
        "maxItems": 3,
        "description": "Boîte [w, h, d] en m (provisoire : boîte type du genre d'équipement)"
       }
      },
      "required": [
       "source",
       "attachment_id",
       "source_url",
       "scale",
       "rotation",
       "footprint"
      ]
     }
    ]
   },
   "ModelResult": {
    "type": "object",
    "properties": {
     "asset_ids": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "model": {
      "oneOf": [
       {
        "$ref": "#/components/schemas/AssetModel"
       },
       {
        "type": "null"
       }
      ]
     }
    },
    "required": [
     "asset_ids",
     "model"
    ]
   },
   "AnchorsInput": {
    "type": "object",
    "properties": {
     "isolation": {
      "type": "object",
      "additionalProperties": {
       "description": "[x, y, z] en m dans le repère du modèle 3D, ou null",
       "oneOf": [
        {
         "type": "array",
         "items": {
          "type": "number"
         },
         "minItems": 3,
         "maxItems": 3
        },
        {
         "type": "null"
        }
       ]
      },
      "description": "id du point d'isolement → [x, y, z] ou null"
     },
     "points": {
      "type": "object",
      "additionalProperties": {
       "description": "[x, y, z] en m dans le repère du modèle 3D, ou null",
       "oneOf": [
        {
         "type": "array",
         "items": {
          "type": "number"
         },
         "minItems": 3,
         "maxItems": 3
        },
        {
         "type": "null"
        }
       ]
      },
      "description": "id du point de mesure → [x, y, z] ou null"
     }
    }
   },
   "Calendar": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string"
     },
     "kind": {
      "enum": [
       "personal",
       "site",
       "team"
      ]
     },
     "site_id": {
      "type": "string"
     },
     "owner_user_id": {
      "type": "string"
     },
     "team_id": {
      "type": "string"
     },
     "name": {
      "type": "string"
     },
     "tz": {
      "type": "string"
     },
     "default_reminders": {
      "type": "array",
      "items": {
       "type": "integer"
      }
     },
     "archived": {
      "type": "boolean"
     }
    }
   },
   "EventInput": {
    "type": "object",
    "properties": {
     "calendar_id": {
      "type": "string"
     },
     "title": {
      "type": "string"
     },
     "description": {
      "type": "string"
     },
     "location": {
      "type": "string"
     },
     "when": {
      "description": "Horaire : {kind: timed, start: AAAA-MM-JJTHH:MM:SS (heure murale), tz: fuseau IANA, minutes} ou {kind: all_day, start: AAAA-MM-JJ, days}",
      "type": "object"
     },
     "rule": {
      "type": "string",
      "description": "RRULE RFC 5545, sous-ensemble : FREQ DAILY à YEARLY, INTERVAL, COUNT ou UNTIL, BYDAY (rang en mensuel, en annuel avec BYMONTH), BYMONTHDAY, BYMONTH, WKST ; toute autre partie refusée avec son nom (422)"
     },
     "exdates": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "rdates": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "attendees": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "technician_id": {
         "type": "string"
        },
        "user_id": {
         "type": "string"
        },
        "name": {
         "type": "string"
        }
       }
      }
     },
     "reminders": {
      "type": "array",
      "items": {
       "type": "integer"
      }
     },
     "busy": {
      "type": "boolean"
     },
     "links": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "kind": {
         "enum": [
          "asset",
          "work_order"
         ]
        },
        "id": {
         "type": "string"
        }
       }
      }
     },
     "accept_conflicts": {
      "type": "boolean"
     }
    },
    "required": [
     "calendar_id",
     "title",
     "when"
    ]
   },
   "Event": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string"
     },
     "uid": {
      "type": "string"
     },
     "calendar_id": {
      "type": "string"
     },
     "site_id": {
      "type": "string"
     },
     "sequence": {
      "type": "integer"
     },
     "status": {
      "enum": [
       "confirmed",
       "tentative",
       "cancelled"
      ]
     },
     "title": {
      "type": "string"
     },
     "when": {
      "description": "Horaire : {kind: timed, start: AAAA-MM-JJTHH:MM:SS (heure murale), tz: fuseau IANA, minutes} ou {kind: all_day, start: AAAA-MM-JJ, days}",
      "type": "object"
     },
     "rule": {
      "type": "string"
     },
     "exdates": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "rdates": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "overrides": {
      "type": "object"
     },
     "organizer": {
      "type": "string"
     },
     "attendees": {
      "type": "array",
      "items": {
       "type": "object"
      }
     },
     "reminders": {
      "type": "array",
      "items": {
       "type": "integer"
      }
     },
     "busy": {
      "type": "boolean"
     },
     "links": {
      "type": "array",
      "items": {
       "type": "object"
      }
     },
     "split_from": {
      "type": "string"
     },
     "created_at": {
      "type": "integer",
      "format": "int64",
      "description": "Horodatage en millisecondes (epoch)"
     },
     "updated_at": {
      "type": "integer",
      "format": "int64",
      "description": "Horodatage en millisecondes (epoch)"
     }
    }
   },
   "Occurrence": {
    "type": "object",
    "properties": {
     "key": {
      "type": "string"
     },
     "source": {
      "type": "string"
     },
     "record_id": {
      "type": "string"
     },
     "calendar_id": {
      "type": "string"
     },
     "site_id": {
      "type": "string"
     },
     "title": {
      "type": "string"
     },
     "start": {
      "type": "integer",
      "format": "int64",
      "description": "Horodatage en millisecondes (epoch)"
     },
     "end": {
      "type": "integer",
      "format": "int64",
      "description": "Horodatage en millisecondes (epoch)"
     },
     "all_day": {
      "type": "boolean"
     },
     "local_date": {
      "type": "string"
     },
     "tz": {
      "type": "string"
     },
     "recurring": {
      "type": "boolean"
     },
     "forecast": {
      "type": "boolean"
     },
     "deviation": {
      "enum": [
       "overdue",
       "moved"
      ]
     },
     "my_partstat": {
      "type": "string"
     }
    }
   },
   "Conflict": {
    "type": "object",
    "properties": {
     "kind": {
      "enum": [
       "absent",
       "overlap",
       "overload",
       "asset_busy"
      ]
     },
     "technician_id": {
      "type": "string"
     },
     "start": {
      "type": "integer",
      "format": "int64",
      "description": "Horodatage en millisecondes (epoch)"
     },
     "reason": {
      "type": "string"
     },
     "event_id": {
      "type": "string"
     },
     "date": {
      "type": "string"
     },
     "hours": {
      "type": "number"
     },
     "capacity": {
      "type": "number"
     },
     "asset_id": {
      "type": "string"
     },
     "work_order_id": {
      "type": "string"
     }
    }
   },
   "Part": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string"
     },
     "site_id": {
      "type": "string"
     },
     "code": {
      "type": "string"
     },
     "name": {
      "type": "string"
     },
     "unit": {
      "type": "string",
      "description": "Unité de gestion (u, kg, g, L, m, paquet…)"
     },
     "unit_cost": {
      "type": "number"
     },
     "on_hand": {
      "type": "number"
     },
     "min_qty": {
      "type": "number"
     },
     "max_qty": {
      "type": "number"
     },
     "location": {
      "type": "string"
     },
     "supplier": {
      "type": "string"
     },
     "critical": {
      "type": "boolean"
     },
     "last_move_at": {
      "type": "integer",
      "format": "int64",
      "description": "Horodatage en millisecondes (epoch)"
     },
     "specs": {
      "type": "object",
      "additionalProperties": {
       "type": "number"
      },
      "description": "Caractéristiques (clés fermées de calc::specs : power_kw, speed_rpm, voltage_v, current_a, cos_phi, efficiency, poles, frequency_hz, ratio, bore_mm, rod_mm, flow_m3h, head_m, rotor_mass_kg, bearing_d_mm, bearing_od_mm, bearing_width_mm, bearing_c_kn, bearing_c0_kn, bearing_f0), bornées ; absent si vide"
     },
     "pack": {
      "description": "Conditionnement d'achat ou de sortie : 1 `unit` = `factor` unités de gestion (1 paquet = 25 u) ; null : aucun",
      "oneOf": [
       {
        "type": "object",
        "properties": {
         "unit": {
          "type": "string"
         },
         "factor": {
          "type": "number"
         }
        },
        "required": [
         "unit",
         "factor"
        ]
       },
       {
        "type": "null"
       }
      ]
     },
     "components": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "part_id": {
         "type": "string"
        },
        "qty": {
         "type": "number"
        }
       },
       "required": [
        "part_id",
        "qty"
       ]
      },
      "description": "Article composé (kit) : composants pour `kit_base` unités du kit, déstockés à la sortie. Un kit n'a pas de stock propre ; un composant n'est jamais un kit. À la modification, absent : composition inchangée"
     },
     "kit_base": {
      "type": "number",
      "description": "Quantité de kit (unité de gestion) pour laquelle la composition est donnée (1 990 paquets de 25 = 49 750 u)"
     }
    }
   },
   "Operation": {
    "type": "object",
    "properties": {
     "seq": {
      "type": "integer",
      "description": "Numéro de phase (10, 20, 30…), identifiant stable dans l'OT"
     },
     "title": {
      "type": "string"
     },
     "trade": {
      "enum": [
       "mechanic",
       "electrician",
       "automation"
      ]
     },
     "planned_minutes": {
      "type": "integer"
     },
     "status": {
      "enum": [
       "todo",
       "in_progress",
       "done"
      ]
     },
     "done_by": {
      "type": [
       "string",
       "null"
      ]
     },
     "done_at": {
      "type": "integer",
      "format": "int64",
      "description": "Horodatage en millisecondes (epoch)"
     }
    }
   },
   "OperationTemplate": {
    "type": "object",
    "properties": {
     "title": {
      "type": "string"
     },
     "trade": {
      "enum": [
       "mechanic",
       "electrician",
       "automation"
      ]
     },
     "planned_minutes": {
      "type": "integer"
     }
    },
    "required": [
     "title",
     "trade"
    ]
   },
   "Rollup": {
    "type": "object",
    "properties": {
     "work_order_id": {
      "type": "string"
     },
     "children": {
      "type": "integer",
      "description": "Sous-OT de tous niveaux"
     },
     "open_children": {
      "type": "integer"
     },
     "progress": {
      "type": "number",
      "description": "Avancement de l'arbre pondéré par la charge prévue, en %"
     },
     "planned_minutes": {
      "type": "number"
     },
     "actual_minutes": {
      "type": "number"
     },
     "cost": {
      "type": "object",
      "properties": {
       "labor": {
        "type": "number"
       },
       "parts": {
        "type": "number",
        "description": "Pièces sorties (coût figé)"
       },
       "external": {
        "type": "number"
       },
       "total": {
        "type": "number",
        "description": "Coût réel : main-d'œuvre + pièces sorties + sous-traitance"
       },
       "committed": {
        "type": "number",
        "description": "Engagé : pièces réservées pas encore sorties (hors total)"
       }
      }
     }
    }
   },
   "Window": {
    "type": "object",
    "properties": {
     "work_order_id": {
      "type": "string"
     },
     "remaining_minutes": {
      "type": "integer",
      "description": "Temps prévu restant (opérations pas encore faites), en minutes ; durées mises bout à bout en temps continu"
     },
     "earliest_start": {
      "type": "integer",
      "format": "int64",
      "description": "Début au plus tôt : maintenant (ou le démarrage réel), décalé par l'arrivée annoncée des pièces attendues"
     },
     "earliest_finish": {
      "type": "integer",
      "format": "int64",
      "description": "Fin au plus tôt (un parent ne finit pas avant ses sous-OT)"
     },
     "latest_finish": {
      "description": "Fin au plus tard : échéance de l'OT, sinon celle de son parent",
      "oneOf": [
       {
        "type": "integer",
        "format": "int64",
        "description": "Horodatage en millisecondes (epoch)"
       },
       {
        "type": "null"
       }
      ]
     },
     "latest_start": {
      "description": "Début au plus tard : fin au plus tard − temps restant",
      "oneOf": [
       {
        "type": "integer",
        "format": "int64",
        "description": "Horodatage en millisecondes (epoch)"
       },
       {
        "type": "null"
       }
      ]
     },
     "supply_by": {
      "description": "Appro au plus tard (= début au plus tard)",
      "oneOf": [
       {
        "type": "integer",
        "format": "int64",
        "description": "Horodatage en millisecondes (epoch)"
       },
       {
        "type": "null"
       }
      ]
     },
     "supply_at": {
      "description": "Arrivée annoncée de la dernière pièce attendue",
      "oneOf": [
       {
        "type": "integer",
        "format": "int64",
        "description": "Horodatage en millisecondes (epoch)"
       },
       {
        "type": "null"
       }
      ]
     },
     "supply": {
      "$ref": "#/components/schemas/SupplyState"
     },
     "slack_minutes": {
      "type": [
       "integer",
       "null"
      ],
      "description": "Marge : fin au plus tard − fin au plus tôt (négative : retard prévu)"
     },
     "late": {
      "type": "boolean"
     },
     "held_by": {
      "type": [
       "string",
       "null"
      ],
      "description": "Sous-OT qui repousse la fin au plus tôt (il finit après le travail propre de l'OT)"
     },
     "awaited": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "part_id": {
         "type": "string"
        },
        "code": {
         "type": "string"
        },
        "name": {
         "type": "string"
        },
        "unit": {
         "type": "string"
        },
        "qty": {
         "type": "number"
        },
        "available": {
         "type": "number"
        },
        "purchase_id": {
         "type": [
          "string",
          "null"
         ]
        },
        "purchase_code": {
         "type": [
          "string",
          "null"
         ]
        },
        "expected_at": {
         "oneOf": [
          {
           "type": "integer",
           "format": "int64",
           "description": "Horodatage en millisecondes (epoch)"
          },
          {
           "type": "null"
          }
         ]
        },
        "state": {
         "$ref": "#/components/schemas/SupplyState"
        }
       }
      }
     }
    }
   },
   "Amounts": {
    "type": "object",
    "properties": {
     "planned": {
      "type": "number"
     },
     "actual": {
      "type": "number"
     },
     "variance": {
      "type": "number",
      "description": "Écart : réel − prévu"
     }
    }
   },
   "Analysis": {
    "type": "object",
    "properties": {
     "work_order_id": {
      "type": "string"
     },
     "code": {
      "type": "string"
     },
     "title": {
      "type": "string"
     },
     "site_id": {
      "type": "string"
     },
     "asset_id": {
      "type": "string"
     },
     "kind": {
      "type": "string"
     },
     "status": {
      "type": "string"
     },
     "parent_id": {
      "type": [
       "string",
       "null"
      ]
     },
     "created_at": {
      "type": "integer",
      "format": "int64",
      "description": "Horodatage en millisecondes (epoch)"
     },
     "closed_at": {
      "oneOf": [
       {
        "type": "integer",
        "format": "int64",
        "description": "Horodatage en millisecondes (epoch)"
       },
       {
        "type": "null"
       }
      ]
     },
     "labor_minutes": {
      "$ref": "#/components/schemas/Amounts",
      "description": "Minutes : gamme (sinon estimation de charge) contre temps passés"
     },
     "labor_cost": {
      "$ref": "#/components/schemas/Amounts",
      "description": "Euros : temps prévus × coût horaire moyen du métier dans l'usine, contre temps passés × coût horaire de la personne"
     },
     "parts_cost": {
      "$ref": "#/components/schemas/Amounts",
      "description": "Euros : quantités prévues au coût moyen, contre consommé au coût figé des sorties"
     },
     "external_cost": {
      "$ref": "#/components/schemas/Amounts",
      "description": "Sous-traitance : devis (0 sans devis) contre montant facturé"
     },
     "total_cost": {
      "$ref": "#/components/schemas/Amounts"
     },
     "estimated": {
      "type": "boolean",
      "description": "Pas de gamme : la main-d'œuvre prévue est l'estimation de charge (hors écarts comparables)"
     },
     "unlogged": {
      "type": "boolean",
      "description": "OT clôturé sans temps saisi : réel de main-d'œuvre inconnu (hors écarts comparables)"
     },
     "external_quoted": {
      "type": "boolean",
      "description": "Sous-traitance avec devis (sinon, hors écarts comparables)"
     },
     "labor_rate_effect": {
      "type": "number",
      "description": "Effet taux : coût réel − temps réel au taux prévu du métier"
     },
     "variance": {
      "$ref": "#/components/schemas/Variances"
     },
     "operations": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "seq": {
         "type": [
          "integer",
          "null"
         ],
         "description": "Phase ; null : temps hors gamme (ou OT sans gamme)"
        },
        "title": {
         "type": "string"
        },
        "trade": {
         "type": "string"
        },
        "status": {
         "type": [
          "string",
          "null"
         ]
        },
        "minutes": {
         "$ref": "#/components/schemas/Amounts"
        },
        "cost": {
         "$ref": "#/components/schemas/Amounts"
        },
        "rate_effect": {
         "type": "number",
         "description": "Effet taux de l'opération"
        }
       }
      }
     },
     "parts": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "part_id": {
         "type": "string"
        },
        "code": {
         "type": "string"
        },
        "name": {
         "type": "string"
        },
        "unit": {
         "type": "string"
        },
        "kit_code": {
         "type": [
          "string",
          "null"
         ]
        },
        "planned_qty": {
         "type": "number"
        },
        "reserved_qty": {
         "type": "number"
        },
        "consumed_qty": {
         "type": "number"
        },
        "cost": {
         "$ref": "#/components/schemas/Amounts"
        }
       }
      }
     }
    }
   },
   "Variances": {
    "type": "object",
    "properties": {
     "labor_minutes": {
      "type": "number"
     },
     "labor_cost": {
      "type": "number"
     },
     "parts_cost": {
      "type": "number"
     },
     "external_cost": {
      "type": "number"
     },
     "total_cost": {
      "type": "number"
     }
    },
    "description": "Écarts comparables (réel − prévu), là où un vrai prévu existe"
   },
   "AnalysisSummary": {
    "type": "object",
    "properties": {
     "from": {
      "type": "integer",
      "format": "int64",
      "description": "Horodatage en millisecondes (epoch)"
     },
     "to": {
      "type": "integer",
      "format": "int64",
      "description": "Horodatage en millisecondes (epoch)"
     },
     "count": {
      "type": "integer"
     },
     "closed": {
      "type": "integer"
     },
     "labor_minutes": {
      "$ref": "#/components/schemas/Amounts"
     },
     "labor_cost": {
      "$ref": "#/components/schemas/Amounts"
     },
     "parts_cost": {
      "$ref": "#/components/schemas/Amounts"
     },
     "external_cost": {
      "$ref": "#/components/schemas/Amounts"
     },
     "total_cost": {
      "$ref": "#/components/schemas/Amounts"
     },
     "realized": {
      "$ref": "#/components/schemas/Variances",
      "description": "Écart réalisé : OT clôturés de la période"
     },
     "open": {
      "$ref": "#/components/schemas/Variances",
      "description": "OT ouverts : un écart négatif est un reste à consommer"
     },
     "estimated": {
      "type": "integer",
      "description": "OT sans gamme (main-d'œuvre estimée)"
     },
     "unlogged": {
      "type": "integer",
      "description": "OT clôturés sans temps saisi"
     },
     "by_trade": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "trade": {
         "type": "string"
        },
        "minutes": {
         "$ref": "#/components/schemas/Amounts"
        },
        "cost": {
         "$ref": "#/components/schemas/Amounts"
        }
       }
      }
     },
     "rows": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Analysis"
      },
      "description": "Une ligne par OT, sans le détail des opérations et des pièces"
     }
    }
   },
   "SupplyState": {
    "enum": [
     "ready",
     "in_stock",
     "ordered",
     "to_order",
     "late"
    ],
    "description": "ready : tout est réservé ou sorti ; in_stock : à réserver ; ordered : attendue à temps ; to_order : ni commandée ni en stock ; late : livraison annoncée après l'appro au plus tard (ou appro au plus tard dépassée sans date). ordered, to_order et late : « attente appro »"
   },
   "Explosion": {
    "type": "object",
    "properties": {
     "part_id": {
      "type": "string"
     },
     "qty": {
      "type": "number",
      "description": "Quantité demandée en unité de gestion"
     },
     "kit": {
      "type": "boolean"
     },
     "lines": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "part_id": {
         "type": "string"
        },
        "code": {
         "type": "string"
        },
        "name": {
         "type": "string"
        },
        "unit": {
         "type": "string"
        },
        "qty": {
         "type": "number",
         "description": "Quantité exacte (au millième)"
        },
        "available": {
         "type": "number",
         "description": "En stock − réservé sur les OT ouverts"
        },
        "missing": {
         "type": "number"
        }
       }
      }
     }
    }
   },
   "PartUse": {
    "type": "object",
    "properties": {
     "part_id": {
      "type": "string"
     },
     "qty": {
      "type": "number",
      "description": "Quantité prévue"
     },
     "unit_cost": {
      "type": "number"
     },
     "reserved_by": {
      "type": "string"
     },
     "issued_at": {
      "type": "integer",
      "format": "int64",
      "description": "Horodatage en millisecondes (epoch)"
     },
     "issued_qty": {
      "type": "number",
      "description": "Cumul des sorties (au-delà du prévu : écart)"
     },
     "reserved": {
      "type": "boolean",
      "description": "Faux : prévu, à approvisionner"
     },
     "settled": {
      "type": "boolean",
      "description": "Reliquat libéré"
     },
     "kit_id": {
      "type": [
       "string",
       "null"
      ],
      "description": "Kit dont la ligne est un composant"
     }
    }
   },
   "Purchase": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string"
     },
     "code": {
      "type": "string"
     },
     "site_id": {
      "type": "string"
     },
     "supplier": {
      "type": "string"
     },
     "status": {
      "enum": [
       "draft",
       "ordered",
       "partial",
       "received",
       "cancelled"
      ]
     },
     "lines": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "part_id": {
         "type": "string"
        },
        "qty": {
         "type": "number"
        },
        "unit_cost": {
         "type": "number"
        },
        "received_qty": {
         "type": "number"
        }
       }
      }
     },
     "work_order_id": {
      "type": "string"
     },
     "expected_at": {
      "type": "integer",
      "format": "int64",
      "description": "Horodatage en millisecondes (epoch)"
     }
    }
   },
   "Inspection": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string"
     },
     "site_id": {
      "type": "string"
     },
     "asset_id": {
      "type": "string"
     },
     "kind": {
      "enum": [
       "regulatory",
       "calibration",
       "functional"
      ]
     },
     "title": {
      "type": "string"
     },
     "reference": {
      "type": "string"
     },
     "body": {
      "type": "string"
     },
     "interval_days": {
      "type": "integer"
     },
     "due_at": {
      "type": "integer",
      "format": "int64",
      "description": "Horodatage en millisecondes (epoch)"
     },
     "active": {
      "type": "boolean"
     },
     "history": {
      "type": "array",
      "items": {
       "type": "object"
      }
     }
    }
   },
   "Contract": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string"
     },
     "site_id": {
      "type": "string"
     },
     "vendor_id": {
      "type": "string"
     },
     "reference": {
      "type": "string"
     },
     "title": {
      "type": "string"
     },
     "kind": {
      "type": "string"
     },
     "asset_ids": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "start_at": {
      "type": "integer",
      "format": "int64",
      "description": "Horodatage en millisecondes (epoch)"
     },
     "end_at": {
      "type": "integer",
      "format": "int64",
      "description": "Horodatage en millisecondes (epoch)"
     },
     "notice_days": {
      "type": "integer"
     },
     "auto_renew": {
      "type": "boolean"
     },
     "response_hours": {
      "type": "integer"
     },
     "annual_amount": {
      "type": "number"
     }
    }
   },
   "Vendor": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string"
     },
     "name": {
      "type": "string"
     },
     "kind": {
      "enum": [
       "contractor",
       "inspection_body",
       "supplier"
      ]
     },
     "contact": {
      "type": "string"
     },
     "email": {
      "type": "string"
     },
     "phone": {
      "type": "string"
     },
     "active": {
      "type": "boolean"
     }
    }
   },
   "Snapshot": {
    "type": "object",
    "properties": {
     "now": {
      "type": "integer",
      "format": "int64",
      "description": "Horodatage en millisecondes (epoch)"
     },
     "sites": {
      "type": "array",
      "items": {
       "type": "object"
      }
     },
     "lines": {
      "type": "array",
      "items": {
       "type": "object"
      }
     },
     "assets": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Asset"
      }
     },
     "work_orders": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/WorkOrder"
      }
     },
     "plans": {
      "type": "array",
      "items": {
       "type": "object"
      }
     },
     "parts": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Part"
      }
     },
     "purchases": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Purchase"
      }
     },
     "inspections": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Inspection"
      }
     },
     "contracts": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Contract"
      }
     },
     "vendors": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Vendor"
      }
     },
     "technicians": {
      "type": "array",
      "items": {
       "type": "object"
      }
     },
     "messages": {
      "type": "array",
      "items": {
       "type": "object"
      }
     }
    }
   },
   "Site": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string"
     },
     "code": {
      "type": "string"
     },
     "name": {
      "type": "string"
     },
     "activity": {
      "type": "string"
     },
     "address": {
      "type": "string"
     },
     "city": {
      "type": "string"
     },
     "country": {
      "type": "string"
     },
     "timezone": {
      "type": "string"
     },
     "locale": {
      "type": "string"
     },
     "sector": {
      "enum": [
       "agrifood",
       "automotive"
      ]
     },
     "origin": {
      "type": "object",
      "properties": {
       "x": {
        "type": "number"
       },
       "z": {
        "type": "number"
       }
      }
     },
     "footprint": {
      "type": "object",
      "properties": {
       "width": {
        "type": "number"
       },
       "depth": {
        "type": "number"
       }
      }
     },
     "aisle_z": {
      "type": "number"
     },
     "workshop": {
      "type": "object",
      "properties": {
       "x": {
        "type": "number"
       },
       "z": {
        "type": "number"
       }
      }
     }
    }
   },
   "SiteInput": {
    "type": "object",
    "properties": {
     "code": {
      "type": "string"
     },
     "name": {
      "type": "string"
     },
     "activity": {
      "type": "string"
     },
     "address": {
      "type": "string"
     },
     "city": {
      "type": "string"
     },
     "country": {
      "type": "string",
      "description": "ISO 3166-1 alpha-2"
     },
     "timezone": {
      "type": "string"
     },
     "locale": {
      "type": "string"
     },
     "sector": {
      "enum": [
       "agrifood",
       "automotive"
      ]
     },
     "width": {
      "type": "number",
      "description": "Emprise du bâtiment en m (20 à 400, 60 par défaut)"
     },
     "depth": {
      "type": "number",
      "description": "En m (20 à 400, 40 par défaut)"
     }
    },
    "required": [
     "code",
     "name",
     "country",
     "timezone",
     "locale",
     "sector"
    ]
   },
   "ProductionLine": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string"
     },
     "site_id": {
      "type": "string"
     },
     "code": {
      "type": "string"
     },
     "name": {
      "type": "string"
     },
     "product": {
      "type": "string"
     },
     "nominal_rate": {
      "type": "integer"
     }
    }
   },
   "LineInput": {
    "type": "object",
    "properties": {
     "site_id": {
      "type": "string"
     },
     "code": {
      "type": "string"
     },
     "name": {
      "type": "string"
     },
     "product": {
      "type": "string"
     },
     "nominal_rate": {
      "type": "integer"
     }
    },
    "required": [
     "site_id",
     "code",
     "name"
    ]
   },
   "LayoutResult": {
    "type": "object",
    "properties": {
     "site": {
      "$ref": "#/components/schemas/Site"
     },
     "assets": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Asset"
      }
     }
    }
   },
   "Indicator": {
    "type": "object",
    "properties": {
     "key": {
      "enum": [
       "availability",
       "oee",
       "mtbf",
       "mttr",
       "lead_time",
       "coded_share",
       "preventive_share_hours",
       "preventive_share_count",
       "pm_compliance",
       "planned_share",
       "backlog_weeks",
       "labor_hours",
       "cost_total",
       "cost_to_rav",
       "satisfaction"
      ],
      "description": "satisfaction : notes de 4 ou 5 ÷ notes reçues des demandeurs sans compte (portail QR), au jour de la note"
     },
     "code": {
      "type": [
       "string",
       "null"
      ],
      "description": "Code EN 15341:2019+A1:2022 vérifié sur le texte de la norme (PHA6, PHA6 · R1), sinon null"
     },
     "value": {
      "type": [
       "number",
       "null"
      ],
      "description": "null : non calculable (voir missing)"
     },
     "unit": {
      "enum": [
       "%",
       "h",
       "min",
       "wk",
       "EUR"
      ]
     },
     "numerator": {
      "type": "number"
     },
     "denominator": {
      "type": "number"
     },
     "missing": {
      "type": [
       "string",
       "null"
      ],
      "description": "production_counts, no_failure, no_repair, no_corrective, no_labor, no_work_order, no_plan_work_order, no_technician, no_replacement_value, no_asset, no_elapsed_time, no_rating"
     }
    },
    "required": [
     "key",
     "value",
     "unit",
     "numerator",
     "denominator"
    ]
   },
   "Report": {
    "type": "object",
    "properties": {
     "from": {
      "type": "integer",
      "format": "int64",
      "description": "Horodatage en millisecondes (epoch)"
     },
     "to": {
      "type": "integer",
      "format": "int64",
      "description": "Fin exclue ; bornes alignées sur des jours UTC entiers"
     },
     "generated_at": {
      "type": "integer",
      "format": "int64",
      "description": "Horodatage en millisecondes (epoch)"
     },
     "site_ids": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "granularity": {
      "enum": [
       "day",
       "week",
       "month"
      ]
     },
     "days": {
      "type": "integer"
     },
     "required_hours": {
      "type": "number"
     },
     "assets_in_scope": {
      "type": "integer"
     },
     "technicians": {
      "type": "integer"
     },
     "indicators": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Indicator"
      }
     },
     "closed": {
      "type": "object",
      "properties": {
       "corrective": {
        "type": "integer"
       },
       "preventive": {
        "type": "integer"
       },
       "improvement": {
        "type": "integer"
       }
      }
     },
     "failures": {
      "type": "integer",
      "description": "Défaillances demandées sur la période : correctifs, hors réparation définitive (même défaillance que la réparation temporaire)"
     },
     "failure_modes": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "key": {
         "type": "string"
        },
        "count": {
         "type": "integer"
        },
        "cumulative_pct": {
         "type": "number"
        }
       }
      },
      "description": "Pareto des défaillances clôturées (correctifs hors réparation définitive)"
     },
     "causes": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "key": {
         "type": "string"
        },
        "count": {
         "type": "integer"
        },
        "cumulative_pct": {
         "type": "number"
        }
       }
      }
     },
     "assets": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "asset_id": {
         "type": "string"
        },
        "site_id": {
         "type": "string"
        },
        "failures": {
         "type": "integer"
        },
        "repairs": {
         "type": "integer"
        },
        "downtime_minutes": {
         "type": "integer"
        },
        "mttr_minutes": {
         "type": "number"
        },
        "mtbf_hours": {
         "type": "number"
        },
        "availability": {
         "type": "number"
        },
        "labor_hours": {
         "type": "number"
        },
        "cost": {
         "type": "object"
        }
       }
      }
     },
     "costs": {
      "type": "object",
      "properties": {
       "labor": {
        "type": "number"
       },
       "parts": {
        "type": "number"
       },
       "external": {
        "type": "number"
       },
       "total": {
        "type": "number"
       },
       "replacement_value": {
        "type": "number"
       }
      }
     },
     "backlog": {
      "type": "object",
      "properties": {
       "open": {
        "type": "integer"
       },
       "hours": {
        "type": "number"
       },
       "weeks": {
        "type": "number"
       },
       "capacity_hours_per_week": {
        "type": "number"
       }
      }
     },
     "micro_stops": {
      "type": "object",
      "properties": {
       "count": {
        "type": "integer"
       },
       "minutes": {
        "type": "integer"
       },
       "by_reason": {
        "type": "array",
        "items": {
         "type": "object",
         "properties": {
          "reason": {
           "type": "string"
          },
          "count": {
           "type": "integer"
          },
          "minutes": {
           "type": "integer"
          }
         }
        }
       }
      }
     },
     "series": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "start": {
         "type": "integer",
         "format": "int64",
         "description": "Horodatage en millisecondes (epoch)"
        },
        "closed": {
         "type": "object"
        },
        "failures": {
         "type": "integer"
        },
        "downtime_minutes": {
         "type": "integer"
        },
        "labor_hours": {
         "type": "number"
        },
        "cost": {
         "type": "number"
        }
       }
      }
     },
     "by_site": {
      "type": "array",
      "items": {
       "type": "object",
       "properties": {
        "site_id": {
         "type": "string"
        },
        "assets_in_scope": {
         "type": "integer"
        },
        "technicians": {
         "type": "integer"
        },
        "closed": {
         "type": "object"
        },
        "failures": {
         "type": "integer"
        },
        "costs": {
         "type": "object"
        },
        "indicators": {
         "type": "array",
         "items": {
          "$ref": "#/components/schemas/Indicator"
         }
        }
       }
      },
      "description": "Comparatif entre usines (vue réseau) ; vide pour une seule usine"
     },
     "assumptions": {
      "type": "object",
      "properties": {
       "required_hours_per_day": {
        "type": "number"
       },
       "weekly_hours_per_technician": {
        "type": "number"
       },
       "day_boundary": {
        "const": "utc"
       },
       "pm_on_time_days": {
        "type": "integer"
       }
      }
     }
    }
   },
   "DemoRequestInput": {
    "type": "object",
    "properties": {
     "name": {
      "type": "string",
      "description": "Nom et prénom, 1 à 100 caractères après rognage (blancs d'une ligne réduits)"
     },
     "company": {
      "type": "string",
      "description": "Société, 1 à 120 caractères"
     },
     "email": {
      "type": "string",
      "description": "E-mail professionnel (messageries grand public admises) ; gardé en minuscules"
     },
     "phone": {
      "type": [
       "string",
       "null"
      ],
      "description": "Facultatif : 6 à 20 chiffres, « + » en tête, groupes séparés par une espace ou un point"
     },
     "sites": {
      "enum": [
       "1",
       "2-3",
       "4-10",
       "10+"
      ],
      "description": "Nombre de sites : 1, 2 à 3, 4 à 10, plus de 10"
     },
     "message": {
      "type": [
       "string",
       "null"
      ],
      "description": "Facultatif : 2 000 caractères au plus (un saut de ligne compte pour un)"
     },
     "consent": {
      "type": "boolean",
      "description": "Consentement au traitement de la demande : doit valoir true"
     },
     "locale": {
      "enum": [
       "fr",
       "en",
       "de",
       "es"
      ],
      "description": "Langue de la page qui envoie la demande"
     },
     "website": {
      "type": "string",
      "description": "Champ piège caché aux humains, à laisser vide : rempli, la demande est jetée (réponse 201 ordinaire)"
     }
    },
    "required": [
     "name",
     "company",
     "email",
     "sites",
     "consent",
     "locale"
    ]
   },
   "DemoRequestErrors": {
    "type": "object",
    "properties": {
     "error": {
      "type": "string"
     },
     "code": {
      "const": "invalid"
     },
     "errors": {
      "type": "object",
      "description": "Champ → code stable : required, invalid, too_long, consent_required",
      "additionalProperties": {
       "enum": [
        "required",
        "invalid",
        "too_long",
        "consent_required"
       ]
      }
     }
    },
    "required": [
     "error",
     "code",
     "errors"
    ]
   },
   "WebhookPayload": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string"
     },
     "event": {
      "type": "string"
     },
     "tenant": {
      "type": "string"
     },
     "at": {
      "type": "integer",
      "format": "int64",
      "description": "Horodatage en millisecondes (epoch)"
     },
     "actor": {
      "type": "string"
     },
     "target": {
      "type": "string"
     },
     "detail": {
      "type": "object"
     },
     "data": {
      "description": "État courant de l'enregistrement (OT, achat, contrôle, article, contrat, équipement)",
      "type": "object"
     }
    }
   }
  }
 },
 "paths": {
  "/api/health": {
   "get": {
    "tags": [
     "Système"
    ],
    "summary": "Santé de l'API",
    "parameters": [],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "security": []
   }
  },
  "/api/auth/login": {
   "post": {
    "tags": [
     "Authentification"
    ],
    "summary": "Connexion e-mail / mot de passe → jeton de session (12 h). Double authentification active : 401 « mfa_required » sans `code`, puis code TOTP à 6 chiffres. 5 échecs (mot de passe ou code) en 15 minutes bloquent l'adresse e-mail, 50 échecs bloquent l'adresse IP : 429 `rate_limited`, en-tête `Retry-After` (secondes) et `retry_after` dans le corps",
    "parameters": [],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "token": {
           "type": "string"
          },
          "user": {
           "type": "object"
          }
         }
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Trop d'essais : réessayer après `Retry-After` secondes",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "email": {
          "type": "string"
         },
         "password": {
          "type": "string"
         },
         "code": {
          "type": "string"
         }
        },
        "required": [
         "email",
         "password"
        ]
       }
      }
     }
    },
    "security": []
   }
  },
  "/api/auth/options": {
   "get": {
    "tags": [
     "Authentification"
    ],
    "summary": "Ce que la page de connexion propose : comptes de démonstration (`demo`, si le tenant de démonstration existe) et connexion unique d'entreprise (`sso.label`, ou null)",
    "parameters": [],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "demo": {
           "type": "boolean"
          },
          "sso": {
           "oneOf": [
            {
             "type": "object",
             "properties": {
              "label": {
               "type": "string"
              }
             }
            },
            {
             "type": "null"
            }
           ]
          }
         }
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "security": []
   }
  },
  "/api/auth/logout": {
   "post": {
    "tags": [
     "Authentification"
    ],
    "summary": "Se déconnecter : le jeton de la session est refusé jusqu'à son échéance. `everywhere` : toutes les sessions du compte, sur tous les appareils",
    "parameters": [],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": false,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "everywhere": {
          "type": "boolean"
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/security": {
   "get": {
    "tags": [
     "Comptes"
    ],
    "summary": "Administrateur : double authentification imposée par le client (`off`, `privileged` = administrateurs et responsables, `all`) et nombre de comptes actifs qui devront l'activer selon la politique",
    "parameters": [],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "mfa_policy": {
           "enum": [
            "off",
            "privileged",
            "all"
           ]
          },
          "without_mfa": {
           "type": "object",
           "properties": {
            "privileged": {
             "type": "integer"
            },
            "all": {
             "type": "integer"
            }
           }
          }
         }
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   },
   "put": {
    "tags": [
     "Comptes"
    ],
    "summary": "Administrateur : imposer la double authentification. Un compte visé qui ne l'a pas encore activée ne peut plus que l'activer (403 `mfa_setup_required` ailleurs) ; une session de connexion unique d'entreprise en est dispensée ; la désactiver devient impossible (422 `mfa_required`)",
    "parameters": [],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "mfa_policy": {
           "type": "string"
          },
          "without_mfa": {
           "type": "object",
           "properties": {
            "privileged": {
             "type": "integer"
            },
            "all": {
             "type": "integer"
            }
           }
          }
         }
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "mfa_policy": {
          "enum": [
           "off",
           "privileged",
           "all"
          ]
         }
        },
        "required": [
         "mfa_policy"
        ]
       }
      }
     }
    }
   }
  },
  "/api/auth/forgot": {
   "post": {
    "tags": [
     "Authentification"
    ],
    "summary": "Mot de passe oublié : 202, que l'adresse existe ou non. Un compte actif reçoit par e-mail un lien d'une heure (`/acces#gmaolink_…`), un compte invité un nouveau lien d'invitation. 5 demandes par adresse et par quart d'heure (429)",
    "parameters": [],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "202": {
      "description": "OK"
     },
     "429": {
      "description": "Trop d'essais : réessayer après `Retry-After` secondes",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "email": {
          "type": "string"
         },
         "locale": {
          "type": "string"
         }
        },
        "required": [
         "email"
        ]
       }
      }
     }
    },
    "security": []
   }
  },
  "/api/auth/link": {
   "post": {
    "tags": [
     "Authentification"
    ],
    "summary": "À qui est un lien d'accès (réinitialisation, invitation ou activation d'un essai `gmaotrial_…`) : adresse, nom, motif, entreprise de l'essai, échéance. 410 si le lien est échu, déjà utilisé ou falsifié",
    "parameters": [],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "email": {
           "type": "string"
          },
          "name": {
           "type": "string"
          },
          "purpose": {
           "enum": [
            "reset",
            "invite",
            "trial"
           ]
          },
          "company": {
           "type": "string"
          },
          "expires_at": {
           "type": "integer"
          }
         }
        }
       }
      }
     },
     "429": {
      "description": "Trop d'essais : réessayer après `Retry-After` secondes",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "410": {
      "description": "Lien échu, déjà utilisé ou falsifié"
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "token": {
          "type": "string"
         }
        },
        "required": [
         "token"
        ]
       }
      }
     }
    },
    "security": []
   }
  },
  "/api/auth/reset": {
   "post": {
    "tags": [
     "Authentification"
    ],
    "summary": "Choisir son mot de passe par un lien d'accès (10 caractères au moins). Le lien ne sert qu'une fois ; les sessions ouvertes du compte tombent. Lien d'activation d'un essai (`gmaotrial_…`) : l'espace du client est créé à cet instant, essai de 30 jours. 410 si le lien n'est plus valable",
    "parameters": [],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "email": {
           "type": "string"
          }
         }
        }
       }
      }
     },
     "429": {
      "description": "Trop d'essais : réessayer après `Retry-After` secondes",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "410": {
      "description": "Lien échu, déjà utilisé ou falsifié"
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "token": {
          "type": "string"
         },
         "password": {
          "type": "string"
         }
        },
        "required": [
         "token",
         "password"
        ]
       }
      }
     }
    },
    "security": []
   }
  },
  "/api/users/{id}/access-link": {
   "post": {
    "tags": [
     "Comptes"
    ],
    "summary": "Administrateur : (ré)envoie l'invitation d'un compte invité (7 jours, la précédente est périmée) ou un lien de réinitialisation de 24 h pour un compte actif. Le lien est aussi rendu, à transmettre si l'e-mail n'est pas configuré",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "url": {
           "type": "string"
          },
          "purpose": {
           "enum": [
            "reset",
            "invite"
           ]
          },
          "expires_at": {
           "type": "integer"
          },
          "emailed": {
           "type": "boolean"
          }
         }
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "locale": {
          "type": "string"
         },
         "send": {
          "type": "boolean"
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/auth/renew": {
   "post": {
    "tags": [
     "Authentification"
    ],
    "summary": "Prolonger une session encore valide (écran d'atelier, poste partagé) : nouveau jeton de 12 h, rôle et nom relus dans le compte ; au plus 7 jours après la connexion (401 ensuite : se reconnecter) ; 400 pour une clé d'API, qui n'expire pas",
    "parameters": [],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "token": {
           "type": "string"
          },
          "user": {
           "type": "object"
          }
         }
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/auth/oidc": {
   "get": {
    "tags": [
     "Authentification"
    ],
    "summary": "Connexion unique d'entreprise : activée ? libellé du bouton",
    "parameters": [],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "enabled": {
           "type": "boolean"
          },
          "label": {
           "type": "string"
          }
         }
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "security": []
   }
  },
  "/api/auth/oidc/start": {
   "get": {
    "tags": [
     "Authentification"
    ],
    "summary": "Redirige vers le fournisseur d'identité (OpenID Connect, code + PKCE)",
    "parameters": [],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "security": []
   }
  },
  "/api/auth/oidc/callback": {
   "get": {
    "tags": [
     "Authentification"
    ],
    "summary": "Retour du fournisseur : vérifie le jeton d'identité et redirige vers le front avec la session (#sso=…)",
    "parameters": [],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "security": []
   }
  },
  "/api/me/mfa/setup": {
   "post": {
    "tags": [
     "Authentification"
    ],
    "summary": "Double authentification : nouveau secret TOTP et adresse otpauth:// à scanner",
    "parameters": [],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "secret": {
           "type": "string"
          },
          "otpauth_url": {
           "type": "string"
          }
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/me/mfa/enable": {
   "post": {
    "tags": [
     "Authentification"
    ],
    "summary": "Activer la double authentification avec un premier code",
    "parameters": [],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "code": {
          "type": "string"
         }
        },
        "required": [
         "code"
        ]
       }
      }
     }
    }
   }
  },
  "/api/me/mfa/disable": {
   "post": {
    "tags": [
     "Authentification"
    ],
    "summary": "Désactiver la double authentification (code requis)",
    "parameters": [],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "code": {
          "type": "string"
         }
        },
        "required": [
         "code"
        ]
       }
      }
     }
    }
   }
  },
  "/api/users/{id}/personal-data": {
   "get": {
    "tags": [
     "Comptes"
    ],
    "summary": "RGPD art. 15 : export des données d'un utilisateur (compte, OT, temps, messages, absences, journal d'audit)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/users/{id}/erase": {
   "post": {
    "tags": [
     "Comptes"
    ],
    "summary": "RGPD art. 17 : pseudonymise l'utilisateur dans tout l'historique, anonymise et désactive le compte (journal d'audit conservé)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/privacy/erase": {
   "post": {
    "tags": [
     "Comptes"
    ],
    "summary": "RGPD : effacement d'un demandeur sans compte (nom et e-mail)",
    "parameters": [],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "name": {
          "type": "string"
         },
         "email": {
          "type": "string"
         }
        },
        "required": [
         "name"
        ]
       }
      }
     }
    }
   }
  },
  "/api/me": {
   "get": {
    "tags": [
     "Authentification"
    ],
    "summary": "Utilisateur courant (dont `reminders` : reçoit les rappels par e-mail)",
    "parameters": [],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/me/reminders": {
   "put": {
    "tags": [
     "Authentification"
    ],
    "summary": "Recevoir ou non le rappel du matin et le récapitulatif du lundi (pour soi seulement)",
    "parameters": [],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "reminders": {
           "type": "boolean"
          }
         }
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "on": {
          "type": "boolean"
         }
        },
        "required": [
         "on"
        ]
       }
      }
     }
    }
   }
  },
  "/api/snapshot": {
   "get": {
    "tags": [
     "Données"
    ],
    "summary": "État complet du tenant (filtré par usines autorisées)",
    "parameters": [],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Snapshot"
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/assets/{id}": {
   "get": {
    "tags": [
     "Équipements"
    ],
    "summary": "Fiche équipement",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Asset"
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   },
   "patch": {
    "tags": [
     "Équipements"
    ],
    "summary": "Corriger la fiche (droit d'implantation) : nom, fabricant, modèle, année (0 : inconnue), criticité, ligne (`null` : hors ligne), numéro de série, fournisseur, valeur, dates AAAA-MM-JJ. Le code et la classe ne changent pas. Chaque champ changé est tracé dans le fil de l'équipement",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "name": {
          "type": "string"
         },
         "manufacturer": {
          "type": "string"
         },
         "model": {
          "type": "string"
         },
         "year": {
          "type": "integer"
         },
         "criticality": {
          "enum": [
           "A",
           "B",
           "C"
          ]
         },
         "line_id": {
          "type": "string"
         },
         "serial_no": {
          "type": "string"
         },
         "supplier": {
          "type": "string"
         },
         "cost_eur": {
          "type": "integer"
         },
         "commissioned_on": {
          "type": "string"
         },
         "warranty_until": {
          "type": "string"
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/assets/{id}/retire": {
   "post": {
    "tags": [
     "Équipements"
    ],
    "summary": "Retirer du parc (rebut, vente) : plus de demande, d'OT ni de portail QR, plans préventifs désactivés, hors indicateurs ; historique gardé. 422 open_work tant qu'un OT est ouvert sur l'équipement",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "note": {
          "type": "string"
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/assets/{id}/reinstate": {
   "post": {
    "tags": [
     "Équipements"
    ],
    "summary": "Remettre au parc un équipement retiré par erreur (ses plans restent à réactiver)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/assets/{id}/restart": {
   "post": {
    "tags": [
     "Équipements"
    ],
    "summary": "Remise en service d'une machine arrêtée (rôle d'exécution) : l'arrêt finit pour les OT qui la tenaient à l'heure du geste (`at`), qui restent ouverts ; une panne « machine arrêtée » déclarée après ce geste tient toujours la machine arrêtée. 422 si la machine n'est pas arrêtée (not_stopped), sous consignation (lockout_active), en cours d'intervention (in_progress) ou en attente de levée d'hygiène (hygiene_pending), ou si l'heure du geste précède l'arrêt (order)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "at": {
          "type": "integer",
          "format": "int64",
          "description": "Heure du geste (ms epoch), saisie hors ligne : 48 h au plus dans le passé ; un léger futur est ramené à l'heure du serveur. Absent : heure de réception"
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/assets/{id}/public-token": {
   "post": {
    "tags": [
     "Équipements"
    ],
    "summary": "Nouveau QR code (gestion des usines) : un nouveau jeton remplace l'ancien, qui n'ouvre plus le portail de demande (QR perdu, déclarations abusives). Journalisé sans le jeton",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Asset"
        }
       }
      }
     }
    }
   }
  },
  "/api/assets": {
   "post": {
    "tags": [
     "Usines"
    ],
    "summary": "Poser un équipement depuis le catalogue de l'éditeur (mêmes champs et règles qu'une ligne d'import ; x/z absents : zone d'attente)",
    "parameters": [],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Asset"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "site_code": {
          "type": "string"
         },
         "line_code": {
          "type": "string"
         },
         "code": {
          "type": "string"
         },
         "name": {
          "type": "string"
         },
         "kind": {
          "type": "string"
         },
         "manufacturer": {
          "type": "string"
         },
         "model": {
          "type": "string"
         },
         "year": {
          "type": "integer"
         },
         "criticality": {
          "enum": [
           "A",
           "B",
           "C"
          ]
         },
         "x": {
          "type": "number"
         },
         "z": {
          "type": "number"
         },
         "length": {
          "type": "number"
         },
         "serial_no": {
          "type": "string"
         },
         "supplier": {
          "type": "string"
         },
         "cost_eur": {
          "type": "integer"
         },
         "commissioned_on": {
          "type": "string"
         },
         "warranty_until": {
          "type": "string"
         }
        },
        "required": [
         "site_code",
         "code",
         "name",
         "kind",
         "criticality"
        ]
       }
      }
     }
    }
   }
  },
  "/api/sites": {
   "post": {
    "tags": [
     "Usines"
    ],
    "summary": "Créer une usine (responsable, administrateur) : place libre sur la vue réseau, équipes et canal de maintenance créés",
    "parameters": [],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Site"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/SiteInput"
       }
      }
     }
    }
   }
  },
  "/api/sites/{id}": {
   "patch": {
    "tags": [
     "Usines"
    ],
    "summary": "Modifier une usine (identifiant et place sur la vue réseau inchangés)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Site"
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/SiteInput"
       }
      }
     }
    }
   }
  },
  "/api/sites/{id}/layout": {
   "put": {
    "tags": [
     "Usines"
    ],
    "summary": "Enregistrer le plan (tout ou rien) : emprise facultative, positions et rotations (radians) ; cour de 40 m autour du bâtiment",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/LayoutResult"
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "width": {
          "type": "number"
         },
         "depth": {
          "type": "number"
         },
         "placements": {
          "type": "array",
          "items": {
           "type": "object",
           "properties": {
            "asset_id": {
             "type": "string"
            },
            "x": {
             "type": "number"
            },
            "z": {
             "type": "number"
            },
            "rotation": {
             "type": "number"
            }
           },
           "required": [
            "asset_id",
            "x",
            "z"
           ]
          }
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/sites/{id}/auto-layout": {
   "post": {
    "tags": [
     "Usines"
    ],
    "summary": "Implantation automatique : une rangée par ligne selon le flux, utilités au nord, silos dehors à l'ouest, allée principale au sud ; bâtiment redimensionné",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/LayoutResult"
        }
       }
      }
     }
    }
   }
  },
  "/api/sites/{id}/settings": {
   "patch": {
    "tags": [
     "Usines"
    ],
    "summary": "Administrateur : mode petite structure (`simple` : barre courte, clôture d'une phrase, codes de panne facultatifs) et rappels par e-mail (`reminders` : rappel du matin à 7 h et récapitulatif du lundi aux administrateurs et responsables)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "simple": {
          "type": "boolean"
         },
         "reminders": {
          "type": "boolean"
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/sites/{id}/digest-preview": {
   "get": {
    "tags": [
     "Usines"
    ],
    "summary": "Récapitulatif du lundi tel qu'il partirait maintenant, groupé par urgence (en retard, cette semaine, plus tard) avec un lien par fiche : objet, texte, HTML, `empty` (rien à signaler : il ne partirait pas), `reminders`",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "subject": {
           "type": "string"
          },
          "body": {
           "type": "string"
          },
          "html": {
           "type": "string"
          },
          "empty": {
           "type": "boolean"
          },
          "reminders": {
           "type": "boolean"
          }
         }
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/lines": {
   "post": {
    "tags": [
     "Usines"
    ],
    "summary": "Créer une ligne de production (code unique dans l'usine)",
    "parameters": [],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ProductionLine"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/LineInput"
       }
      }
     }
    }
   }
  },
  "/api/lines/{id}": {
   "patch": {
    "tags": [
     "Usines"
    ],
    "summary": "Modifier une ligne",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ProductionLine"
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/LineInput"
       }
      }
     }
    }
   }
  },
  "/api/assets/{id}/model": {
   "post": {
    "tags": [
     "Équipements"
    ],
    "summary": "Modèle 3D depuis un lien (fichier glTF/GLB stocké en pièce jointe, ou fiche produit / image → recette générée par l'IA), une description (`prompt`) ou une pièce jointe glTF de l'équipement. `same_model` : tous les équipements du même fabricant et modèle. 413 fichier trop gros, 415 contenu non reconnu, 502 lien ou IA en échec, 503 IA non configurée",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ModelResult"
        }
       }
      }
     },
     "413": {
      "description": "Fichier trop volumineux",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "415": {
      "description": "Contenu du lien non reconnu",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "502": {
      "description": "Lien inaccessible ou service IA en échec",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "503": {
      "description": "Génération IA non configurée (ANTHROPIC_API_KEY)",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "url": {
          "type": "string"
         },
         "prompt": {
          "type": "string"
         },
         "attachment_id": {
          "type": "string"
         },
         "same_model": {
          "type": "boolean"
         }
        }
       }
      }
     }
    }
   },
   "delete": {
    "tags": [
     "Équipements"
    ],
    "summary": "Retour au modèle procédural du type (`?same_model=true` : tous les équipements du même modèle)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "same_model",
      "in": "query",
      "required": false,
      "schema": {
       "type": "boolean"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ModelResult"
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/assets/{id}/anchors": {
   "put": {
    "tags": [
     "Équipements"
    ],
    "summary": "Ancrages 3D des points d'isolement et des points de mesure de l'équipement (tout ou rien ; null retire l'ancrage)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Asset"
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/AnchorsInput"
       }
      }
     }
    }
   }
  },
  "/api/calendars": {
   "get": {
    "tags": [
     "Agenda"
    ],
    "summary": "Agendas visibles : personnels du compte, d'usine et d'équipe des usines autorisées",
    "parameters": [],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "array",
         "items": {
          "$ref": "#/components/schemas/Calendar"
         }
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   },
   "post": {
    "tags": [
     "Agenda"
    ],
    "summary": "Créer un agenda : personnel (tout compte, propriétaire), d'usine ou d'équipe (droit `plan`) ; fuseau de l'usine par défaut",
    "parameters": [],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Calendar"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "kind": {
          "enum": [
           "personal",
           "site",
           "team"
          ]
         },
         "site_id": {
          "type": "string"
         },
         "team_id": {
          "type": "string"
         },
         "name": {
          "type": "string"
         },
         "tz": {
          "type": "string"
         },
         "default_reminders": {
          "type": "array",
          "items": {
           "type": "integer"
          }
         }
        },
        "required": [
         "kind",
         "name"
        ]
       }
      }
     }
    }
   }
  },
  "/api/calendars/{id}": {
   "patch": {
    "tags": [
     "Agenda"
    ],
    "summary": "Renommer, changer de fuseau, rappels par défaut, archiver (propriétaire, ou `plan` et usine)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "name": {
          "type": "string"
         },
         "tz": {
          "type": "string"
         },
         "default_reminders": {
          "type": "array",
          "items": {
           "type": "integer"
          }
         },
         "archived": {
          "type": "boolean"
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/calendar/occurrences": {
   "get": {
    "tags": [
     "Agenda"
    ],
    "summary": "Occurrences de [from, to) (ms, 400 jours au plus) : événements visibles dépliés (répétition RFC 5545, exceptions, dérogations) et calques demandés (`layers` : work_orders, inspections, contracts, absences, on_call, preventive, qualifications, deliveries, activities, holidays, ou all). Un OT apparaît sur son jour sans heure ; un contrôle échu est « overdue » et son suivant projeté ; le motif d'une absence n'est donné qu'au droit `plan`. `no_holidays` : usines dont le pays n'a pas de fériés connus. Réponse `no-store`",
    "parameters": [],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "items": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/Occurrence"
           }
          },
          "more": {
           "type": "boolean"
          },
          "no_holidays": {
           "type": "array",
           "items": {
            "type": "string"
           }
          }
         }
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/calendar/preview": {
   "post": {
    "tags": [
     "Agenda"
    ],
    "summary": "Aperçu sans enregistrement : dix premières occurrences, conflits, règle en phrase dans la langue de l'usine",
    "parameters": [],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "occurrences": {
           "type": "array",
           "items": {
            "type": "array",
            "items": {
             "type": "integer"
            }
           }
          },
          "conflicts": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/Conflict"
           }
          },
          "describe": {
           "type": "string"
          }
         }
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/EventInput"
       }
      }
     }
    }
   }
  },
  "/api/calendar/events": {
   "post": {
    "tags": [
     "Agenda"
    ],
    "summary": "Créer un événement (`schedule` pour un agenda d'usine ou d'équipe, propriétaire pour un agenda personnel). La première occurrence doit suivre la règle. Conflits (absence, chevauchement, charge du jour au-delà de 7 h, machine prise par un OT programmé) : 409 `conflicts` avec leur liste, sauf `accept_conflicts` ; ils restent dans l'audit",
    "parameters": [],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "event": {
           "$ref": "#/components/schemas/Event"
          },
          "conflicts": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/Conflict"
           }
          }
         }
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/EventInput"
       }
      }
     }
    }
   }
  },
  "/api/calendar/events/{id}": {
   "patch": {
    "tags": [
     "Agenda"
    ],
    "summary": "Modifier (`scope` : all, this ou following, `occurrence` : clé « @2026-11-18T07:30 ») : une occurrence devient une dérogation ; « et les suivantes » coupe la série (COUNT partagé ou UNTIL une seconde avant, nouvel UID, `split_from`) ; un nouvel horaire de série emmène ses exceptions. 409 comme à la création",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "conflicts": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/Conflict"
           }
          }
         }
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "title": {
          "type": "string"
         },
         "description": {
          "type": "string"
         },
         "location": {
          "type": "string"
         },
         "when": {
          "description": "Horaire : {kind: timed, start: AAAA-MM-JJTHH:MM:SS (heure murale), tz: fuseau IANA, minutes} ou {kind: all_day, start: AAAA-MM-JJ, days}",
          "type": "object"
         },
         "rule": {
          "type": "string"
         },
         "attendees": {
          "type": "array",
          "items": {
           "type": "object"
          }
         },
         "reminders": {
          "type": "array",
          "items": {
           "type": "integer"
          }
         },
         "busy": {
          "type": "boolean"
         },
         "links": {
          "type": "array",
          "items": {
           "type": "object"
          }
         },
         "cancelled": {
          "type": "boolean"
         },
         "accept_conflicts": {
          "type": "boolean"
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/calendar/events/{id}/cancel": {
   "post": {
    "tags": [
     "Agenda"
    ],
    "summary": "Annuler un événement (organisateur, ou droit d'écriture de l'agenda)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/calendar/events/{id}/rsvp": {
   "post": {
    "tags": [
     "Agenda"
    ],
    "summary": "Réponse du compte invité",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "partstat": {
          "enum": [
           "accepted",
           "declined",
           "tentative",
           "needs_action"
          ]
         }
        },
        "required": [
         "partstat"
        ]
       }
      }
     }
    }
   }
  },
  "/api/calendar/events/{id}/event.ics": {
   "get": {
    "tags": [
     "Agenda"
    ],
    "summary": "Fichier iCalendar (RFC 5545) d'un événement visible, série complète (`text/calendar`)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "text/calendar": {
        "schema": {
         "type": "string"
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/calendars/{id}/export.ics": {
   "get": {
    "tags": [
     "Agenda"
    ],
    "summary": "Fichier iCalendar d'un agenda visible : ses événements, 30 jours en arrière et un an devant (`text/calendar`)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "text/calendar": {
        "schema": {
         "type": "string"
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/me/calendar-feeds": {
   "get": {
    "tags": [
     "Agenda"
    ],
    "summary": "Flux d'agenda du compte ; `all=true` : ceux du tenant (`manage_users`)",
    "parameters": [
     {
      "name": "all",
      "in": "query",
      "required": false,
      "schema": {
       "type": "boolean"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "array",
         "items": {
          "type": "object",
          "properties": {
           "id": {
            "type": "string"
           },
           "user_id": {
            "type": "string"
           },
           "prefix": {
            "type": "string"
           },
           "scope": {
            "type": "object"
           },
           "created_at_ms": {
            "type": "integer",
            "format": "int64",
            "description": "Horodatage en millisecondes (epoch)"
           },
           "last_fetched_ms": {
            "type": "integer",
            "format": "int64",
            "description": "Horodatage en millisecondes (epoch)"
           },
           "revoked": {
            "type": "boolean"
           }
          }
         }
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   },
   "post": {
    "tags": [
     "Agenda"
    ],
    "summary": "Créer un flux d'abonnement : calques, agendas (absent : tous ceux que le compte voit), « occupé seulement ». Le jeton `gmaocal_…` n'est rendu qu'une fois ; seule son empreinte est gardée",
    "parameters": [],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "feed": {
           "type": "object",
           "properties": {
            "id": {
             "type": "string"
            },
            "user_id": {
             "type": "string"
            },
            "prefix": {
             "type": "string"
            },
            "scope": {
             "type": "object"
            },
            "created_at_ms": {
             "type": "integer",
             "format": "int64",
             "description": "Horodatage en millisecondes (epoch)"
            },
            "last_fetched_ms": {
             "type": "integer",
             "format": "int64",
             "description": "Horodatage en millisecondes (epoch)"
            },
            "revoked": {
             "type": "boolean"
            }
           }
          },
          "token": {
           "type": "string"
          },
          "path": {
           "type": "string"
          }
         }
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "layers": {
          "type": "array",
          "items": {
           "type": "string"
          }
         },
         "calendars": {
          "type": "array",
          "items": {
           "type": "string"
          }
         },
         "busy_only": {
          "type": "boolean"
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/me/calendar-feeds/{id}": {
   "delete": {
    "tags": [
     "Agenda"
    ],
    "summary": "Révoquer un flux (le sien ; tout flux du tenant avec `manage_users`) : sa lecture répond 404",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/ical/{token}/agenda.ics": {
   "get": {
    "tags": [
     "Agenda"
    ],
    "summary": "Lecture d'un flux par Outlook, Thunderbird ou un téléphone, sans session : droits **actuels** du compte recalculés à chaque appel (404 si le flux est révoqué ou le compte désactivé), `ETag` et 304, `Cache-Control: private, max-age=300`, 60 lectures par heure (429). Le jeton n'ouvre aucune autre route et n'apparaît pas dans la trace",
    "parameters": [
     {
      "name": "token",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "text/calendar": {
        "schema": {
         "type": "string"
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "304": {
      "description": "Inchangé (`If-None-Match`)"
     },
     "404": {
      "description": "Flux inconnu, révoqué, ou compte désactivé"
     }
    },
    "security": []
   }
  },
  "/api/assets/{id}/specs": {
   "put": {
    "tags": [
     "Calculs"
    ],
    "summary": "Caractéristiques de la plaque de l'équipement (droit implantation) : remplacent les précédentes ; 422 sur une clé inconnue ou une valeur hors bornes. Elles pré-remplissent les calculateurs",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "additionalProperties": {
         "type": "number"
        },
        "description": "Caractéristiques (clés fermées de calc::specs : power_kw, speed_rpm, voltage_v, current_a, cos_phi, efficiency, poles, frequency_hz, ratio, bore_mm, rod_mm, flow_m3h, head_m, rotor_mass_kg, bearing_d_mm, bearing_od_mm, bearing_width_mm, bearing_c_kn, bearing_c0_kn, bearing_f0), bornées ; absent si vide"
       }
      }
     }
    }
   }
  },
  "/api/parts/{id}/specs": {
   "put": {
    "tags": [
     "Calculs"
    ],
    "summary": "Caractéristiques techniques de l'article (droit stock), par ex. un roulement : d, D, B, C, C0, f0",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "additionalProperties": {
         "type": "number"
        },
        "description": "Caractéristiques (clés fermées de calc::specs : power_kw, speed_rpm, voltage_v, current_a, cos_phi, efficiency, poles, frequency_hz, ratio, bore_mm, rod_mm, flow_m3h, head_m, rotor_mass_kg, bearing_d_mm, bearing_od_mm, bearing_width_mm, bearing_c_kn, bearing_c0_kn, bearing_f0), bornées ; absent si vide"
       }
      }
     }
    }
   }
  },
  "/api/parts/{id}/reorder": {
   "get": {
    "tags": [
     "Stock"
    ],
    "summary": "Point de commande proposé : sorties des 365 derniers jours (moyenne et écart type journaliers), délai moyen des commandes reçues (14 j par défaut), z 1,645 (2,326 pour une pièce critique) ; SS = z·σd·√L, PC = d·L + SS arrondi à l'unité. `min_qty` n'est pas modifié",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "part_id": {
           "type": "string"
          },
          "window_days": {
           "type": "integer"
          },
          "issued": {
           "type": "number"
          },
          "daily_demand": {
           "type": "number"
          },
          "demand_sd": {
           "type": "number"
          },
          "lead_time_days": {
           "type": "number"
          },
          "lead_time_measured": {
           "type": "boolean"
          },
          "z": {
           "type": "number"
          },
          "safety_stock": {
           "type": "number"
          },
          "reorder_point": {
           "type": "number"
          },
          "current_min": {
           "type": "number"
          }
         }
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/assets/{id}/isolation-points": {
   "put": {
    "tags": [
     "Sécurité"
    ],
    "summary": "Procédure de consignation",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "array",
        "items": {
         "type": "object",
         "properties": {
          "energy": {
           "type": "string"
          },
          "label": {
           "type": "string"
          }
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/assets/{id}/bom/{part}": {
   "post": {
    "tags": [
     "Stock"
    ],
    "summary": "Ligne de nomenclature (qty = 0 la retire)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "part",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "qty": {
          "type": "number"
         }
        },
        "required": [
         "qty"
        ]
       }
      }
     }
    }
   }
  },
  "/api/work-orders": {
   "post": {
    "tags": [
     "Interventions"
    ],
    "summary": "Créer une demande d'intervention (rejouée avec la même `client_ref` : même OT renvoyé)",
    "parameters": [],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/WorkOrder"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "asset_id": {
          "type": "string"
         },
         "title": {
          "type": "string"
         },
         "priority": {
          "type": "string"
         },
         "client_ref": {
          "type": "string",
          "description": "Référence de l'appareil (UUID, 8 à 64 caractères [A-Za-z0-9_-]) : un envoi rejoué renvoie l'enregistrement déjà créé"
         },
         "at": {
          "type": "integer",
          "format": "int64",
          "description": "Heure du geste (ms epoch), saisie hors ligne : 48 h au plus dans le passé ; un léger futur est ramené à l'heure du serveur. Absent : heure de réception"
         },
         "machine_stopped": {
          "type": "boolean",
          "description": "La panne arrête la machine : elle passe « En panne » à l'heure du geste (`at`)"
         }
        },
        "required": [
         "asset_id",
         "title",
         "priority"
        ]
       }
      }
     }
    }
   }
  },
  "/api/work-orders/done": {
   "post": {
    "tags": [
     "Interventions"
    ],
    "summary": "Intervention déjà faite, saisie après coup : OT créé clôturé, daté du travail (`done_at`, un an au plus dans le passé ; défaut : maintenant), avec son temps passé (1 à 1 440 min) ; codes de défaillance facultatifs (un correctif sans mode : mode lu dans la phrase, marqué `report.inferred`), un mode saisi doit convenir à la classe ; `technician_id` : le technicien de l'usine qui a fait le travail (défaut : la personne qui saisit)",
    "parameters": [],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/WorkOrder"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "asset_id": {
          "type": "string"
         },
         "kind": {
          "enum": [
           "corrective",
           "preventive",
           "improvement"
          ]
         },
         "title": {
          "type": "string"
         },
         "remedy": {
          "type": "string"
         },
         "minutes": {
          "type": "integer"
         },
         "done_at": {
          "type": "integer",
          "format": "int64",
          "description": "Horodatage en millisecondes (epoch)"
         },
         "downtime_minutes": {
          "type": "integer"
         },
         "failure_mode": {
          "$ref": "#/components/schemas/FailureMode"
         },
         "technician_id": {
          "type": "string"
         }
        },
        "required": [
         "asset_id",
         "title",
         "minutes"
        ]
       }
      }
     }
    }
   }
  },
  "/api/work-orders/{id}": {
   "patch": {
    "tags": [
     "Interventions"
    ],
    "summary": "Modifier titre, priorité, état kanban, notes. `base_notes` : notes sur lesquelles la saisie a été faite ; si elles ont changé entre-temps, 422 (code notes_changed) au lieu d'écraser",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "title": {
          "type": "string"
         },
         "priority": {
          "type": "string"
         },
         "kanban_state": {
          "type": "string"
         },
         "notes": {
          "type": "string"
         },
         "base_notes": {
          "type": "string"
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/work-orders/{id}/checklist/{index}": {
   "put": {
    "tags": [
     "Interventions"
    ],
    "summary": "Cocher ou décocher une case (idempotent ; refusé sur un OT clôturé). `failed` : true, étape faite et non conforme ; false, l'échec est retiré ; absent, seule la case change (la décocher retire aussi l'échec)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "index",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "done": {
          "type": "boolean"
         },
         "failed": {
          "type": "boolean"
         },
         "at": {
          "type": "integer",
          "format": "int64",
          "description": "Heure du geste (ms epoch), saisie hors ligne : 48 h au plus dans le passé ; un léger futur est ramené à l'heure du serveur. Absent : heure de réception"
         }
        },
        "required": [
         "done"
        ]
       }
      }
     }
    }
   }
  },
  "/api/work-orders/{id}/checklist/{index}/escalate": {
   "post": {
    "tags": [
     "Interventions"
    ],
    "summary": "« Réparation urgente » depuis une étape de checklist (rôle d'exécution) : l'étape passe faite et non conforme, un correctif priorité haute est créé au statut « Demande » sur le même équipement, titré du libellé de l'étape, lié dans les deux sens (`escalated_from`, `escalated_to`) et tracé dans les deux fils ; sans effet sur l'état de la machine. Idempotent : étape déjà escaladée ou même `client_ref` → même OT. 422 sur un OT clôturé (closed)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "index",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/WorkOrder"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "client_ref": {
          "type": "string",
          "description": "Référence de l'appareil (UUID, 8 à 64 caractères [A-Za-z0-9_-]) : un envoi rejoué renvoie l'enregistrement déjà créé"
         },
         "at": {
          "type": "integer",
          "format": "int64",
          "description": "Heure du geste (ms epoch), saisie hors ligne : 48 h au plus dans le passé ; un léger futur est ramené à l'heure du serveur. Absent : heure de réception"
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/work-orders/{id}/operations": {
   "put": {
    "tags": [
     "Interventions"
    ],
    "summary": "Remplacer la gamme d'opérations (ordre de la liste). Une opération gardée (même `seq`) conserve son état ; sans `seq`, elle est numérotée à la dizaine suivante ; une opération qui a des temps passés ne peut pas être retirée (422). Changer le temps prévu ou le métier d'une opération, ou en retirer une, demande le droit de planifier (403 sinon) ; le fil trace « phase:minutes:métier » avant et après. L'avancement de l'OT suit le temps prévu des opérations faites",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "array",
        "items": {
         "type": "object",
         "properties": {
          "seq": {
           "type": "integer"
          },
          "title": {
           "type": "string"
          },
          "trade": {
           "enum": [
            "mechanic",
            "electrician",
            "automation"
           ]
          },
          "planned_minutes": {
           "type": "integer"
          }
         },
         "required": [
          "title",
          "trade"
         ]
        }
       }
      }
     }
    }
   }
  },
  "/api/work-orders/{id}/children": {
   "post": {
    "tags": [
     "Interventions"
    ],
    "summary": "Créer un sous-OT (arrêt technique, révision) : même usine, nature et échéance que le parent, validé d'office si le parent l'est ; équipement facultatif (celui du parent), gamme facultative (validée avant la création : un refus ne crée rien). Trois niveaux au plus. Le parent ne se clôture pas tant qu'un sous-OT est ouvert (422, code children_open)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/WorkOrder"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "title": {
          "type": "string"
         },
         "asset_id": {
          "type": "string"
         },
         "trade": {
          "enum": [
           "mechanic",
           "electrician",
           "automation"
          ]
         },
         "priority": {
          "type": "string"
         },
         "operations": {
          "type": "array",
          "items": {
           "type": "object",
           "properties": {
            "title": {
             "type": "string"
            },
            "trade": {
             "type": "string"
            },
            "planned_minutes": {
             "type": "integer"
            }
           }
          }
         }
        },
        "required": [
         "title"
        ]
       }
      }
     }
    }
   }
  },
  "/api/work-orders/{id}/rollup": {
   "get": {
    "tags": [
     "Interventions"
    ],
    "summary": "Cumul de l'OT et de tout son arbre : sous-OT ouverts, avancement, temps prévu et passé, coût réel (main-d'œuvre, pièces sorties, sous-traitance) et engagé (pièces réservées)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Rollup"
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/work-orders/windows": {
   "get": {
    "tags": [
     "Interventions"
    ],
    "summary": "Dates au plus tôt / au plus tard des OT ouverts (query `site_id` facultatif) : depuis l'échéance et le temps prévu restant, appro au plus tard, pièces attendues et leur commande, attente appro et retard prévu",
    "parameters": [
     {
      "name": "site_id",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "array",
         "items": {
          "$ref": "#/components/schemas/Window"
         }
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/work-orders/{id}/window": {
   "get": {
    "tags": [
     "Interventions"
    ],
    "summary": "Dates au plus tôt / au plus tard d'un OT (null s'il est clôturé)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Window"
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/work-orders/{id}/due": {
   "put": {
    "tags": [
     "Interventions"
    ],
    "summary": "Échéance de l'OT (null : aucune) ; les sous-OT qui suivaient l'ancienne échéance suivent la nouvelle. Une commande passée avec une livraison annoncée après l'appro au plus tard d'un OT écrit une alerte (supply_late) dans son fil",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "due_at": {
          "oneOf": [
           {
            "type": "integer",
            "format": "int64",
            "description": "Horodatage en millisecondes (epoch)"
           },
           {
            "type": "null"
           }
          ]
         }
        },
        "required": [
         "due_at"
        ]
       }
      }
     }
    }
   }
  },
  "/api/work-orders/{id}/analysis": {
   "get": {
    "tags": [
     "Interventions"
    ],
    "summary": "Analyse prévu / réel de l'OT : main-d'œuvre par opération (minutes et coût), temps hors gamme, pièces prévues, réservées et consommées, sous-traitance (devis contre montant final), écarts",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Analysis"
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/work-orders/analysis": {
   "get": {
    "tags": [
     "Interventions"
    ],
    "summary": "Analyse prévu / réel des OT actifs sur la période (créés avant sa fin, ouverts ou clôturés depuis son début) : totaux, main-d'œuvre par métier, une ligne par OT. Mêmes paramètres et droits que GET /api/reports ; seuls comptent les OT gardés en mémoire (ouverts et derniers clôturés de chaque usine)",
    "parameters": [
     {
      "name": "site_id",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      },
      "description": "Usine ; absent : toutes les usines autorisées (comparatif)"
     },
     {
      "name": "from",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer",
       "format": "int64",
       "description": "Horodatage en millisecondes (epoch)"
      },
      "description": "Début (ms) ; arrondi au jour UTC. Défaut : 30 jours avant `to`"
     },
     {
      "name": "to",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer",
       "format": "int64",
       "description": "Horodatage en millisecondes (epoch)"
      },
      "description": "Fin exclue (ms). Défaut : demain 00:00 UTC. 3 660 jours au plus"
     },
     {
      "name": "line_id",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "asset_kind",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/AnalysisSummary"
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "400": {
      "description": "Paramètre illisible",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "Usine ou ligne inconnue",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/export/work-order-analysis.csv": {
   "get": {
    "tags": [
     "Données"
    ],
    "summary": "Analyse prévu / réel des OT d'une période en CSV (une ligne par OT ; mêmes paramètres que GET /api/work-orders/analysis)",
    "parameters": [
     {
      "name": "site_id",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      },
      "description": "Usine ; absent : toutes les usines autorisées (comparatif)"
     },
     {
      "name": "from",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer",
       "format": "int64",
       "description": "Horodatage en millisecondes (epoch)"
      },
      "description": "Début (ms) ; arrondi au jour UTC. Défaut : 30 jours avant `to`"
     },
     {
      "name": "to",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer",
       "format": "int64",
       "description": "Horodatage en millisecondes (epoch)"
      },
      "description": "Fin exclue (ms). Défaut : demain 00:00 UTC. 3 660 jours au plus"
     },
     {
      "name": "line_id",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "asset_kind",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "400": {
      "description": "Paramètre illisible",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "Usine ou ligne inconnue",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/work-orders/{id}/operations/{seq}/status": {
   "post": {
    "tags": [
     "Interventions"
    ],
    "summary": "État d'une opération : todo, in_progress, done (faite : auteur et heure gardés). `at` : heure du geste (saisie hors ligne)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "seq",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "status": {
          "enum": [
           "todo",
           "in_progress",
           "done"
          ]
         },
         "at": {
          "type": "integer",
          "format": "int64",
          "description": "Heure du geste (ms epoch), saisie hors ligne : 48 h au plus dans le passé ; un léger futur est ramené à l'heure du serveur. Absent : heure de réception"
         }
        },
        "required": [
         "status"
        ]
       }
      }
     }
    }
   }
  },
  "/api/work-orders/{id}/timer/start": {
   "post": {
    "tags": [
     "Interventions"
    ],
    "summary": "Démarrer le minuteur, éventuellement sur une opération de la gamme (`operation` : son numéro, qui passe en cours). Corps facultatif `at` (saisie hors ligne) : pas avant la vérification de la consignation, le dernier changement d'étape ni la fin du temps précédent de la personne ; rejouer le même geste est sans effet",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": false,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "at": {
          "type": "integer",
          "format": "int64",
          "description": "Heure du geste (ms epoch), saisie hors ligne : 48 h au plus dans le passé ; un léger futur est ramené à l'heure du serveur. Absent : heure de réception"
         },
         "operation": {
          "type": "integer"
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/work-orders/{id}/timer/stop": {
   "post": {
    "tags": [
     "Interventions"
    ],
    "summary": "Arrêter le minuteur (corps facultatif `at`, pas avant le démarrage ; rejeu sans effet)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": false,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "at": {
          "type": "integer",
          "format": "int64",
          "description": "Heure du geste (ms epoch), saisie hors ligne : 48 h au plus dans le passé ; un léger futur est ramené à l'heure du serveur. Absent : heure de réception"
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/work-orders/{id}/transition": {
   "post": {
    "tags": [
     "Interventions"
    ],
    "summary": "Changer d'étape. « in_progress » exige, comme le démarrage du minuteur, une consignation vérifiée de chaque point d'isolement (422 lockout) et des permis valables (422 permit). « done » est toujours refusé (422 close_with_report, ou hygiene_checks en zone d'hygiène) : un OT se clôture par POST /close, qui contrôle compte rendu, codes, déconsignation, signature et surveillance incendie",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "status": {
          "type": "string"
         },
         "assignee_id": {
          "type": "string"
         }
        },
        "required": [
         "status"
        ]
       }
      }
     }
    }
   }
  },
  "/api/work-orders/{id}/cancel": {
   "post": {
    "tags": [
     "Interventions"
    ],
    "summary": "Annuler l'OT, ou refuser la demande s'il n'est pas validé (droit plan) : doublon (`duplicate_of`, un OT de la même usine), fausse alerte, plus nécessaire, ou autre motif écrit (422 note_required). Le technicien, les réservations et l'arrêt déclaré sont libérés ; l'OT n'entre pas dans les rapports ; le demandeur est prévenu. Refusé sous consignation (lockout_active), avec un permis délivré (permit_open) ou un sous-OT ouvert (children_open)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "reason": {
          "enum": [
           "duplicate",
           "false_alarm",
           "not_needed",
           "other"
          ]
         },
         "note": {
          "type": "string"
         },
         "duplicate_of": {
          "type": "string"
         }
        },
        "required": [
         "reason"
        ]
       }
      }
     }
    }
   }
  },
  "/api/work-orders/{id}/suggested-codes": {
   "get": {
    "tags": [
     "Interventions"
    ],
    "summary": "Codes proposés à la clôture de l'OT : modes admis pour la classe, codes fréquents (appris des seules usines autorisées), détection déduite, codes hérités d'une réparation temporaire",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Codes"
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/work-orders/{id}/close": {
   "post": {
    "tags": [
     "Interventions"
    ],
    "summary": "Clôturer avec compte rendu. Correctif : mode de défaillance admis pour la classe de l'équipement et cause obligatoires (422 sinon) ; mécanisme et activité complétés par le serveur ; détection enregistrée si saisie ou déductible de l'origine. Réparation définitive : codes repris de l'OT d'origine. Préventif : codes facultatifs. Sécurité : sur un équipement à points d'isolement, chaque point a été condamné puis vérifié (422 lockout_missing) et levé (422 lockout_active) ; chaque permis exigé par les dangers a été délivré à l'heure du geste (422 permit). Parent : tous ses sous-OT clôturés (422 children_open). `at` : heure du geste (saisie hors ligne) pour le compte rendu, l'historique et l'arrêt des minuteurs ; rejeu par le même auteur sans effet, sinon 422 closed. `amended` : clôture refusée puis complétée sur l'appareil, la signature et les contrôles d'hygiène prennent l'heure de réception",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "at": {
          "type": "integer",
          "format": "int64",
          "description": "Heure du geste (ms epoch), saisie hors ligne : 48 h au plus dans le passé ; un léger futur est ramené à l'heure du serveur. Absent : heure de réception"
         },
         "amended": {
          "type": "boolean",
          "description": "Clôture complétée après un refus : signature et contrôles d'hygiène horodatés à la réception"
         },
         "problem": {
          "type": "string"
         },
         "cause": {
          "type": "string"
         },
         "remedy": {
          "type": "string"
         },
         "failure_mode": {
          "$ref": "#/components/schemas/FailureMode"
         },
         "downtime_minutes": {
          "type": [
           "integer",
           "null"
          ],
          "description": "Arrêt de production déclaré, en minutes. Absent ou null : la part de l'arrêt mesuré imputée à l'OT (panne déclarée « machine arrêtée ») est reprise ; 0 : la machine n'a pas été arrêtée"
         },
         "hygiene": {
          "type": "object"
         },
         "temporary_days": {
          "type": "integer"
         },
         "mechanism": {
          "type": "string",
          "description": "Tableau B.2 ; par défaut, le mécanisme type du mode"
         },
         "cause_code": {
          "type": "string",
          "description": "Tableau B.3 ; obligatoire sur un correctif"
         },
         "activity": {
          "type": "string",
          "description": "Tableau B.5 ; par défaut, l'activité type du mode"
         },
         "detection": {
          "$ref": "#/components/schemas/DetectionMethod"
         },
         "signature": {
          "type": "object",
          "properties": {
           "signer": {
            "type": "string"
           },
           "image": {
            "type": "string",
            "description": "data:image/png;base64,… (60 000 caractères au plus) ; obligatoire si l'OT est sous-traité"
           }
          },
          "required": [
           "signer",
           "image"
          ]
         }
        },
        "required": [
         "remedy"
        ]
       }
      }
     }
    }
   }
  },
  "/api/work-orders/{id}/parts/{part}": {
   "post": {
    "tags": [
     "Stock"
    ],
    "summary": "Ajouter un besoin à l'OT : réservé (défaut ; 422 code shortage si le disponible ne suffit pas, avec la liste des manques « not enough stock: CODE missing 4 kg; … ») ou seulement prévu (`reserve: false`, pièce à approvisionner). Un kit est éclaté en ses composants. `unit` : unité de gestion, conditionnement (paquet) ou multiple usuel (g pour un article en kg)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "part",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "qty": {
          "type": "number"
         },
         "unit": {
          "type": "string"
         },
         "reserve": {
          "type": "boolean"
         }
        },
        "required": [
         "qty"
        ]
       }
      }
     }
    }
   },
   "delete": {
    "tags": [
     "Stock"
    ],
    "summary": "Libérer une réservation ou un besoin prévu (kit : tous ses composants) ; ce qui est déjà sorti reste imputé, la ligne est soldée",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "part",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/work-orders/{id}/parts/{part}/issue": {
   "post": {
    "tags": [
     "Stock"
    ],
    "summary": "Sortir du stock sur l'OT : une partie (`qty`) ou tout le reste de la ligne ; un kit sort composant par composant, par différence de cumuls (n sorties d'un paquet = la sortie globale, au gramme près) ; au-delà du prévu, l'écart est gardé. `lot` : lot ou numéro de série tracé sur le mouvement ; `kit_id` : ligne d'un composant de kit. Saisie hors ligne : `at` (heure du geste) et `client_ref` (référence de l'appareil : la même sortie rejouée ne sort rien de plus)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "part",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": false,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "qty": {
          "type": "number"
         },
         "unit": {
          "type": "string"
         },
         "lot": {
          "type": "string"
         },
         "kit_id": {
          "type": "string"
         },
         "at": {
          "type": "integer",
          "format": "int64",
          "description": "Heure du geste (ms epoch), saisie hors ligne : 48 h au plus dans le passé ; un léger futur est ramené à l'heure du serveur. Absent : heure de réception"
         },
         "client_ref": {
          "type": "string",
          "description": "Référence de l'appareil (UUID, 8 à 64 caractères [A-Za-z0-9_-]) : un envoi rejoué renvoie l'enregistrement déjà créé"
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/work-orders/{id}/reserve": {
   "post": {
    "tags": [
     "Stock"
    ],
    "summary": "« Réserver » : tous les besoins prévus de l'OT, tout ou rien (422 avec la liste des manques)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/work-orders/{id}/issue": {
   "post": {
    "tags": [
     "Stock"
    ],
    "summary": "« Sortir les réservations » : tout le reste réservé de l'OT sort du stock, un mouvement par ligne",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/parts/{id}/explode": {
   "get": {
    "tags": [
     "Stock"
    ],
    "summary": "Besoins pour une quantité d'article (`qty`, `unit` en paramètres) : un kit est éclaté en ses composants, quantités exactes, disponible et manque de chacun",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "qty",
      "in": "query",
      "required": true,
      "schema": {
       "type": "number"
      }
     },
     {
      "name": "unit",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Explosion"
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/work-orders/{id}/schedule": {
   "post": {
    "tags": [
     "Interventions"
    ],
    "summary": "Planifier l'OT (technicien et jour, sans le lancer)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "technician_id": {
          "type": "string"
         },
         "at": {
          "type": "integer",
          "format": "int64",
          "description": "Horodatage en millisecondes (epoch)"
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/technicians/{id}/qualifications": {
   "put": {
    "tags": [
     "Sécurité"
    ],
    "summary": "Remplacer les habilitations d'un technicien (BR, BC, ATEX, CATEC, HAUTEUR) — contrôlées à l'affectation, au planning et à la consignation",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "array",
        "items": {
         "type": "object",
         "properties": {
          "code": {
           "enum": [
            "BR",
            "BC",
            "ATEX",
            "CATEC",
            "HAUTEUR"
           ]
          },
          "obtained_at": {
           "type": "integer",
           "format": "int64",
           "description": "Horodatage en millisecondes (epoch)"
          },
          "expires_at": {
           "type": "integer",
           "format": "int64",
           "description": "Horodatage en millisecondes (epoch)"
          },
          "issuer": {
           "type": "string"
          }
         },
         "required": [
          "code",
          "obtained_at",
          "expires_at"
         ]
        }
       }
      }
     }
    }
   }
  },
  "/api/technicians": {
   "post": {
    "tags": [
     "Équipe"
    ],
    "summary": "Créer la fiche d'un technicien (droit plan) : nom unique dans l'entreprise (422 name_taken), usine, métier, coût horaire chargé (défaut : celui du métier). Il rejoint l'équipe de son métier",
    "parameters": [],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "site_id": {
          "type": "string"
         },
         "name": {
          "type": "string"
         },
         "trade": {
          "enum": [
           "mechanic",
           "electrician",
           "automation"
          ]
         },
         "hourly_rate": {
          "type": "number"
         }
        },
        "required": [
         "site_id",
         "name",
         "trade"
        ]
       }
      }
     }
    }
   }
  },
  "/api/technicians/{id}": {
   "patch": {
    "tags": [
     "Équipe"
    ],
    "summary": "Corriger un technicien, le changer d'usine, le faire partir (`active: false`) ou revenir. Parti : plus affecté (422 tech_left), planifié ni d'astreinte, ses absences à venir tombent. Changement d'usine et départ refusés tant qu'il a des OT ouverts (422 tech_has_work)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "name": {
          "type": "string"
         },
         "trade": {
          "enum": [
           "mechanic",
           "electrician",
           "automation"
          ]
         },
         "hourly_rate": {
          "type": "number"
         },
         "site_id": {
          "type": "string"
         },
         "active": {
          "type": "boolean"
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/work-orders/{id}/permits": {
   "post": {
    "tags": [
     "Sécurité"
    ],
    "summary": "Demander un permis de travail (hot_work, confined_space, height, atex) — exigé avant de démarrer selon les dangers de l'équipement",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "kind": {
          "enum": [
           "hot_work",
           "confined_space",
           "height",
           "atex"
          ]
         },
         "at": {
          "type": "integer",
          "format": "int64",
          "description": "Heure du geste (ms epoch), saisie hors ligne : 48 h au plus dans le passé ; un léger futur est ramené à l'heure du serveur. Absent : heure de réception"
         }
        },
        "required": [
         "kind"
        ]
       }
      }
     }
    }
   }
  },
  "/api/work-orders/{id}/permits/{permit}/issue": {
   "post": {
    "tags": [
     "Sécurité"
    ],
    "summary": "Délivrer un permis (admin, responsable, qualité ; pas le demandeur) : toutes les précautions confirmées, 1 à 12 h",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "permit",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "confirmed": {
          "type": "array",
          "items": {
           "type": "string"
          }
         },
         "hours": {
          "type": "integer"
         }
        },
        "required": [
         "confirmed",
         "hours"
        ]
       }
      }
     }
    }
   }
  },
  "/api/work-orders/{id}/permits/{permit}/close": {
   "post": {
    "tags": [
     "Sécurité"
    ],
    "summary": "Clôturer un permis (les permis ouverts sont clôturés avec l'OT) ; `at` facultatif : la surveillance incendie part de l'heure du geste",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "permit",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": false,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "at": {
          "type": "integer",
          "format": "int64",
          "description": "Heure du geste (ms epoch), saisie hors ligne : 48 h au plus dans le passé ; un léger futur est ramené à l'heure du serveur. Absent : heure de réception"
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/work-orders/{id}/permits/{permit}/fire-watch": {
   "post": {
    "tags": [
     "Sécurité"
    ],
    "summary": "Confirmer la fin de la surveillance incendie (1 h après la clôture d'un permis de feu) ; l'OT ne se clôture pas avant ; `at` facultatif, sans tolérance sur l'heure",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "permit",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": false,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "at": {
          "type": "integer",
          "format": "int64",
          "description": "Heure du geste (ms epoch), saisie hors ligne : 48 h au plus dans le passé ; un léger futur est ramené à l'heure du serveur. Absent : heure de réception"
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/ingest/readings": {
   "post": {
    "tags": [
     "IoT"
    ],
    "summary": "Relevés capteurs poussés par une passerelle (clé d'API, droit exécuter) : chaque tag est rattaché à un point de mesure ; filtrage par zone morte et intervalle minimal, seuils toujours évalués, OT conditionnel si dépassement. Réponse : relevés enregistrés, filtrés, tags inconnus, OT ouverts",
    "parameters": [],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "samples": {
          "type": "array",
          "items": {
           "type": "object",
           "properties": {
            "tag": {
             "type": "string"
            },
            "value": {
             "type": "number"
            },
            "at": {
             "type": "integer",
             "format": "int64",
             "description": "Horodatage en millisecondes (epoch)"
            }
           },
           "required": [
            "tag",
            "value"
           ]
          }
         }
        },
        "required": [
         "samples"
        ]
       }
      }
     }
    }
   }
  },
  "/api/points": {
   "post": {
    "tags": [
     "Préventif"
    ],
    "summary": "Créer un point de mesure (maintenance conditionnelle) : équipement, nom, unité, seuil bas et/ou haut, tag capteur",
    "parameters": [],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "asset_id": {
          "type": "string"
         },
         "name": {
          "type": "string"
         },
         "unit": {
          "type": "string"
         },
         "low": {
          "type": "number"
         },
         "high": {
          "type": "number"
         },
         "tag": {
          "type": "string"
         },
         "vibration": {
          "description": "Classe vibratoire ISO 20816-3 de la machine (point en « mm/s » seulement, sinon 422) ; à la modification, absent : inchangée, null : retirée ; un changement d'unité la retire",
          "oneOf": [
           {
            "type": "object",
            "properties": {
             "power_kw": {
              "type": "number"
             },
             "flexible": {
              "type": "boolean"
             }
            },
            "required": [
             "power_kw"
            ]
           },
           {
            "type": "null"
           }
          ]
         }
        },
        "required": [
         "asset_id",
         "name"
        ]
       }
      }
     }
    }
   }
  },
  "/api/points/{id}": {
   "patch": {
    "tags": [
     "Préventif"
    ],
    "summary": "Modifier un point de mesure (mêmes champs qu'à la création, `vibration` comprise)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "asset_id": {
          "type": "string"
         },
         "name": {
          "type": "string"
         },
         "unit": {
          "type": "string"
         },
         "low": {
          "type": "number"
         },
         "high": {
          "type": "number"
         },
         "tag": {
          "type": "string"
         },
         "active": {
          "type": "boolean"
         },
         "vibration": {
          "description": "Classe vibratoire ISO 20816-3 de la machine (point en « mm/s » seulement, sinon 422) ; à la modification, absent : inchangée, null : retirée ; un changement d'unité la retire",
          "oneOf": [
           {
            "type": "object",
            "properties": {
             "power_kw": {
              "type": "number"
             },
             "flexible": {
              "type": "boolean"
             }
            },
            "required": [
             "power_kw"
            ]
           },
           {
            "type": "null"
           }
          ]
         }
        },
        "required": [
         "asset_id",
         "name"
        ]
       }
      }
     }
    }
   }
  },
  "/api/points/{id}/readings": {
   "post": {
    "tags": [
     "Préventif"
    ],
    "summary": "Saisir un relevé ; hors seuil, un OT préventif conditionnel est ouvert (un seul à la fois par point). `at` : relevé saisi hors ligne, replacé dans le temps",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "value": {
          "type": "number"
         },
         "at": {
          "type": "integer",
          "format": "int64",
          "description": "Heure du geste (ms epoch), saisie hors ligne : 48 h au plus dans le passé ; un léger futur est ramené à l'heure du serveur. Absent : heure de réception"
         }
        },
        "required": [
         "value"
        ]
       }
      }
     }
    }
   }
  },
  "/api/assets/{id}/report-suggestions": {
   "get": {
    "tags": [
     "Interventions"
    ],
    "summary": "Descriptions proposées en puces pour signaler une panne (6 au plus) : pannes déclarées sur la même classe d'équipement (hors préventif, réparation définitive, sous-OT, réparation urgente et réserve de contrôle), dans les usines autorisées de même langue que celle de l'équipement, normalisées (casse, accents, espaces, ponctuation finale) et comptées, puis symptômes types de la classe dans la langue de l'usine. Rôle déclarant (pas « external »)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "suggestions": {
           "type": "array",
           "items": {
            "$ref": "#/components/schemas/ReportSuggestion"
           }
          }
         }
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/assets/{id}/suggested-codes": {
   "get": {
    "tags": [
     "Interventions"
    ],
    "summary": "Codes ISO 14224 les plus fréquents de l'équipement (mode admis pour la classe, mécanisme, cause, activité), appris des seules usines autorisées",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Codes"
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/roster": {
   "post": {
    "tags": [
     "Planning"
    ],
    "summary": "Déclarer une absence (leave, sick, training) ou une astreinte (on_call) — sans chevauchement ; un absent n'est ni affecté ni planifié",
    "parameters": [],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "technician_id": {
          "type": "string"
         },
         "kind": {
          "enum": [
           "leave",
           "sick",
           "training",
           "on_call"
          ]
         },
         "start": {
          "type": "integer",
          "format": "int64",
          "description": "Horodatage en millisecondes (epoch)"
         },
         "end": {
          "type": "integer",
          "format": "int64",
          "description": "Horodatage en millisecondes (epoch)"
         },
         "note": {
          "type": "string"
         }
        },
        "required": [
         "technician_id",
         "kind",
         "start",
         "end"
        ]
       }
      }
     }
    }
   }
  },
  "/api/roster/{id}/cancel": {
   "post": {
    "tags": [
     "Planning"
    ],
    "summary": "Annuler une absence ou une astreinte",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/work-orders/{id}/subcontract": {
   "post": {
    "tags": [
     "Contrats"
    ],
    "summary": "Confier l'OT à un prestataire. `cost` : montant facturé ; `quote` : devis (coût prévu de l'analyse), modifiable tant que l'OT est ouvert — absent : inchangé (aucun pour un nouveau prestataire), `null` : retiré",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "vendor_id": {
          "type": "string"
         },
         "cost": {
          "type": "number"
         },
         "quote": {
          "type": [
           "number",
           "null"
          ]
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/work-orders/{id}/lockout/{point}/{action}": {
   "post": {
    "tags": [
     "Sécurité"
    ],
    "summary": "Consigner (lock), vérifier (verify) ou déconsigner (unlock) un point. Corps facultatif : `at` (heure du geste, ordre de la procédure respecté, habilitation évaluée à cette heure), `energy` et `label` du point vu par le technicien — si la procédure a changé, 422 procedure_changed",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "point",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "action",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": false,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "at": {
          "type": "integer",
          "format": "int64",
          "description": "Heure du geste (ms epoch), saisie hors ligne : 48 h au plus dans le passé ; un léger futur est ramené à l'heure du serveur. Absent : heure de réception"
         },
         "energy": {
          "type": "string"
         },
         "label": {
          "type": "string"
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/work-orders/{id}/hygiene-release": {
   "post": {
    "tags": [
     "Sécurité"
    ],
    "summary": "Signer la levée d'hygiène (admin, responsable, qualité) : toujours par une autre personne que celle qui a clôturé ou pointé sur l'OT, quelle que soit la zone (422 sinon)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "note": {
          "type": "string"
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/micro-stops": {
   "post": {
    "tags": [
     "Interventions"
    ],
    "summary": "Déclarer un micro-arrêt (refusé pendant un arrêt déclaré : déjà compté avec la panne)",
    "parameters": [],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "asset_id": {
          "type": "string"
         },
         "minutes": {
          "type": "integer"
         },
         "reason": {
          "type": "string"
         },
         "client_ref": {
          "type": "string",
          "description": "Référence de l'appareil (UUID, 8 à 64 caractères [A-Za-z0-9_-]) : un envoi rejoué renvoie l'enregistrement déjà créé"
         },
         "at": {
          "type": "integer",
          "format": "int64",
          "description": "Heure du geste (ms epoch), saisie hors ligne : 48 h au plus dans le passé ; un léger futur est ramené à l'heure du serveur. Absent : heure de réception"
         }
        },
        "required": [
         "asset_id",
         "minutes",
         "reason"
        ]
       }
      }
     }
    }
   }
  },
  "/api/plans": {
   "post": {
    "tags": [
     "Préventif"
    ],
    "summary": "Créer des plans préventifs (plusieurs équipements). `operations` : gamme type recopiée dans chaque OT généré (phases 10, 20, 30…)",
    "parameters": [],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "array",
         "items": {
          "type": "object"
         }
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "asset_ids": {
          "type": "array",
          "items": {
           "type": "string"
          }
         },
         "title": {
          "type": "string"
         },
         "interval_hours": {
          "type": "number"
         },
         "interval_days": {
          "type": "integer"
         },
         "instructions": {
          "type": "string"
         },
         "checklist": {
          "type": "array",
          "items": {
           "type": "string"
          }
         },
         "operations": {
          "type": "array",
          "items": {
           "$ref": "#/components/schemas/OperationTemplate"
          }
         }
        },
        "required": [
         "asset_ids",
         "title"
        ]
       }
      }
     }
    }
   }
  },
  "/api/plans/{id}": {
   "patch": {
    "tags": [
     "Préventif"
    ],
    "summary": "Modifier un plan (`operations` absent : gamme inchangée)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "title": {
          "type": "string"
         },
         "interval_hours": {
          "type": "number"
         },
         "interval_days": {
          "type": "integer"
         },
         "instructions": {
          "type": "string"
         },
         "checklist": {
          "type": "array",
          "items": {
           "type": "string"
          }
         },
         "active": {
          "type": "boolean"
         },
         "operations": {
          "type": "array",
          "items": {
           "$ref": "#/components/schemas/OperationTemplate"
          }
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/calc": {
   "get": {
    "tags": [
     "Calculs"
    ],
    "summary": "Catalogue des calculateurs (tout compte connecté, commun à tous les tenants) : entrées (clé, unité, bornes comprises, défaut, choix, condition d'affichage), sorties (clé, unité, décimales), avertissements, refus, formule, référence, calcul de sécurité, suites possibles",
    "parameters": [],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "array",
         "items": {
          "type": "object"
         }
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/calc/{id}": {
   "post": {
    "tags": [
     "Calculs"
    ],
    "summary": "Calcule sans rien enregistrer (tout compte connecté). Corps : les entrées `{clé: nombre | texte | booléen | liste}`, les absentes prennent leur défaut. 422 `out_of_range` avec `field`, `min`, `max` ; 404 sur un calculateur inconnu",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/CalcOutcome"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    }
   }
  },
  "/api/work-orders/{id}/calculations": {
   "post": {
    "tags": [
     "Calculs"
    ],
    "summary": "Joint un calcul à l'OT (rôle d'exécution, usine de l'OT) : le serveur refait le calcul avec sa version des formules, garde les entrées et les sorties, et avec `add_step` ajoute l'étape à cocher (clé `calc:<id>`, 50 étapes au plus) dans la langue de l'usine ; tracé dans le fil. 20 calculs au plus par OT. Idempotent par `client_ref`. 422 `calc_version` si la version diffère, `out_of_range` sur une entrée hors bornes, `closed` sur un OT clôturé",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/CalcRecord"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "calc_id": {
          "type": "string"
         },
         "version": {
          "type": "integer"
         },
         "inputs": {
          "type": "object"
         },
         "add_step": {
          "type": "boolean"
         },
         "client_ref": {
          "type": "string",
          "description": "Référence de l'appareil (UUID, 8 à 64 caractères [A-Za-z0-9_-]) : un envoi rejoué renvoie l'enregistrement déjà créé"
         },
         "at": {
          "type": "integer",
          "format": "int64",
          "description": "Heure du geste (ms epoch), saisie hors ligne : 48 h au plus dans le passé ; un léger futur est ramené à l'heure du serveur. Absent : heure de réception"
         }
        },
        "required": [
         "calc_id",
         "version",
         "inputs"
        ]
       }
      }
     }
    }
   }
  },
  "/api/plans/{id}/generate": {
   "post": {
    "tags": [
     "Préventif"
    ],
    "summary": "Générer l'OT d'un plan",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/WorkOrder"
        }
       }
      }
     }
    }
   }
  },
  "/api/parts": {
   "post": {
    "tags": [
     "Stock"
    ],
    "summary": "Créer un article ou un kit (`components`, `kit_base`), avec un conditionnement facultatif (`pack`)",
    "parameters": [],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Part"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    }
   }
  },
  "/api/parts/{id}": {
   "patch": {
    "tags": [
     "Stock"
    ],
    "summary": "Modifier un article (`components` absent : composition inchangée)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    }
   }
  },
  "/api/parts/{id}/receive": {
   "post": {
    "tags": [
     "Stock"
    ],
    "summary": "Réception (coût moyen pondéré). `unit` : conditionnement ou multiple usuel, le coût unitaire est alors celui de cette unité ; `lot` tracé sur le mouvement. Refusée sur un kit (pas de stock propre)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "qty": {
          "type": "number"
         },
         "unit": {
          "type": "string"
         },
         "unit_cost": {
          "type": "number"
         },
         "lot": {
          "type": "string"
         },
         "note": {
          "type": "string"
         }
        },
        "required": [
         "qty"
        ]
       }
      }
     }
    }
   }
  },
  "/api/parts/{id}/count": {
   "post": {
    "tags": [
     "Stock"
    ],
    "summary": "Inventaire",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "qty": {
          "type": "number"
         },
         "note": {
          "type": "string"
         }
        },
        "required": [
         "qty"
        ]
       }
      }
     }
    }
   }
  },
  "/api/purchases": {
   "post": {
    "tags": [
     "Achats"
    ],
    "summary": "Demande d'achat",
    "parameters": [],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Purchase"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "site_id": {
          "type": "string"
         },
         "supplier": {
          "type": "string"
         },
         "lines": {
          "type": "array",
          "items": {
           "type": "object",
           "properties": {
            "part_id": {
             "type": "string"
            },
            "qty": {
             "type": "number"
            },
            "unit_cost": {
             "type": "number"
            }
           }
          }
         },
         "work_order_id": {
          "type": "string"
         },
         "note": {
          "type": "string"
         }
        },
        "required": [
         "site_id",
         "lines"
        ]
       }
      }
     }
    }
   }
  },
  "/api/purchases/reorder": {
   "post": {
    "tags": [
     "Achats"
    ],
    "summary": "Réapprovisionnement automatique d'une usine",
    "parameters": [],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "array",
         "items": {
          "$ref": "#/components/schemas/Purchase"
         }
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "site_id": {
          "type": "string"
         }
        },
        "required": [
         "site_id"
        ]
       }
      }
     }
    }
   }
  },
  "/api/purchases/{id}/lines/{part}": {
   "post": {
    "tags": [
     "Achats"
    ],
    "summary": "Ligne d'un brouillon",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "part",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "qty": {
          "type": "number"
         },
         "unit_cost": {
          "type": "number"
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/purchases/{id}/order": {
   "post": {
    "tags": [
     "Achats"
    ],
    "summary": "Passer la commande",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "supplier": {
          "type": "string"
         },
         "expected_at": {
          "type": "integer",
          "format": "int64",
          "description": "Horodatage en millisecondes (epoch)"
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/purchases/{id}/receive": {
   "post": {
    "tags": [
     "Achats"
    ],
    "summary": "Réceptionner une livraison",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "array",
        "items": {
         "type": "object",
         "properties": {
          "part_id": {
           "type": "string"
          },
          "qty": {
           "type": "number"
          }
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/purchases/{id}/cancel": {
   "post": {
    "tags": [
     "Achats"
    ],
    "summary": "Annuler",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/inspections": {
   "post": {
    "tags": [
     "Contrôles"
    ],
    "summary": "Créer un contrôle réglementaire / étalonnage",
    "parameters": [],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Inspection"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    }
   }
  },
  "/api/inspections/library": {
   "get": {
    "tags": [
     "Contrôles"
    ],
    "summary": "Bibliothèque des contrôles usuels d'un atelier en France (VGP chariots et levage, presses, équipements sous pression, électricité, extincteurs, portes, ventilation, fluides frigorigènes, arrêts d'urgence) : classes concernées, périodicité, texte de référence indicatif, classes cochées d'office (`primary`) et mots du nom qui le font cocher (`keywords`)",
    "parameters": [],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "array",
         "items": {
          "type": "object",
          "properties": {
           "code": {
            "type": "string"
           },
           "asset_kinds": {
            "type": "array",
            "items": {
             "type": "string"
            }
           },
           "kind": {
            "type": "string"
           },
           "title": {
            "type": "string"
           },
           "reference": {
            "type": "string"
           },
           "body": {
            "type": "string"
           },
           "interval_days": {
            "type": "integer"
           },
           "note": {
            "type": "string"
           },
           "primary": {
            "type": "array",
            "items": {
             "type": "string"
            }
           },
           "keywords": {
            "type": "array",
            "items": {
             "type": "string"
            }
           }
          }
         }
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   },
   "post": {
    "tags": [
     "Contrôles"
    ],
    "summary": "Responsable : pose des contrôles de la bibliothèque. `last_at` (date du dernier contrôle) fixe la prochaine échéance ; inconnue, échéance dans 30 jours. Un contrôle déjà suivi (même intitulé, actif) n'est pas doublé ; un code inconnu ou un contrôle qui ne convient pas à la classe refuse tout le lot (2 000 au plus)",
    "parameters": [],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "created": {
           "type": "integer"
          },
          "skipped": {
           "type": "integer"
          }
         }
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "items": {
          "type": "array",
          "items": {
           "type": "object",
           "properties": {
            "code": {
             "type": "string"
            },
            "asset_id": {
             "type": "string"
            },
            "last_at": {
             "type": "integer",
             "format": "int64",
             "description": "Horodatage en millisecondes (epoch)"
            }
           },
           "required": [
            "code",
            "asset_id"
           ]
          }
         }
        },
        "required": [
         "items"
        ]
       }
      }
     }
    }
   }
  },
  "/api/inspections/{id}": {
   "patch": {
    "tags": [
     "Contrôles"
    ],
    "summary": "Modifier un contrôle",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    }
   }
  },
  "/api/inspections/{id}/results": {
   "post": {
    "tags": [
     "Contrôles"
    ],
    "summary": "Saisir un résultat (réserves → OT de levée)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "result": {
          "enum": [
           "compliant",
           "reserves",
           "non_compliant"
          ]
         },
         "inspector": {
          "type": "string"
         },
         "certificate": {
          "type": "string"
         },
         "note": {
          "type": "string"
         },
         "at": {
          "type": "integer",
          "format": "int64",
          "description": "Horodatage en millisecondes (epoch)"
         }
        },
        "required": [
         "result"
        ]
       }
      }
     }
    }
   }
  },
  "/api/vendors": {
   "post": {
    "tags": [
     "Contrats"
    ],
    "summary": "Créer un prestataire",
    "parameters": [],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Vendor"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    }
   }
  },
  "/api/vendors/{id}": {
   "patch": {
    "tags": [
     "Contrats"
    ],
    "summary": "Modifier un prestataire",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    }
   }
  },
  "/api/contracts": {
   "post": {
    "tags": [
     "Contrats"
    ],
    "summary": "Créer un contrat",
    "parameters": [],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Contract"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    }
   }
  },
  "/api/contracts/{id}": {
   "patch": {
    "tags": [
     "Contrats"
    ],
    "summary": "Modifier un contrat",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    }
   }
  },
  "/api/threads/{model}/{id}/messages": {
   "post": {
    "tags": [
     "Collaboration"
    ],
    "summary": "Message ou note interne",
    "parameters": [
     {
      "name": "model",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "body": {
          "type": "string"
         },
         "kind": {
          "type": "string"
         },
         "client_ref": {
          "type": "string",
          "description": "Référence de l'appareil (UUID, 8 à 64 caractères [A-Za-z0-9_-]) : un envoi rejoué renvoie l'enregistrement déjà créé"
         },
         "at": {
          "type": "integer",
          "format": "int64",
          "description": "Heure du geste (ms epoch), saisie hors ligne : 48 h au plus dans le passé ; un léger futur est ramené à l'heure du serveur. Absent : heure de réception"
         }
        },
        "required": [
         "body"
        ]
       }
      }
     }
    }
   }
  },
  "/api/activities": {
   "post": {
    "tags": [
     "Collaboration"
    ],
    "summary": "Planifier une activité",
    "parameters": [],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object"
       }
      }
     }
    }
   }
  },
  "/api/attachments/{model}/{id}": {
   "get": {
    "tags": [
     "Collaboration"
    ],
    "summary": "Pièces jointes d'un enregistrement (`work_order`, `asset`, `inspection`) ; un compte limité à ses usines ne voit que celles de ses usines, même pour une fiche qui n'est plus en mémoire",
    "parameters": [
     {
      "name": "model",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "array",
         "items": {
          "type": "object"
         }
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   },
   "post": {
    "tags": [
     "Collaboration"
    ],
    "summary": "Ajouter une photo ou un document (corps brut, en-têtes `content-type` et `x-filename`) à un OT, un équipement ou un contrôle (le PV de l'organisme, droit de planifier ; cité dans le dossier d'audit). Photo prise hors ligne : `client_id` (att-<référence>, un envoi rejoué renvoie la même pièce) et `at` (heure de la prise de vue) en paramètres",
    "parameters": [
     {
      "name": "model",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "client_id",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "at",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer",
       "format": "int64",
       "description": "Heure du geste (ms epoch), saisie hors ligne : 48 h au plus dans le passé ; un léger futur est ramené à l'heure du serveur. Absent : heure de réception"
      }
     }
    ],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "*/*": {
       "schema": {
        "type": "string",
        "format": "binary"
       }
      }
     }
    }
   }
  },
  "/api/import/assets": {
   "post": {
    "tags": [
     "Données"
    ],
    "summary": "Import d'équipements, CSV ou classeur Excel .xlsx (tout ou rien)",
    "parameters": [],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "string",
        "description": "CSV ou .xlsx"
       }
      }
     }
    }
   }
  },
  "/api/import/work-orders": {
   "post": {
    "tags": [
     "Données"
    ],
    "summary": "Reprise de l'historique des interventions, CSV ou .xlsx (tout ou rien) : OT clôturés avec compte rendu et temps passés. Codes ISO 14224 facultatifs (failure_mode, mechanism, cause_code, activity, detection) : un code inconnu rejette la ligne, une ligne sans code est reprise telle quelle. Réponse : imported, et uncoded (correctifs sans mode ni cause, « non renseignés »)",
    "parameters": [],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "imported": {
           "type": "integer"
          },
          "uncoded": {
           "type": "integer"
          }
         }
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "string",
        "description": "CSV ou .xlsx"
       }
      }
     }
    }
   }
  },
  "/api/import/work-orders/template": {
   "get": {
    "tags": [
     "Données"
    ],
    "summary": "Modèle CSV de l'historique",
    "parameters": [],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/export/assets.csv": {
   "get": {
    "tags": [
     "Données"
    ],
    "summary": "Export CSV des équipements",
    "parameters": [],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/export/work-orders.csv": {
   "get": {
    "tags": [
     "Données"
    ],
    "summary": "Export CSV des OT, avec leurs codes ISO 14224 (même vocabulaire que la reprise d'historique)",
    "parameters": [],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/reports": {
   "get": {
    "tags": [
     "Rapports"
    ],
    "summary": "Rapport de maintenance sur tout l'historique (EN 15341) : disponibilité R1, MTBF, MTTR, taux de codification ISO 14224 (correctifs clôturés avec mode et cause), part du préventif, conformité, backlog, coûts, Pareto, tendance et comparatif entre usines. Une réparation définitive n'est pas une nouvelle défaillance (ni dans le compte, ni dans le Pareto). Rôles admin, manager, qualité ; usines autorisées seulement",
    "parameters": [
     {
      "name": "site_id",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      },
      "description": "Usine ; absent : toutes les usines autorisées (comparatif)"
     },
     {
      "name": "from",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer",
       "format": "int64",
       "description": "Horodatage en millisecondes (epoch)"
      },
      "description": "Début (ms) ; arrondi au jour UTC. Défaut : 30 jours avant `to`"
     },
     {
      "name": "to",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer",
       "format": "int64",
       "description": "Horodatage en millisecondes (epoch)"
      },
      "description": "Fin exclue (ms). Défaut : demain 00:00 UTC. 3 660 jours au plus"
     },
     {
      "name": "line_id",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "asset_kind",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Report"
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "400": {
      "description": "Paramètre illisible",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "Usine ou ligne inconnue",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/export/reports.csv": {
   "get": {
    "tags": [
     "Rapports"
    ],
    "summary": "Une table du rapport en CSV (UTF-8 avec BOM, clés anglaises) : table=indicators|assets|series|sites. Les cellules texte qui commencent par = + - @ sont préfixées d'une apostrophe",
    "parameters": [
     {
      "name": "site_id",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      },
      "description": "Usine ; absent : toutes les usines autorisées (comparatif)"
     },
     {
      "name": "from",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer",
       "format": "int64",
       "description": "Horodatage en millisecondes (epoch)"
      },
      "description": "Début (ms) ; arrondi au jour UTC. Défaut : 30 jours avant `to`"
     },
     {
      "name": "to",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer",
       "format": "int64",
       "description": "Horodatage en millisecondes (epoch)"
      },
      "description": "Fin exclue (ms). Défaut : demain 00:00 UTC. 3 660 jours au plus"
     },
     {
      "name": "line_id",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "asset_kind",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "table",
      "in": "query",
      "required": false,
      "schema": {
       "enum": [
        "indicators",
        "assets",
        "series",
        "sites"
       ]
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "400": {
      "description": "Paramètre illisible",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "Usine ou ligne inconnue",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/export/reports.xlsx": {
   "get": {
    "tags": [
     "Rapports"
    ],
    "summary": "Rapport en classeur Excel : Indicateurs (avec formules), Équipements, Tendance, Usines en vue réseau",
    "parameters": [
     {
      "name": "site_id",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      },
      "description": "Usine ; absent : toutes les usines autorisées (comparatif)"
     },
     {
      "name": "from",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer",
       "format": "int64",
       "description": "Horodatage en millisecondes (epoch)"
      },
      "description": "Début (ms) ; arrondi au jour UTC. Défaut : 30 jours avant `to`"
     },
     {
      "name": "to",
      "in": "query",
      "required": false,
      "schema": {
       "type": "integer",
       "format": "int64",
       "description": "Horodatage en millisecondes (epoch)"
      },
      "description": "Fin exclue (ms). Défaut : demain 00:00 UTC. 3 660 jours au plus"
     },
     {
      "name": "line_id",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "asset_kind",
      "in": "query",
      "required": false,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "400": {
      "description": "Paramètre illisible",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "Usine ou ligne inconnue",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/sites/{id}/audit-dossier.pdf": {
   "get": {
    "tags": [
     "Données"
    ],
    "summary": "Dossier d'audit PDF d'une usine (query : standard=ifs|brcgs|iatf, from, to en ms) : synthèse, interventions, réparations temporaires, levées d'hygiène, contrôles et étalonnages, consignations, permis, signatures ; empreinte SHA-256 journalisée",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/export/plans.csv": {
   "get": {
    "tags": [
     "Données"
    ],
    "summary": "Export CSV des plans préventifs",
    "parameters": [],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/export/audit.csv": {
   "get": {
    "tags": [
     "Données"
    ],
    "summary": "Export CSV complet du journal d'audit",
    "parameters": [],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/export/all.zip": {
   "get": {
    "tags": [
     "Données"
    ],
    "summary": "Export complet du compte en .zip : LISEZMOI.txt, donnees.json (toutes les données des usines autorisées, sans les adresses des demandeurs), equipements.csv, interventions.csv, preventif.csv, controles.csv, pieces.csv, journal-audit.csv (droit d'audit) et fichiers/ (pièces jointes, 300 Mo au plus, les autres listées). Reste ouvert après la fin d'un essai",
    "parameters": [],
    "responses": {
     "200": {
      "description": "Archive .zip",
      "content": {
       "application/zip": {
        "schema": {
         "type": "string",
         "format": "binary"
        }
       }
      }
     }
    }
   }
  },
  "/api/audit": {
   "get": {
    "tags": [
     "Administration"
    ],
    "summary": "Journal d'audit",
    "parameters": [],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "array",
         "items": {
          "type": "object"
         }
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/notifications": {
   "get": {
    "tags": [
     "Administration"
    ],
    "summary": "E-mails envoyés aux demandeurs",
    "parameters": [],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "array",
         "items": {
          "type": "object"
         }
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/api-keys": {
   "get": {
    "tags": [
     "Intégrations"
    ],
    "summary": "Clés d'API (administrateur)",
    "parameters": [],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "array",
         "items": {
          "type": "object"
         }
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   },
   "post": {
    "tags": [
     "Intégrations"
    ],
    "summary": "Créer une clé (le jeton n'est affiché qu'une fois)",
    "parameters": [],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "key": {
           "type": "object"
          },
          "token": {
           "type": "string"
          }
         }
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "name": {
          "type": "string"
         },
         "user_email": {
          "type": "string"
         }
        },
        "required": [
         "name",
         "user_email"
        ]
       }
      }
     }
    }
   }
  },
  "/api/api-keys/{id}": {
   "delete": {
    "tags": [
     "Intégrations"
    ],
    "summary": "Révoquer une clé",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/webhooks": {
   "get": {
    "tags": [
     "Intégrations"
    ],
    "summary": "Webhooks",
    "parameters": [],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "array",
         "items": {
          "type": "object"
         }
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   },
   "post": {
    "tags": [
     "Intégrations"
    ],
    "summary": "Créer un webhook (le secret n'est affiché qu'une fois)",
    "parameters": [],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "webhook": {
           "type": "object"
          },
          "secret": {
           "type": "string"
          }
         }
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "url": {
          "type": "string"
         },
         "events": {
          "type": "array",
          "items": {
           "type": "string"
          }
         }
        },
        "required": [
         "url"
        ]
       }
      }
     }
    }
   }
  },
  "/api/webhooks/{id}": {
   "patch": {
    "tags": [
     "Intégrations"
    ],
    "summary": "Modifier un webhook",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "url": {
          "type": "string"
         },
         "events": {
          "type": "array",
          "items": {
           "type": "string"
          }
         },
         "active": {
          "type": "boolean"
         }
        }
       }
      }
     }
    }
   },
   "delete": {
    "tags": [
     "Intégrations"
    ],
    "summary": "Supprimer un webhook",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/webhooks/{id}/ping": {
   "post": {
    "tags": [
     "Intégrations"
    ],
    "summary": "Envoyer un événement de test",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/webhooks/deliveries": {
   "get": {
    "tags": [
     "Intégrations"
    ],
    "summary": "Dernières livraisons",
    "parameters": [],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "array",
         "items": {
          "type": "object"
         }
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/public/{tenant}/assets/{token}": {
   "get": {
    "tags": [
     "Portail public"
    ],
    "summary": "Équipement scanné (QR), sans compte : OT ouverts et descriptions fréquentes de sa classe (apprises de sa seule usine)",
    "parameters": [
     {
      "name": "tenant",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "token",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/PublicAsset"
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "security": []
   }
  },
  "/api/public/{tenant}/assets/{token}/requests": {
   "post": {
    "tags": [
     "Portail public"
    ],
    "summary": "Demande sans compte (5 / 10 min) ; `machine_stopped` : la machine passe « En panne » aussitôt",
    "parameters": [
     {
      "name": "tenant",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "token",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "code": {
           "type": "string"
          },
          "reference": {
           "type": "string"
          }
         }
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "title": {
          "type": "string"
         },
         "requester": {
          "type": "string"
         },
         "urgent": {
          "type": "boolean"
         },
         "email": {
          "type": "string"
         },
         "machine_stopped": {
          "type": "boolean"
         }
        },
        "required": [
         "title",
         "requester"
        ]
       }
      }
     }
    },
    "security": []
   }
  },
  "/api/public/{tenant}/requests/{reference}": {
   "get": {
    "tags": [
     "Portail public"
    ],
    "summary": "Suivi d'une demande (aussi pour un OT clos qui n'est plus en mémoire)",
    "parameters": [
     {
      "name": "tenant",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "reference",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/PublicTracking"
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "security": []
   }
  },
  "/api/public/{tenant}/requests/{reference}/followups": {
   "post": {
    "tags": [
     "Portail public"
    ],
    "summary": "Relancer une demande ouverte depuis le lien de suivi : 3 relances au plus, une heure après la demande puis après la relance précédente (422 followup_too_soon, followup_limit ; closed si la demande est close). Message facultatif de 300 caractères, tracé dans le fil de l'OT au nom du demandeur, diffusé (écart « relancée » dans le jumeau et la cloche) et envoyé par e-mail aux comptes responsables qui voient l'usine (lien vers la fiche de l'OT). 5 appels / 10 min par référence (429)",
    "parameters": [
     {
      "name": "tenant",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "reference",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "message": {
          "type": "string"
         }
        }
       }
      }
     }
    },
    "security": []
   }
  },
  "/api/public/{tenant}/requests/{reference}/rating": {
   "post": {
    "tags": [
     "Portail public"
    ],
    "summary": "Noter la réponse à une demande close : une note de 1 à 5, une seule fois (rating_done), dans les 30 jours après la clôture (rating_expired), avant la clôture refusée (rating_open) ; commentaire facultatif de 500 caractères, gardé sur l'OT seul. Une demande close sortie de la mémoire est notée en base d'un seul geste (deux appels simultanés : une seule note). Comptée au taux de satisfaction de l'usine (rapports). 5 appels / 10 min par référence (429)",
    "parameters": [
     {
      "name": "tenant",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "reference",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "score": {
          "type": "integer",
          "minimum": 1,
          "maximum": 5
         },
         "comment": {
          "type": "string"
         }
        },
        "required": [
         "score"
        ]
       }
      }
     }
    },
    "security": []
   }
  },
  "/api/public/demo-requests": {
   "post": {
    "tags": [
     "Landing"
    ],
    "summary": "Demande de présentation déposée sur la landing commerciale, sans compte : 201 et sa référence (`DP-` suivi de 10 caractères), seule donnée écrite dans les journaux. Globale à la plateforme (aucun tenant). 422 : un code stable par champ. 5 demandes par adresse e-mail et 30 pour toute la plateforme par tranche de 10 minutes (429 rate_limited). Champ piège `website` rempli : réponse 201 ordinaire, rien n'est gardé ni compté. Si `DEMO_REQUESTS_TO` et `SMTP_URL` sont définis, l'équipe reçoit un e-mail en texte",
    "parameters": [],
    "responses": {
     "422": {
      "description": "Champs à corriger",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/DemoRequestErrors"
        }
       }
      }
     },
     "201": {
      "description": "Demande reçue (ou jetée sans le dire, si le champ piège est rempli)",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "reference": {
           "type": "string"
          }
         },
         "required": [
          "reference"
         ]
        }
       }
      }
     },
     "400": {
      "description": "Corps JSON illisible ou d'un mauvais type",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "413": {
      "description": "Corps de plus de 32 Ko",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "415": {
      "description": "Corps attendu en application/json",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "description": "Trop de demandes (rate_limited)",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/DemoRequestInput"
       }
      }
     }
    },
    "security": []
   }
  },
  "/api/platform/tenants": {
   "get": {
    "tags": [
     "Éditeur"
    ],
    "summary": "Clients de l'instance (usines, comptes). Jeton de l'éditeur `PLATFORM_TOKEN` en `Authorization: Bearer` ; sans PLATFORM_TOKEN configuré, la route n'existe pas (404)",
    "parameters": [],
    "responses": {
     "200": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "array",
         "items": {
          "type": "object",
          "properties": {
           "id": {
            "type": "string"
           },
           "name": {
            "type": "string"
           },
           "sites": {
            "type": "integer"
           },
           "users": {
            "type": "integer"
           }
          }
         }
        }
       }
      }
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "security": [
     {
      "platform": []
     }
    ]
   },
   "post": {
    "tags": [
     "Éditeur"
    ],
    "summary": "Créer un client sans redémarrer : tenant vide (identifiant de 3 à 40 caractères a-z, 0-9 et tirets) et son premier administrateur, invité : il choisit son mot de passe par un lien de 7 jours, envoyé par e-mail et rendu une seule fois. 409 si l'identifiant ou l'adresse existe",
    "parameters": [],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "tenant": {
           "type": "object",
           "properties": {
            "id": {
             "type": "string"
            },
            "name": {
             "type": "string"
            }
           }
          },
          "admin": {
           "type": "object",
           "properties": {
            "id": {
             "type": "string"
            },
            "email": {
             "type": "string"
            },
            "invite": {
             "type": "object",
             "properties": {
              "url": {
               "type": "string"
              },
              "purpose": {
               "type": "string"
              },
              "expires_at": {
               "type": "integer"
              },
              "emailed": {
               "type": "boolean"
              }
             }
            }
           }
          }
         }
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "id": {
          "type": "string"
         },
         "name": {
          "type": "string"
         },
         "admin_email": {
          "type": "string"
         },
         "admin_name": {
          "type": "string"
         },
         "locale": {
          "type": "string"
         }
        },
        "required": [
         "id",
         "name",
         "admin_email",
         "admin_name"
        ]
       }
      }
     }
    },
    "security": [
     {
      "platform": []
     }
    ]
   }
  },
  "/api/platform/tenants/{id}/trial": {
   "patch": {
    "tags": [
     "Éditeur"
    ],
    "summary": "Prolonger l'essai d'un client (`trial_ends_at` en ms) ou le passer sous contrat (`null`). 404 si le client n'existe plus (essai effacé entre-temps)",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "204": {
      "description": "OK"
     },
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "trial_ends_at": {
          "oneOf": [
           {
            "type": "integer",
            "format": "int64",
            "description": "Horodatage en millisecondes (epoch)"
           },
           {
            "type": "null"
           }
          ]
         }
        }
       }
      }
     }
    },
    "security": [
     {
      "platform": []
     }
    ]
   }
  },
  "/api/public/unsubscribe": {
   "post": {
    "tags": [
     "Landing"
    ],
    "summary": "Lien personnel de désabonnement d'un mail de rappel (`/desabonnement#gmaounsub_…`, en-tête List-Unsubscribe) : coupe les rappels de cette personne seulement. 410 si le lien est falsifié ; limite par adresse IP",
    "parameters": [],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "2XX": {
      "description": "OK",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "email": {
           "type": "string"
          },
          "name": {
           "type": "string"
          }
         }
        }
       }
      }
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "token": {
          "type": "string"
         }
        },
        "required": [
         "token"
        ]
       }
      }
     }
    },
    "security": []
   }
  },
  "/api/public/signup": {
   "post": {
    "tags": [
     "Landing"
    ],
    "summary": "Essai gratuit de 30 jours sans rendez-vous (`SIGNUP=on` et SMTP configuré, sinon 404) : rien n'est créé tant que l'adresse n'est pas vérifiée ; un lien d'activation de 7 jours (`/acces#gmaotrial_…`) part à l'adresse, l'espace s'ouvre au choix du mot de passe (`/api/auth/reset`) et l'équipe commerciale est prévenue (`DEMO_REQUESTS_TO`). Toujours 202, que l'adresse ait déjà un compte ou non ; champ piège `website` ; limite par adresse IP (429). À la fin de l'essai, le compte passe en lecture seule (402 `trial_ended` sur toute écriture ; consultation et export restent ouverts) ; sans souscription, il est effacé 90 jours après la fin de l'essai, au plus tôt 7 jours après l'avertissement des administrateurs. Trois liens d'activation par adresse et par jour au plus ; nom et entreprise sans caractère de contrôle (422)",
    "parameters": [],
    "responses": {
     "401": {
      "description": "Non authentifié"
     },
     "403": {
      "description": "Rôle ou usine non autorisés"
     },
     "422": {
      "description": "Règle métier non respectée",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "202": {
      "description": "OK"
     }
    },
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "company": {
          "type": "string"
         },
         "name": {
          "type": "string"
         },
         "email": {
          "type": "string"
         },
         "phone": {
          "type": "string"
         },
         "locale": {
          "type": "string"
         },
         "website": {
          "type": "string"
         }
        },
        "required": [
         "company",
         "name",
         "email"
        ]
       }
      }
     }
    },
    "security": []
   }
  }
 }
}