# Proposition d'architecture — Centre documentaire global

État : étude uniquement, aucune route, table, migration ou modification du
code n'est créée par cette proposition.

## A. État documentaire actuel

Le dépôt ne possède pas de modèle `Document` générique ni de table commune.
Les documents sont spécialisés par module :

| Modèle | Table | Parent métier | Métadonnées | État UI / routes |
|---|---|---|---|---|
| `InterventionDocument` | `intervention_documents` | `Intervention` obligatoire | `filename`, `filepath`, `description`, `uploaded_at`, `uploaded_by_id` | upload, édition de description, téléchargement et suppression depuis la fiche intervention |
| `EquipmentDocument` | `equipment_documents` | `Equipment` obligatoire | `filename`, `filepath`, `description`, `uploaded_at`, `uploaded_by_id` | deux familles de routes et upload depuis la fiche équipement |
| `ProductDocument` | `cleaning_product_documents` | `CommercialProduct` obligatoire | `document_type`, `title`, `filename`, `filepath`, `version`, `published_at`, `uploaded_at`, `uploaded_by_id`, `is_current` | liste FDS/fiches techniques ; aucun upload ou téléchargement produit clairement exposé dans les routes actuelles |

Le modèle `Contract` possède en plus un champ historique `contract_file` qui
est un simple chemin de fichier, sans ligne documentaire ni métadonnées
associées. Aucun modèle documentaire dédié aux entreprises, à la prévention
ou aux contrats n'a été trouvé. Les pièces jointes Outlook/ENT sont un autre
système d'intégration et restent hors périmètre de ce centre documentaire.

Le centre devra donc fonctionner par adaptateurs de sources et non par
copie des objets existants.

## B. Modèles et relations existants

### Interventions

`Intervention.documents` est une relation vers
`InterventionDocument`. Le document pointe vers l'intervention par
`intervention_id`; l'uploader est facultatif (`users.id`, `SET NULL` côté
modèle logique). Il n'existe pas de type métier normalisé pour le document.

### Équipements

`Equipment.documents` est une relation vers `EquipmentDocument`. Le parent
est obligatoire. La localisation doit être calculée via l'équipement,
éventuellement son parent et `effective_room`, puis `Room → Zone → Building`.

### Produits d'entretien

`CommercialProduct.documents` est une relation vers `ProductDocument`.
Le produit commercial remonte vers le produit générique, le fabricant et le
fournisseur. Une localisation éventuelle doit être calculée via les lots,
balances et `StockLocation → Building/Zone/Room`; elle ne doit pas être
recopiée dans le document.

### Ce qui n'est pas encore un document global

- `Contract.contract_file` est un chemin historique, pas un document
  normalisé.
- Outlook/ENT ont leurs propres pièces jointes distantes et ne doivent pas
  être aspirés dans le centre pendant cette phase.
- Les photos, plans, certificats ou documents de prévention n'ont pas de
  modèle local identifié dans le dépôt.

## C. Stockage actuel

La configuration définit `UPLOAD_FOLDER` sous
`app_new/uploads`. Docker monte ce répertoire dans le volume
`gmao_uploads` (`/app/app_new/uploads`). La limite globale est de 50 MiB.

Les routes historiques utilisent actuellement plusieurs conventions :

- intervention : `uploads/interventions/<uuid>_<secure_filename>` ;
- équipement, blueprint `documents` : `uploads/equipments/<uuid>_<secure_filename>` ;
- équipement, blueprint `equipments_documents` :
  `uploads/documents/<equipment_id>/<uuid>.<extension>` ;
- produit : le modèle stocke un chemin, mais aucune route locale d'upload
  produit n'a été identifiée.

Le fichier physique est donc unique aujourd'hui, mais l'organisation n'est
pas homogène. Les noms sont générés avec UUID ; `secure_filename` réduit le
risque de traversal. Les extensions sont allowlistées (`pdf`, images,
documents Office), mais le MIME réel n'est pas inspecté. Les modèles ne
stockent généralement ni MIME, ni taille, ni hash, et le nom original n'est
pas distingué proprement du nom interne dans toutes les routes.

Le volume n'est pas servi comme répertoire statique public : les téléchargements
passent par `send_file`. Cependant, plusieurs téléchargements font confiance
directement au `filepath` DB et certaines routes ne vérifient pas l'existence
du fichier avant `send_file`.

