feat(audit) : contexte forensique dans le journal d'activité (IP, appareil, device id) (#33)
Auto Tag Develop / tag (push) Successful in 9s
Auto Tag Develop / tag (push) Successful in 9s
## Contexte Certains comptes sont **partagés** par plusieurs personnes (ex. compte « Usine »), y compris depuis des smartphones. Le journal d'activité ne stockait que le `username` → impossible de distinguer les intervenants. Cette PR ajoute un **contexte forensique automatique** à chaque entrée du journal. ## Ce qui est ajouté (capté automatiquement, sans friction utilisateur) - **Adresse IP** de la requête - **User-Agent brut** (borné à 1024 caractères) - **Libellé appareil lisible** dérivé du User-Agent : `Type · OS · Navigateur` (ex. `Mobile · Android · Chrome`) - **Identifiant d'appareil persistant** envoyé par le front (header `X-Device-Id`, stocké en `localStorage`, borné à 64 car.) — distingue les **appareils** derrière un compte partagé ## Implémentation - `UserAgentParser` (service maison, sans dépendance) — détection ordonnée OS/navigateur, testée - 4 colonnes **nullable** sur `audit_logs` + migration réversible (pas de backfill, rétro-compatible) - Capture **centralisée** dans `AuditLogger::log()` via `RequestStack` — aucun processor modifié - Champs exposés dans l'API lecture (`AuditLogProvider` + DTO TS aligné) via `AuditLogReadRepositoryInterface` (suit le pattern existant des autres read-repos) - Front : `useDeviceId` + injection du header `X-Device-Id` dans `useApi` (sur toutes les requêtes, SSR-safe) - `framework.trusted_proxies` documenté (commenté) pour une IP correcte derrière un reverse proxy - Docs : `doc/audit-logging.md` + `CLAUDE.md` ## Hors périmètre (étapes suivantes) - **Écran du journal (`audit-logs.vue`) non modifié** — l'affichage des nouvelles colonnes fera l'objet d'une refonte séparée. Les données sont prêtes côté API. - La doc in-app (`documentation-content.ts`) n'est pas touchée : le journal est un outil caché `ROLE_SUPER_ADMIN` sans article existant ni niveau de doc super-admin. ## À noter pour le déploiement - L'IP n'est fiable derrière un reverse proxy qu'une fois `framework.trusted_proxies` activé (livré commenté). ## Tests `OK (249 tests, 533 assertions)` — sortie PHPUnit propre (aucune notice). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: #33 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #33.
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
# Contexte forensique dans le journal d'activité
|
||||
|
||||
Date : 2026-06-24
|
||||
Branche : `feature/SIRH-41-ajouter-plus-d-info-dans-le-journal-d-activite`
|
||||
|
||||
## Problème
|
||||
|
||||
Le journal d'activité (`audit_logs`) ne stocke comme « qui » que le `username`. Or
|
||||
certains comptes sont **partagés** par plusieurs personnes (ex. compte « Usine »). Sous
|
||||
un compte partagé, toutes les actions apparaissent sous le même nom → impossible de
|
||||
distinguer les intervenants en cas de litige. Les utilisateurs se connectent aussi
|
||||
depuis des **smartphones**.
|
||||
|
||||
## Objectif
|
||||
|
||||
Ajouter du **contexte forensique automatique** à chaque entrée du journal, sans rien
|
||||
demander à l'utilisateur. But : disposer d'assez d'indices techniques pour enquêter
|
||||
(IP, type d'appareil/OS/navigateur, identifiant d'appareil stable, User-Agent brut) et
|
||||
distinguer les **appareils** derrière un compte partagé.
|
||||
|
||||
Non-objectif (volontairement exclu) : identification explicite de la personne physique
|
||||
(liste de noms, PIN…). Écarté par l'utilisateur — on reste sur du signal automatique.
|
||||
|
||||
## Périmètre
|
||||
|
||||
**Inclus :**
|
||||
- Capture automatique de 4 signaux à chaque écriture d'audit.
|
||||
- 4 nouvelles colonnes nullable sur `audit_logs` + migration (avec `down()`).
|
||||
- Service `UserAgentParser` (libellé appareil lisible, sans dépendance externe).
|
||||
- Front : identifiant d'appareil persistant (`localStorage`) envoyé en header sur toutes
|
||||
les requêtes API.
|
||||
- Exposition des 4 champs dans l'API de lecture du journal (`AuditLogResource` / provider)
|
||||
pour que la future refonte d'écran les ait à disposition.
|
||||
- Config `framework.trusted_proxies` documentée (conservatrice, à activer selon l'infra).
|
||||
- Docs (`doc/audit-logging.md`, `documentation-content.ts`, `CLAUDE.md`) + tests unitaires.
|
||||
|
||||
**Exclu (étape suivante) :**
|
||||
- Refonte de l'écran `frontend/pages/audit-logs.vue` (affichage des nouvelles colonnes,
|
||||
filtre par appareil). L'utilisateur prévoit de revoir cet écran séparément. On se
|
||||
contente d'exposer les données via l'API ; aucune modif du composant Vue dans ce lot.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Capture — un seul point d'entrée
|
||||
|
||||
Toutes les écritures d'audit passent par `AuditLogger::log()` (`src/Service/AuditLogger.php`).
|
||||
On y injecte `RequestStack`. À chaque `log()`, on lit la requête courante et on renseigne
|
||||
les 4 champs sur l'entité `AuditLog` avant persistance. **Aucun processor à modifier.**
|
||||
|
||||
Extraction depuis la requête :
|
||||
- `ip_address` ← `Request::getClientIp()`
|
||||
- `user_agent` ← header `User-Agent` (brut)
|
||||
- `device_label` ← `UserAgentParser::parse(userAgent)`
|
||||
- `device_id` ← header `X-Device-Id`
|
||||
|
||||
Si aucune requête courante (ex. commande CLI / cron), les 4 champs restent `null`
|
||||
(comportement « system » déjà existant pour le username).
|
||||
|
||||
### Modèle de données — `audit_logs`
|
||||
|
||||
Ajout de 4 colonnes **nullable** (pas de backfill, l'existant reste valide) :
|
||||
|
||||
| Colonne | Type | Contenu |
|
||||
|---|---|---|
|
||||
| `ip_address` | VARCHAR(45) | IP source. 45 = longueur max IPv6 (avec mapping IPv4). |
|
||||
| `user_agent` | TEXT | User-Agent brut, stocké tel quel. |
|
||||
| `device_label` | VARCHAR(255) | Libellé lisible, ex. `Mobile · Android · Chrome`. |
|
||||
| `device_id` | VARCHAR(64) | UUID persistant fourni par le front. |
|
||||
|
||||
Migration Doctrine avec `down()` supprimant les 4 colonnes. Pas de nouvel index dans ce
|
||||
lot (le filtre par appareil étant reporté à la refonte d'écran).
|
||||
|
||||
### Service `UserAgentParser`
|
||||
|
||||
Nouveau service `src/Service/UserAgentParser.php`, maison, sans dépendance.
|
||||
`parse(?string $userAgent): ?string` → libellé court composé de :
|
||||
- **Type** : Mobile / Tablette / Ordinateur (détecté sur tokens `Mobile`, `Tablet`, `iPad`…).
|
||||
- **OS** : Android / iOS / Windows / macOS / Linux / autre.
|
||||
- **Navigateur** : Chrome / Safari / Firefox / Edge / autre (ordre de test important :
|
||||
Edge avant Chrome, Chrome avant Safari, car les UA s'imbriquent).
|
||||
|
||||
Format : `Type · OS · Navigateur` (ex. `Ordinateur · Windows · Firefox`). Retourne `null`
|
||||
si User-Agent vide. Heuristique volontairement simple et lisible ; suffisant pour
|
||||
distinguer mobile/poste et familles d'OS. (Alternative écartée : librairie
|
||||
`matomo/device-detector` — plus précise mais lourde et non nécessaire ici.)
|
||||
|
||||
### Front — identifiant d'appareil persistant
|
||||
|
||||
- Composable `frontend/composables/useDeviceId.ts` : côté client uniquement, lit
|
||||
`localStorage['sirh-device-id']` ; si absent, génère `crypto.randomUUID()` et le persiste.
|
||||
Retourne l'ID (ou `null` côté serveur en SSR).
|
||||
- `frontend/composables/useApi.ts`, fonction `request()` (point unique de construction des
|
||||
headers) : `headers.set('X-Device-Id', deviceId)` quand l'ID est disponible. Appliqué à
|
||||
toutes les méthodes (GET/POST/PUT/PATCH/DELETE).
|
||||
- Note : l'auth est par cookie JWT (`credentials: 'include'`), donc le device ID n'est pas
|
||||
lié à l'auth — `localStorage` est ici un usage non sensible, acceptable.
|
||||
- **Limite assumée** : l'ID est par navigateur/appareil, pas par personne. Sur un poste
|
||||
partagé (même navigateur), l'ID est identique pour tous → distingue les appareils, pas
|
||||
les humains. Cohérent avec l'objectif forensique.
|
||||
|
||||
### API de lecture
|
||||
|
||||
Exposer `ipAddress`, `userAgent`, `deviceLabel`, `deviceId` dans la sortie de lecture du
|
||||
journal (`src/ApiResource/AuditLogResource.php` + sérialisation dans `AuditLogProvider`),
|
||||
ainsi que dans le DTO front `frontend/services/dto/audit-log.ts`. Aucune modif du
|
||||
composant `audit-logs.vue` (refonte ultérieure). Objectif : les données sont prêtes à
|
||||
être affichées par la future refonte.
|
||||
|
||||
### Trusted proxies (IP fiable)
|
||||
|
||||
`framework.trusted_proxies` n'est pas configuré aujourd'hui. Derrière un reverse proxy
|
||||
(nginx/traefik), `getClientIp()` renvoie l'IP du proxy. Architecture de déploiement non
|
||||
confirmée → on prévoit dans `config/packages/framework.yaml` une entrée **commentée et
|
||||
documentée** (avec exemple `trusted_proxies` réseau privé / loopback + `trusted_headers`),
|
||||
à activer selon l'infra. En attendant, l'IP est stockée telle que renvoyée par Symfony.
|
||||
|
||||
## Stratégie de test
|
||||
|
||||
- `tests/.../UserAgentParserTest` : table de User-Agents réels (Chrome desktop, Safari
|
||||
iPhone, Chrome Android, Firefox, Edge, UA vide/null) → libellés attendus.
|
||||
- `tests/.../AuditLoggerTest` : avec un `RequestStack` peuplé d'une `Request` factice
|
||||
(IP, headers User-Agent + X-Device-Id), vérifier que l'`AuditLog` persisté porte bien les
|
||||
4 champs ; et qu'avec une `RequestStack` vide (contexte CLI), les 4 champs sont `null`.
|
||||
|
||||
## Documentation à mettre à jour (règles obligatoires CLAUDE.md)
|
||||
|
||||
- `doc/audit-logging.md` : section « Données stockées par entrée » + nouveaux champs +
|
||||
note sur le device ID front et le caveat trusted proxies.
|
||||
- `frontend/data/documentation-content.ts` : doc in-app (niveau admin) du journal.
|
||||
- `CLAUDE.md` : section Audit Logging — mentionner les 4 nouveaux signaux et le point de
|
||||
capture unique (`AuditLogger` + `RequestStack`).
|
||||
|
||||
## Risques / limites
|
||||
|
||||
- Device ID = par appareil, pas par humain (cf. ci-dessus).
|
||||
- IP peu utile derrière proxy tant que `trusted_proxies` n'est pas activé.
|
||||
- Plusieurs personnes dans la même usine sortent souvent sur la même IP publique et
|
||||
peuvent avoir le même modèle de téléphone → les signaux se recoupent ; ce lot fournit
|
||||
des indices, pas une preuve d'identité.
|
||||
@@ -0,0 +1,207 @@
|
||||
# Refonte de l'écran Journal d'activité (MalioDataTable + drawer de filtre)
|
||||
|
||||
Date : 2026-06-24
|
||||
Branche : `feature/SIRH-41-ajouter-plus-d-info-dans-le-journal-d-activite`
|
||||
|
||||
## Problème / objectif
|
||||
|
||||
L'écran `frontend/pages/audit-logs.vue` (journal d'activité, `ROLE_SUPER_ADMIN`) est aujourd'hui
|
||||
fait main : `<select>`/`<input>` natifs, tableau en grille CSS, lignes dépliables affichant le diff
|
||||
JSON brut, pagination « précédent/suivant » figée à 50/page. Il faut le **moderniser** :
|
||||
|
||||
1. Passer le tableau en **`MalioDataTable`** (1er usage dans SIRH).
|
||||
2. Mettre les filtres dans un **drawer**, sur le **même principe que STARSEED** (les écrans de liste
|
||||
`modules/.../pages/.../index.vue` : `MalioDrawer` + `MalioAccordion`, état brouillon/appliqué,
|
||||
footer Réinitialiser/Appliquer, badge de compteur de filtres actifs).
|
||||
3. Passer **tous** les composants de l'écran en composants **Malio** quand l'équivalent existe.
|
||||
4. Exploiter les nouvelles données forensiques (IP, appareil, User-Agent, device id) déjà captées
|
||||
par le backend.
|
||||
|
||||
## Référence de pattern
|
||||
|
||||
- STARSEED, écran canonique : `/home/m-tristan/workspace/Starseed/frontend/modules/commercial/pages/clients/index.vue`
|
||||
(drawer de filtre, `MalioAccordion`, brouillon→appliqué, `MalioDataTable`, badge compteur).
|
||||
- Adaptations SIRH : **libellés en français en dur** (convention des drawers SIRH existants —
|
||||
`employees/index.vue`, `sites.vue` — pas d'i18n comme STARSEED) ; **filtres non persistés en URL**
|
||||
(comme STARSEED et l'écran actuel).
|
||||
- Malio `@malio/layer-ui` 1.7.15 (doc `node_modules/@malio/layer-ui/COMPONENTS.md`).
|
||||
|
||||
## Périmètre
|
||||
|
||||
**Inclus :** refonte complète de `audit-logs.vue` (tableau, filtres, détail) + évolutions backend
|
||||
nécessaires (perPage + nouveaux filtres) + DTO TS + docs.
|
||||
|
||||
**Exclus :** toute autre page ; l'audit reste `ROLE_SUPER_ADMIN` ; pas de doc in-app (outil caché,
|
||||
aucun article existant — décision déjà prise au lot précédent).
|
||||
|
||||
---
|
||||
|
||||
## A. Tableau — `MalioDataTable`
|
||||
|
||||
API (1.7.15) : `:columns` (`{key,label}[]`), `:items`, `:total-items`, `v-model:page`,
|
||||
`v-model:per-page`, `:per-page-options`, `row-clickable`, événements `row-click` /
|
||||
`update:page` / `update:per-page`, slots `#cell-{key}` et `#empty`.
|
||||
|
||||
Colonnes :
|
||||
|
||||
| key | label | rendu |
|
||||
|---|---|---|
|
||||
| `createdAt` | Date action | `JJ/MM/AAAA HH:MM` (déjà formaté par le provider) |
|
||||
| `username` | Utilisateur | texte brut |
|
||||
| `action` | Action | badge couleur via `#cell-action` (create=vert, update=bleu, delete=rouge, validate=violet, site_validate=indigo, défaut=neutre) |
|
||||
| `entityType` | Type | libellé FR via `#cell-entityType` (work_hour→Heures, absence→Absence, employee→Employé, contract_suspension→Suspension, rtt_payment→RTT, fractioned_days→Fract., paid_leave_days→Congés payés, week_comment→Commentaire) |
|
||||
| `employeeName` | Employé | nom ou `—` |
|
||||
| `deviceLabel` | Appareil | `deviceLabel` ou `—` |
|
||||
| `description` | Description | tronqué (`truncate` + `title`) via `#cell-description` |
|
||||
|
||||
- `:per-page-options="[25, 50, 100]"`, `perPage` par défaut 50.
|
||||
- `@row-click` → ouvre le drawer de détail avec la ligne cliquée.
|
||||
- `:items` = directement les `AuditLog` de la page courante (le DTO porte déjà toutes les clés ;
|
||||
les `key` de colonnes correspondent aux champs).
|
||||
|
||||
## B. Drawer de détail (clic ligne)
|
||||
|
||||
`MalioDrawer` (droite, `drawer-class="max-w-xl"`), titre `#header` = « Détail de l'action ».
|
||||
Contenu (lecture seule, sections) :
|
||||
|
||||
- **Méta** : Utilisateur, Employé, Date action, Date affectée, Action (badge), Type (libellé).
|
||||
- **Contexte technique** : IP (`ipAddress`), Appareil (`deviceLabel`), User-Agent brut
|
||||
(`userAgent`, en `break-all`/petite police), Device id (`deviceId`). Champs nuls → `—`.
|
||||
- **Changements** : si `changes` non nul, rendu lisible — pour chaque clé présente dans
|
||||
`old`/`new`, une ligne `clé : ancienne → nouvelle` (au lieu du double bloc JSON brut actuel).
|
||||
Helper front `formatChanges(changes)` qui fusionne les clés de `old` et `new`. Si `changes` nul →
|
||||
« Aucun détail de modification ».
|
||||
|
||||
État : `selectedLog: AuditLog | null` + `detailOpen: boolean`. Fermeture standard MalioDrawer.
|
||||
|
||||
## C. Drawer de filtre (principe STARSEED)
|
||||
|
||||
Bouton **« Filtrer »** (`MalioButton variant="tertiary" icon-name="mdi:tune"`) dans la barre de titre ;
|
||||
son label porte le **compteur de filtres actifs** (`Filtrer (N)` si N>0).
|
||||
|
||||
`MalioDrawer` (`drawer-class="max-w-[450px]"`, `body-class="p-0"`,
|
||||
`footer-class="justify-between border-t border-black p-6"`), titre `#header` = « Filtres ».
|
||||
Corps en `MalioAccordion` (un `MalioAccordionItem` par section) :
|
||||
|
||||
| Section | Composant | Champ filtre |
|
||||
|---|---|---|
|
||||
| Période | `MalioDateRange` (`v-model` = `{start,end}` ISO) | `from`/`to` sur `affectedDate` (sémantique actuelle conservée) |
|
||||
| Employé | `MalioSelect` (options = employés chargés au mount) | `employeeId` (valeur unique) |
|
||||
| Type d'entité | liste de `MalioCheckbox` (multi) | `entityType[]` |
|
||||
| Action | liste de `MalioCheckbox` (multi) | `action[]` |
|
||||
| Utilisateur / compte | `MalioInputText` (`icon mdi:magnify`) | `username` (ILIKE partiel) |
|
||||
| IP | `MalioInputText` | `ip` (ILIKE partiel) |
|
||||
| Appareil | `MalioInputText` | `device` (ILIKE partiel sur `device_label` OU `device_id`) |
|
||||
|
||||
Footer : `MalioButton variant="tertiary"` **Réinitialiser** (gauche) + `MalioButton variant="primary"`
|
||||
**Appliquer** (droite).
|
||||
|
||||
**État brouillon → appliqué** (pattern STARSEED) :
|
||||
- `draft*` refs (éditées dans le drawer) et `applied*` refs (pilotent le fetch).
|
||||
- `openFilters()` : copie `applied*` → `draft*` puis ouvre.
|
||||
- `applyFilters()` : copie `draft*` → `applied*`, remet `page=1`, refetch, ferme le drawer.
|
||||
- `resetFilters()` : vide `draft*` **et** `applied*`, remet `page=1`, refetch, **laisse le drawer ouvert**.
|
||||
- `activeFilterCount` (computed sur `applied*`) → badge bouton.
|
||||
- Helpers `toggle(arrayRef, value, selected)` pour les multi-select.
|
||||
- Options Type d'entité / Action = listes statiques (mêmes codes que le provider) ; options Employé
|
||||
chargées une fois au `onMounted` (réutiliser le chargement employés déjà fait par l'écran actuel).
|
||||
|
||||
## D. Composable `useAuditLogsList`
|
||||
|
||||
Composable **spécifique à l'écran** (`frontend/composables/useAuditLogsList.ts`) — pas de
|
||||
`usePaginatedList` générique (un seul consommateur → YAGNI). Expose :
|
||||
|
||||
- état : `items`, `total`, `page`, `perPage`, `loading`, les `draft*`/`applied*`, `activeFilterCount`,
|
||||
`employeeOptions`.
|
||||
- actions : `load()` (fetch avec filtres appliqués + page/perPage), `goToPage(n)`, `setPerPage(n)`,
|
||||
`openFilters()`, `applyFilters()`, `resetFilters()`, `loadEmployeeOptions()`.
|
||||
- `load()` doit ignorer les réponses périmées (garde anti-race : compteur de requête, on jette
|
||||
les réponses dont l'index n'est pas le dernier émis).
|
||||
|
||||
La page `audit-logs.vue` se réduit à : barre de titre (titre + bouton Filtrer), `MalioDataTable`,
|
||||
drawer filtre, drawer détail — toute la logique vit dans le composable.
|
||||
|
||||
## E. Backend
|
||||
|
||||
### `frontend/services/dto/audit-log.ts` (`AuditLogFilters`)
|
||||
Étendre :
|
||||
```ts
|
||||
export type AuditLogFilters = {
|
||||
employeeId?: number
|
||||
from?: string
|
||||
to?: string
|
||||
entityType?: string[]
|
||||
action?: string[]
|
||||
username?: string
|
||||
ip?: string
|
||||
device?: string
|
||||
page?: number
|
||||
perPage?: number
|
||||
}
|
||||
```
|
||||
`fetchAuditLogs` sérialise les tableaux en `entityType[]`/`action[]` (syntaxe PHP) et n'inclut que
|
||||
les filtres non vides.
|
||||
|
||||
### `src/ApiResource/AuditLogResource.php`
|
||||
Ajouter les `QueryParameter` : `perPage`, `username`, `ip`, `device`, `action` (`entityType` existe
|
||||
déjà). (Les `QueryParameter` sont surtout documentaires : le provider lit `$request->query`.)
|
||||
|
||||
### `src/State/AuditLogProvider.php`
|
||||
- Lire `perPage` (défaut 50, clampé à un ensemble autorisé `[25,50,100]`, fallback 50 ; borne dure).
|
||||
- Lire `username`, `ip`, `device` (chaînes, `null` si vide).
|
||||
- Lire `entityType` et `action` en **tableaux** (`$request->query->all('entityType')` /
|
||||
`->all('action')`), `null`/`[]` si absent. Conserver la rétro-compat : si `entityType` arrive en
|
||||
scalaire, le normaliser en tableau à un élément.
|
||||
- Passer le tout au repository ; `perPage` remplace la constante `PER_PAGE`. La réponse renvoie
|
||||
`perPage` réel.
|
||||
|
||||
### `src/Repository/Contract/AuditLogReadRepositoryInterface.php` + `AuditLogRepository.php`
|
||||
Faire évoluer `findByFilters` / `countByFilters` :
|
||||
```php
|
||||
findByFilters(
|
||||
?int $employeeId,
|
||||
?DateTimeImmutable $from,
|
||||
?DateTimeImmutable $to,
|
||||
?array $entityTypes, // list<string>|null
|
||||
?array $actions, // list<string>|null
|
||||
?string $username,
|
||||
?string $ip,
|
||||
?string $device,
|
||||
int $limit,
|
||||
int $offset,
|
||||
): array
|
||||
countByFilters(... mêmes filtres ...): int
|
||||
```
|
||||
Clauses : `employeeId` =, dates BETWEEN sur `affectedDate` (inchangé), `entityTypes`/`actions`
|
||||
`IN (:...)` si non vides, `username`/`ip` `ILIKE %v%` (paramètre échappé), `device` →
|
||||
`(device_label ILIKE :d OR device_id ILIKE :d)`. Tri inchangé (`createdAt DESC`).
|
||||
Mutualiser la construction des critères entre les deux méthodes (méthode privée
|
||||
`applyFilters(QueryBuilder, ...)`) pour rester DRY.
|
||||
|
||||
## Tests
|
||||
|
||||
- Backend : `AuditLogProviderTest` étendu — vérifier que `perPage`, `username`, `ip`, `device`,
|
||||
`entityType[]`, `action[]` sont lus et transmis au repository (repo stubbé, on asserte les
|
||||
arguments via un spy), et que `perPage` hors liste retombe sur 50.
|
||||
- Backend : test repository des nouvelles clauses si un test repository existe ; sinon couvrir via le
|
||||
provider (le repo réel n'est pas unit-testé aujourd'hui — ne pas introduire d'intégration DB).
|
||||
- Front : pas de test auto (convention SIRH, pas de build) — revue de diff. Le composable
|
||||
`useAuditLogsList` reste pur/réactif et testable manuellement.
|
||||
|
||||
## Documentation
|
||||
|
||||
- `doc/audit-logging.md` : section « Filtres disponibles » mise à jour (employé, période, type[],
|
||||
action[], utilisateur, IP, appareil ; pagination perPage) + mention du drawer et du drawer de
|
||||
détail.
|
||||
- `CLAUDE.md` : compléter la puce « Contexte forensique » / journal pour noter l'écran refondu
|
||||
(`MalioDataTable`, drawer de filtre façon STARSEED, drawer de détail, filtres back
|
||||
username/ip/device/action[]/entityType[]/perPage).
|
||||
|
||||
## Risques / notes
|
||||
|
||||
- 1er `MalioDataTable` de SIRH : valider le rendu (le composant gère sa propre pagination/markup ;
|
||||
ne pas réappliquer le gabarit grille maison du CLAUDE.md à ce tableau).
|
||||
- `MalioDateRange` filtre `affectedDate` (cohérent avec l'existant) ; ne pas confondre avec
|
||||
`createdAt` (date d'action affichée en colonne).
|
||||
- Évolution de signature de `AuditLogReadRepositoryInterface` : mettre à jour l'implémentation et le
|
||||
provider dans le même lot (ils sont les seuls consommateurs).
|
||||
Reference in New Issue
Block a user