diff --git a/docs/superpowers/specs/2026-03-25-entity-versioning-design.md b/docs/superpowers/specs/2026-03-25-entity-versioning-design.md new file mode 100644 index 0000000..89cdb52 --- /dev/null +++ b/docs/superpowers/specs/2026-03-25-entity-versioning-design.md @@ -0,0 +1,303 @@ +# Entity Versioning — Design Spec + +**Date :** 2026-03-25 +**Entites concernees :** Machine, Composant, Piece, Produit +**Approche :** Extension du systeme AuditLog existant + +--- + +## Objectif + +Permettre de consulter l'historique des versions numerotees (v1, v2, v3...) des entites principales et de restaurer n'importe quelle version anterieure, afin de ne jamais perdre de donnees. + +--- + +## Regles metier + +### Creation de version +- Chaque `create` ou `update` sur une entite incremente automatiquement le compteur `version` de l'entite +- Le numero de version est enregistre dans l'AuditLog correspondant (nouvelle colonne `version`) + +### Restauration +- La restauration cree une **nouvelle version** (v+1) — on ne supprime jamais d'historique +- L'AuditLog de la restauration a `action = "restore"` et le diff contient `restoredFromVersion: N` + +### Controle de squelette (Composant, Piece, Produit uniquement) +- Avant restauration, on compare le ModelType actuel avec celui du snapshot +- **Meme squelette (ModelType)** : restore complet — champs de base + slots + custom fields +- **Squelette different** : restore partiel — uniquement les champs de base (nom, description, reference, constructeurs, prix) + +### Controle d'integrite +- Avant restauration, on verifie que toutes les entites liees dans le snapshot existent encore en base : + - **Composant** : pieces selectionnees dans les slots, produits, sous-composants, constructeurs + - **Piece** : produits selectionnes dans les slots, constructeurs + - **Produit** : constructeurs + - **Machine** : site, liens composants/pieces/produits (MachineComponentLink, MachinePieceLink, MachineProductLink) +- Les entites manquantes generent des **warnings** affiches a l'utilisateur +- Les slots avec des entites supprimees sont restaures **vides** (sans selection) + +### Machines +- Pas de controle de squelette (pas de ModelType) : restauration toujours complete +- Controle d'integrite sur le site et les liens machine + +### Permissions +- Consulter les versions : `ROLE_VIEWER` +- Restaurer une version : `ROLE_GESTIONNAIRE` et au-dessus + +--- + +## Modifications backend + +### 1. Colonne `version` sur AuditLog + +```sql +ALTER TABLE audit_logs ADD COLUMN version INT DEFAULT NULL; +``` + +Nullable car les AuditLogs existants n'ont pas de version. + +### 2. Colonne `version` sur Machine + +```sql +ALTER TABLE machine ADD COLUMN version INT NOT NULL DEFAULT 1; +``` + +Les entites Composant, Piece, Produit ont deja cette colonne. + +### 3. Enrichissement des snapshots + +Les Audit Subscribers doivent inclure dans le `snapshot` : + +**Composant :** +```json +{ + "id": "cl...", + "name": "...", + "reference": "...", + "description": "...", + "prix": 100.00, + "typeComposant": { "id": "cl...", "name": "...", "code": "..." }, + "product": { "id": "cl...", "name": "..." }, + "constructeurs": [{ "id": "cl...", "name": "..." }], + "customFieldValues": [{ "id": "cl...", "fieldName": "...", "value": "..." }], + "pieceSlots": [ + { "id": "cl...", "typePieceId": "cl...", "selectedPieceId": "cl...", "quantity": 1, "position": 0 } + ], + "subcomponentSlots": [ + { "id": "cl...", "alias": "...", "familyCode": "...", "typeComposantId": "cl...", "selectedComposantId": "cl...", "position": 0 } + ], + "productSlots": [ + { "id": "cl...", "typeProductId": "cl...", "selectedProductId": "cl...", "familyCode": "...", "position": 0 } + ], + "version": 3 +} +``` + +**Piece :** +```json +{ + "id": "cl...", + "name": "...", + "reference": "...", + "description": "...", + "prix": 50.00, + "typePiece": { "id": "cl...", "name": "...", "code": "..." }, + "product": { "id": "cl...", "name": "..." }, + "constructeurs": [{ "id": "cl...", "name": "..." }], + "customFieldValues": [{ "id": "cl...", "fieldName": "...", "value": "..." }], + "productSlots": [ + { "id": "cl...", "typeProductId": "cl...", "selectedProductId": "cl...", "familyCode": "...", "position": 0 } + ], + "version": 2 +} +``` + +**Produit :** +```json +{ + "id": "cl...", + "name": "...", + "reference": "...", + "supplierPrice": 25.00, + "typeProduct": { "id": "cl...", "name": "...", "code": "..." }, + "constructeurs": [{ "id": "cl...", "name": "..." }], + "customFieldValues": [{ "id": "cl...", "fieldName": "...", "value": "..." }], + "version": 1 +} +``` + +**Machine :** +```json +{ + "id": "cl...", + "name": "...", + "reference": "...", + "description": "...", + "site": { "id": "cl...", "name": "..." }, + "customFieldValues": [{ "id": "cl...", "fieldName": "...", "value": "..." }], + "version": 4 +} +``` + +### 4. Incrementation automatique de la version + +Dans chaque Audit Subscriber, a chaque `create`/`update` : +1. Appeler `$entity->incrementVersion()` +2. Ecrire `$auditLog->setVersion($entity->getVersion())` + +Pour Machine, ajouter la methode `incrementVersion()` et la propriete `version` a l'entite. + +### 5. Nouveaux endpoints — `EntityVersionController` + +| Methode | Route | Description | Role | +|---------|-------|-------------|------| +| GET | `/api/{entity}/{id}/versions` | Liste des versions | ROLE_VIEWER | +| GET | `/api/{entity}/{id}/versions/{version}/preview` | Preview + controles avant restore | ROLE_GESTIONNAIRE | +| POST | `/api/{entity}/{id}/versions/{version}/restore` | Execute la restauration | ROLE_GESTIONNAIRE | + +`{entity}` = `machines`, `composants`, `pieces`, `products` + +**GET versions — Response :** +```json +{ + "items": [ + { + "version": 3, + "action": "update", + "createdAt": "2026-03-25T14:30:00+00:00", + "actor": { "id": "cl...", "label": "Jean Dupont" }, + "diff": { "name": { "from": "Ancien", "to": "Nouveau" } } + } + ], + "total": 3 +} +``` + +**GET preview — Response :** +```json +{ + "version": 2, + "restoreMode": "full", + "diff": { + "name": { "current": "Nouveau", "restored": "Ancien" }, + "reference": { "current": "REF-002", "restored": "REF-001" } + }, + "warnings": [ + { + "field": "pieceSlots[0].selectedPieceId", + "message": "La piece 'Roulement XY' (cl...) n'existe plus. Le slot sera restaure vide.", + "missingEntityId": "cl...", + "missingEntityName": "Roulement XY" + } + ], + "snapshot": { } +} +``` + +`restoreMode` : `"full"` (meme squelette) ou `"partial"` (squelette different, champs de base uniquement). + +**POST restore — Response :** +```json +{ + "success": true, + "newVersion": 6, + "restoredFromVersion": 2, + "restoreMode": "full", + "warnings": [] +} +``` + +### 6. Service `EntityVersionService` + +Service centralise pour la logique de versioning : + +- `getVersions(string $entityType, string $entityId): array` — liste des versions depuis AuditLog +- `getRestorePreview(string $entityType, string $entityId, int $version): array` — controles + diff +- `restore(string $entityType, string $entityId, int $version): array` — execution du restore + +Methodes internes : +- `checkSkeletonCompatibility(object $entity, array $snapshot): string` — retourne `"full"` ou `"partial"` +- `checkIntegrity(string $entityType, array $snapshot): array` — retourne les warnings +- `applyRestore(object $entity, array $snapshot, string $mode): void` — applique les changements + +--- + +## Modifications frontend + +### 1. Composant `EntityVersionList.vue` + +Composant reutilisable affiche dans un onglet "Versions" sur les pages de detail. + +**Props :** +- `entityType: 'machines' | 'composants' | 'pieces' | 'products'` +- `entityId: string` + +**Affichage :** +- Tableau : version, date, auteur, action, diff resume +- Badge "Actuelle" sur la version la plus recente +- Bouton "Restaurer" sur chaque ligne (sauf version actuelle), visible uniquement pour ROLE_GESTIONNAIRE+ + +### 2. Composant `VersionRestoreModal.vue` + +Modal de confirmation avec preview. + +**Props :** +- `entityType`, `entityId`, `version` (cible) +- `previewData` (resultat du GET preview) + +**Affichage :** +- Indicateur de mode : "Restauration complete" ou "Restauration partielle" +- Diff visuel : champs qui changent (valeur actuelle -> valeur restauree) +- Warnings en alerte orange pour les entites manquantes +- Boutons "Confirmer la restauration" / "Annuler" + +### 3. Composable `useEntityVersions.ts` + +```typescript +interface Deps { + entityType: MaybeRef + entityId: MaybeRef +} + +export function useEntityVersions(deps: Deps) { + // fetchVersions() — GET /api/{entity}/{id}/versions + // fetchPreview(version) — GET /api/{entity}/{id}/versions/{version}/preview + // restore(version) — POST /api/{entity}/{id}/versions/{version}/restore +} +``` + +### 4. Integration dans les pages de detail + +Ajouter un onglet "Versions" dans les pages : +- `pages/machines/[id].vue` +- `pages/composants/[id].vue` +- `pages/pieces/[id].vue` +- `pages/products/[id].vue` + +L'onglet affiche `EntityVersionList` qui gere l'ouverture de `VersionRestoreModal`. + +--- + +## Migration + +Une seule migration PostgreSQL : + +```sql +-- Colonne version sur audit_logs +ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS version INT DEFAULT NULL; + +-- Colonne version sur machine +ALTER TABLE machine ADD COLUMN IF NOT EXISTS version INT NOT NULL DEFAULT 1; + +-- Index pour requetes par version +CREATE INDEX IF NOT EXISTS idx_audit_entity_version ON audit_logs (entity_type, entity_id, version); +``` + +--- + +## Ce qui ne change PAS + +- L'onglet/page d'historique existant (`EntityHistoryController`) reste inchange +- Les AuditLogs existants (sans version) continuent de fonctionner +- Le mecanisme d'audit automatique via les Subscribers reste identique, juste enrichi +- Les documents ne sont pas versionnes (hors scope)