État observé sur la copie locale au moment de l'étude : 2 lignes
`intervention_documents`, 0 ligne `equipment_documents` et 0 ligne
`cleaning_product_documents`. Les deux fichiers d'intervention présents dans
le volume correspondent aux deux lignes DB. Ce constat est un instantané, pas
une hypothèse de conception.

## D. Routes actuelles

### Blueprint `documents` (`/documents`)

- `POST /documents/upload/intervention/<id>`
- `POST /documents/upload/equipment/<id>`
- `POST /documents/intervention/delete/<doc_id>`
- `POST /documents/intervention/<doc_id>/edit`
- `GET /documents/intervention/download/<doc_id>`
- `POST /documents/equipment/delete/<doc_id>`
- `GET /documents/equipment/download/<doc_id>`

Ces routes utilisent `login_required` et la garde globale d'autorisation,
qui déduit actuellement `intervention.view/manage` ou
`patrimoine.view/manage` du nom d'endpoint.

### Blueprint `equipments_documents` (préfixé `/equipments`)

- `POST /equipments/<id>/documents/upload`
- `GET /equipments/documents/<doc_id>/download`
- `POST /equipments/documents/<doc_id>/delete`

Il s'agit d'une seconde famille d'upload/téléchargement pour les équipements.
Elle utilise un autre répertoire et un autre format de nom. Cette duplication
de routes devra être résolue ou encapsulée avant d'exposer un index global.

### Produits

`GET /cleaning/documents` liste les `ProductDocument`, mais n'offre pas de
route globale, ni de téléchargement produit identifié dans le code inspecté.
`GET /documents/` n'est donc pas l'index attendu par UI-011 et répond 404.

## E. Problèmes actuels à traiter avant ou pendant le MVP

1. Trois schémas de documents et plusieurs conventions de chemin coexistent.
2. Le nom affiché/téléchargé n'est pas toujours le nom original (la route
   intervention stocke notamment le nom UUID comme `filename`).
3. Une route d'upload équipement tente de renseigner `file_type` et
   `file_size`, alors que ces colonnes ne sont pas déclarées sur
   `EquipmentDocument` ; les deux chemins d'upload ne sont donc pas
   équivalents et doivent être consolidés avant extension.
4. Les types métier sont riches pour les produits, mais absents pour les
   interventions et équipements.
5. Le stockage de document produit est modélisé, mais son workflow d'upload/
   download n'est pas complet.
6. `Contract.contract_file` n'est pas interrogeable comme un document.
7. Les routes de téléchargement doivent vérifier parent, permission et
   fichier sous la racine autorisée afin d'éviter IDOR et chemins arbitraires.
8. Une erreur après écriture physique mais avant commit DB peut laisser un
   fichier orphelin ; l'inverse peut laisser une ligne sans fichier.
9. La validation par extension seule ne suffit pas pour les exécutables
   renommés et les fichiers malformés.

## F. Architecture recommandée

### Recommandation : option A pour la V1, avec adaptateurs de lecture

La V1 doit agréger les trois tables spécialisées sans créer une table de
stockage ou recopier les fichiers. Un service de lecture produira un
descripteur commun en mémoire, par exemple :

```text
DocumentResult
  source_kind        intervention | equipment | product
  source_id          identifiant de la ligne documentaire
  parent_id          identifiant de l'objet métier
  display_name       nom lisible
  document_type      type métier normalisé à l'affichage
  description
  uploaded_at / published_at
  stored_path        utilisé seulement côté téléchargement sécurisé
  context_label      Intervention #… / Équipement … / Produit …
  context_url        URL de détail autorisée
  location           calculée par relations métier
  capabilities       view / download / edit / delete
```

Chaque adaptateur sait lire sa table, construire le contexte, appliquer la
permission du module source et produire l'action de téléchargement vers la
ligne propriétaire. Le centre est ainsi un index de recherche logique, pas
un second propriétaire de fichier.

Pour une recherche SQL paginée, les adaptateurs peuvent exposer des sélections
alignées combinées par `UNION ALL` (ou trois requêtes paginées fusionnées pour
le premier incrément). Aucune modification de schéma n'est nécessaire pour
la V1.

