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

@@ -1,15 +1,18 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import * as Minio from 'minio';
import { loadEnv } from '../config/env';
import type { FileStorage } from './file-storage';
/** Seul fichier du dépôt autorisé à importer le SDK MinIO. */
/** Seul fichier du dépôt autorisé à importer le SDK MinIO (règle ESLint). */
@Injectable()
export class MinioStorageService implements FileStorage {
export class MinioStorageService implements FileStorage, OnModuleInit {
private readonly logger = new Logger(MinioStorageService.name);
private readonly client: Minio.Client;
private readonly bucket: string;
constructor() {
const env = loadEnv();
this.bucket = env.MINIO_BUCKET;
this.client = new Minio.Client({
endPoint: env.MINIO_ENDPOINT,
port: env.MINIO_PORT,
@@ -19,7 +22,34 @@ export class MinioStorageService implements FileStorage {
});
}
/** Le bucket est créé au démarrage — aucune étape manuelle au déploiement. */
async onModuleInit(): Promise<void> {
try {
if (!(await this.client.bucketExists(this.bucket))) {
await this.client.makeBucket(this.bucket);
this.logger.log(`Bucket « ${this.bucket} » créé.`);
}
} catch (e) {
// Le stockage peut être en retard au boot : /health le signalera.
this.logger.warn(`Stockage indisponible au démarrage : ${String(e)}`);
}
}
async healthCheck(): Promise<void> {
await this.client.listBuckets();
}
async putObject(key: string, body: Buffer, contentType: string): Promise<void> {
await this.client.putObject(this.bucket, key, body, body.length, {
'Content-Type': contentType,
});
}
async getObjectStream(key: string): Promise<NodeJS.ReadableStream> {
return this.client.getObject(this.bucket, key);
}
async removeObject(key: string): Promise<void> {
await this.client.removeObject(this.bucket, key);
}
}