feat(r3.2): bibliothèque de documents (FileStorage réel) + analytics

- FileStorage : putObject/getObjectStream/removeObject, bucket créé au
  démarrage — MinIO confiné à son implémentation (règle ESLint intacte)
- documents : upload multipart (PDF/JPG/PNG, 20 Mo max, rattachement
  appareil OU OT requis, permission d'édition sur la CIBLE), liste
  filtrable, téléchargement STREAMÉ par l'API (MinIO jamais exposé),
  suppression ; e2e : octets téléchargés identiques aux octets envoyés
- analytics : GET /analytics/summary dérivé du réel — coûts/mois
  (mouvements + main-d'œuvre figés), pannes par organe (bilans codés),
  taux de préventif, durée moyenne de résolution, top équipements
- générateur OpenAPI : query params, multipart, réponse binaire (71 ops)
- CI : service MinIO (bitnami) sur les jobs api et e2e
- test de régression du tri « Interventions récentes » rendu déterministe
  (positions absolues instables sous 12 suites parallèles) ; 58 tests,
  6 runs complets consécutifs verts

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
pr-daaif
2026-07-16 21:24:04 +01:00
parent 32eb20b5a0
commit 2ffdd13cf4
23 changed files with 1652 additions and 30 deletions

View File

@@ -38,18 +38,26 @@ for (const op of API_CONTRACT) {
operationId: op.operationId,
summary: op.summary,
tags: op.tags,
...(op.pathParams?.length
...(op.pathParams?.length || op.queryParams?.length
? {
parameters: op.pathParams.map((name) => ({
name,
in: 'path',
required: true,
// Convention : « id » / «…Id » sont des UUID, le reste est libre
schema:
name === 'id' || name.endsWith('Id')
? { type: 'string', format: 'uuid' }
: { type: 'string' },
})),
parameters: [
...(op.pathParams ?? []).map((name) => ({
name,
in: 'path',
required: true,
// Convention : « id » / «…Id » sont des UUID, le reste est libre
schema:
name === 'id' || name.endsWith('Id')
? { type: 'string', format: 'uuid' }
: { type: 'string' },
})),
...(op.queryParams ?? []).map((q) => ({
name: q.name,
in: 'query',
required: q.required ?? false,
schema: { type: 'string' },
})),
],
}
: {}),
...(op.isPublic ? {} : { security: [{ bearerAuth: [] }] }),
@@ -70,6 +78,29 @@ for (const op of API_CONTRACT) {
},
}
: {}),
...(op.multipartFields
? {
requestBody: {
required: true,
content: {
'multipart/form-data': {
schema: {
type: 'object',
properties: Object.fromEntries(
Object.entries(op.multipartFields).map(([name, sorte]) => [
name,
sorte === 'file'
? { type: 'string', format: 'binary' }
: { type: 'string' },
]),
),
required: ['file'],
},
},
},
},
}
: {}),
responses,
},
};