### Pourquoi cette option

- aucune migration ni backfill destructif ;
- conservation immédiate des documents existants ;
- aucun fichier copié ;
- réutilisation des relations et permissions existantes ;
- retour arrière simple : retirer la vue globale ne touche pas aux sources.

La contrepartie est une couche d'adaptation plus explicite et une pagination
globale plus complexe si le volume devient très élevé.

## G. Alternative étudiée : option B — index documentaire commun

Une table `document_index` pourrait contenir une ligne par document source,
avec `source_kind`, `source_document_id`, `parent_type`, `parent_id`, type,
dates et éventuellement un hash. Elle ne contiendrait pas une copie du
fichier : le chemin et la suppression resteraient propriétaires de la table
source.

### Avantages

- recherche et pagination uniformes ;
- index SQL uniques et filtres simples ;
- base future pour tags, OCR et classement ;
- détection centralisée des documents orphelins.

### Inconvénients et risques

- migration et backfill des trois tables ;
- synchronisation lors de chaque création, modification, suppression et
  changement de parent ;
- gestion des documents historiques incomplets et de `contract_file` ;
- risque de ligne d'index périmée ou d'un faux sentiment de stockage unifié ;
- complexité supplémentaire avant d'avoir validé les besoins de recherche.

### Recommandation de trajectoire

Ne pas créer cette table pour le MVP. La réévaluer si les mesures montrent
que l'agrégation SQL dépasse les objectifs à 10 000+ documents ou si les tags,
l'OCR et la recherche plein texte deviennent prioritaires. Dans ce cas, la
table restera un index réparable, jamais le propriétaire physique du fichier.

## H. Règle RBAC recommandée

Le centre ne doit jamais élargir l'accès d'un module.

Pour afficher un résultat, appliquer :

```text
permission d'accès au Centre (documents.view)
ET
permission de lecture de la source
```

Exemples :

- document d'intervention : `documents.view` + `intervention.view` ;
- document équipement : `documents.view` + `patrimoine.view` ;
- FDS produit : `documents.view` + `stock.view` ;
- contrat futur : `documents.view` + `contract.view` ;
- prévention future : `documents.view` + `prevention.view`.

Si `documents.view` n'est pas accordée, le menu et l'URL globale sont
refusés. Si la source est refusée, le résultat est absent, même si sa ligne
documentaire est connue.

Le téléchargement réévalue les deux permissions et l'existence du parent ;
il ne doit jamais accepter uniquement un `doc_id` global. La suppression ou
la modification reste dans la fiche source et utilise la permission de
mutation du module (`intervention.manage`, `patrimoine.manage`, etc.). Il
n'est pas recommandé d'introduire un vague `documents.manage` pour remplacer
ces contrôles.

`super_admin` bénéficie de son wildcard existant. Le rôle `admin` et les
rôles métier devront recevoir `documents.view` seulement après décision du
catalogue RBAC ; aucune permission d'intégration ne doit être impliquée.

## I. Classification métier proposée

V1 : un type principal, peu de valeurs et une valeur de repli :

- FDS ;
- Fiche technique ;
- Notice / manuel ;
- Facture ;
- Devis ;
- Rapport / compte rendu ;
- Contrat ;
- Certificat / contrôle ;
- Photo / plan ;
- Autre.

Pour les produits, `ProductDocument.document_type` est conservé et mappé
vers les libellés métier. Pour les interventions et équipements, le type
sera `Autre` tant qu'aucune donnée source ne le porte. Les tags et sous-types
sont différés ; ils ne doivent pas être simulés dans `description`.

## J. Recherche

### Recherche simple visible

Un champ unique recherche : nom de fichier, titre, description, nom de
l'objet parent, référence équipement, produit, intervention et entreprise
accessible par relation.

### SQL classique suffisant

Pour les volumes attendus, SQL/MariaDB suffit avec `LIKE` ciblé et des indexes
sur les clés étrangères, dates, `document_type`, `filename`, `uploaded_at` et
`expiry`/`is_current` côté produits. La recherche plein texte PDF, OCR et
Elasticsearch sont explicitement hors MVP.

### Résultats

Les requêtes doivent filtrer par source avant pagination et ne jamais charger
en mémoire tous les fichiers. Le contenu binaire n'est jamais recherché.

## K. Filtres

### Visibles par défaut

- Type de document ;
- Source/module ;
- période (date d'ajout ou de publication) ;
- recherche libre.

### Filtres avancés repliés

- bâtiment, zone, salle ;
- équipement ;
- produit/FDS ;
- entreprise ;
- actif/courant pour les produits.

Les filtres non applicables à une source doivent rester neutres, pas produire
un faux « aucun résultat ».

## L. UI proposée pour `/documents/`

Titre : **Centre documentaire**. Sous le titre, une phrase explique que les
fichiers restent attachés à leur fiche métier et sont seulement regroupés
pour la recherche.

```text
Recherche [ nom, titre, équipement, produit... ] [Rechercher]

Filtres principaux : Type | Source | Période
Filtres avancés : Bâtiment | Zone | Salle | Équipement | Produit | Entreprise

Document | Type | Lié à | Localisation | Date | Actions
```

Chaque ligne présente : nom lisible, icône non porteuse d'information seule,
type, source (« Intervention #142 »), localisation calculée, date, bouton
Télécharger si autorisé et lien Ouvrir la fiche. Aucun terme technique
(`source_kind`, `entity_type`, nom de classe Python) ne doit être affiché.

La pagination est obligatoire, avec conservation des filtres dans l'URL. La
V1 est consultation/recherche uniquement : aucun upload global et aucun
bouton de suppression dans ce centre.

## M. Vue FDS `/documents/fds`

Vue filtrée ou route dédiée sur les `ProductDocument` de type FDS et
`is_current=True`. Colonnes : produit commercial, fabricant/fournisseur,
version, date de publication, statut courant, localisation des balances de
stock lorsque disponible, téléchargement et fiche produit.

Un état vide doit expliquer : « Les FDS sont ajoutées depuis la fiche du
produit commercial » et proposer ce lien si `stock.view` le permet.

Le futur « classeur FDS » pourra réutiliser exactement le même service de
sélection autorisé pour produire une liste ou un PDF ; aucune génération n'est
incluse dans le MVP.

## N. Documents et localisation

Ne pas dénormaliser les chemins bâtiment/zone/salle dans une nouvelle table.

- équipement : `Equipment → effective_room → Zone → Building` ;
- intervention : salle directe, sinon équipement lié ;
- FDS produit : `CommercialProduct → StockLot → StockLotBalance →
  StockLocation → Room/Zone/Building`, en signalant « plusieurs
  emplacements » si nécessaire ;
- contrat : relation équipement/lot/entreprise si le document historique est
  finalement inclus.

Si aucune localisation n'est calculable, afficher « Localisation non
renseignée », jamais `None` ou un identifiant interne.

## O. Sécurité

Avant développement, les contrôles suivants sont requis :

- autoriser une liste d'extensions métier limitée et contrôler aussi le MIME
  réel / signatures connues ;
- refuser exécutables, scripts et archives non prévues ;
- conserver un nom original d'affichage séparé d'un nom interne UUID ;
- résoudre le chemin avec `Path.resolve()` et vérifier qu'il reste sous la
  racine `UPLOAD_FOLDER` ;
- vérifier parent, source et permission pour chaque download/delete ;
- ne jamais exposer le volume par une route statique ;
- définir explicitement `Content-Disposition: attachment` et un nom sûr ;
- conserver la limite 50 MiB et afficher une erreur utilisateur claire ;
- supprimer le fichier si l'insertion DB échoue, ou journaliser une tâche de
  réconciliation ;
- fournir un diagnostic non destructif des lignes DB sans fichier et des
  fichiers sans ligne DB ;
- ajouter des tests IDOR avec deux utilisateurs et deux sources.

Les documents de prévention, personnel et contrats peuvent contenir des
données sensibles. Leur inclusion doit rester soumise à la permission source;
aucun filtrage global par simple URL ne suffit.

## P. Performance

| Volume | Stratégie V1 attendue |
|---:|---|
| 100 | `UNION ALL`/adaptateurs, pagination 20–50 lignes, eager loading des parents |
| 1 000 | mêmes principes, indexes FK/date/type, count limité aux lignes filtrées |
| 10 000 | mesurer les plans SQL ; envisager vue/index commun réparable si la fusion paginée devient lente |

Indexes utiles côté sources : clés étrangères parent, `uploaded_at`,
`document_type`, `is_current`, `published_at`, et éventuellement index
composé parent/date. Aucun index de contenu PDF n'est proposé.

## Q. Migrations éventuelles

### Option A recommandée

Aucune migration nécessaire. Il faut seulement ajouter un service de lecture,
des requêtes, des tests et une route protégée.

### Option B différée

Une future migration créerait un index documentaire avec `source_kind`,
`source_document_id`, `parent_type`, `parent_id`, type, dates et hash
optionnels, puis un backfill idempotent. Elle ne déplacerait ni ne copierait
les fichiers et devrait prévoir la réparation des entrées obsolètes. Elle
serait déployée séparément après validation des volumes.

## R. Compatibilité historique

La V1 lit les trois tables sans transformer les lignes existantes. Les
documents sans type reçoivent `Autre`; les descriptions nulles restent vides;
les noms UUID sont présentés avec une règle d'affichage documentée. Les
fichiers manquants apparaissent éventuellement dans une vue d'administration
diagnostique, mais ne sont jamais supprimés automatiquement.

`Contract.contract_file` doit faire l'objet d'une décision séparée : soit il
reste exclu du MVP, soit il est adapté par un lecteur temporaire avec
permission `contract.view`, soit il est converti plus tard en document source
normalisé. Il ne faut pas le mélanger silencieusement aux trois tables.

## S. Tests à prévoir avant implémentation

1. Intervention, équipement et produit présents dans la liste globale.
2. Recherche sur filename, titre, description et parent.
3. Filtres type/source/date et filtres localisation.
4. Pagination stable et conservation des paramètres d'URL.
5. FDS : uniquement documents courants et accès produit autorisé.
6. Lien vers chaque fiche métier et absence de lien si source interdite.
7. Téléchargement autorisé, refusé et IDOR entre deux utilisateurs.
8. Document DB présent mais fichier manquant : réponse contrôlée, jamais 500.
9. Fichier présent sans ligne DB : diagnostic, aucune exposition publique.
10. Parent supprimé/inaccessible : résultat masqué ou marqué orphelin selon
    politique validée.
11. Aucun upload, suppression ou modification possible depuis `/documents/`.
12. Wildcard `super_admin`, rôles source et deny individuel.
13. Compatibilité avec noms spéciaux, extensions refusées et taille maximale.
14. Non-régression des téléchargements depuis les fiches existantes.

## T. MVP et évolutions

### V1 obligatoire

- `GET /documents/` protégé ;
- agrégation lecture seule des interventions, équipements et produits ;
- recherche simple, pagination et trois filtres principaux ;
- type/source/localisation lisibles ;
- téléchargement sécurisé sans duplication ;
- lien vers le parent métier ;
- contrôle `documents.view` + permission source ;
- vue FDS courante ;
- états vides et messages d'accès compréhensibles ;
- tests de sécurité, IDOR, fichier manquant et non-régression.

### Plus tard

- index commun matérialisé si le volume le justifie ;
- tags métier ;
- recherche plein texte/OCR ;
- prévisualisation avancée ;
- versionnement transversal ;
- classeur FDS exportable ;
- intégration explicite des contrats et documents généraux ;
- diagnostic et réparation assistés des orphelins.

## Questions nécessitant validation

1. Le centre doit-il inclure les chemins historiques `Contract.contract_file`
   dès la V1, ou les laisser dans les contrats ?
2. Le catalogue RBAC accepte-t-il deux nouvelles permissions
   `documents.view` et `documents.download`, ou la visibilité doit-elle être
   dérivée uniquement des permissions sources ?
3. Le téléchargement global doit-il être séparé de la consultation, ou
   `documents.view` suffit-il pour la V1 ?
4. Quel est le type officiel des documents d'intervention et d'équipement
   actuellement sans classification ?
5. Les documents prévention/personnel/contrats doivent-ils être inclus après
   une revue de confidentialité ?
6. Faut-il afficher une localisation quand plusieurs emplacements stock sont
   possibles, et sous quel libellé ?
7. Quel seuil de volume déclenche l'étude d'un index matérialisé ?
8. La V1 reste-t-elle strictement consultation/recherche, sans document
   général non lié à un objet métier ?
