# Rapport — v3.30.0 (familles configurables, noms pro, fiches rétractables, vrai mobile)

**Se livre aux DEUX installations, à l'identique. 1 migration additive & idempotente :
`2026_09_03_100000_create_familles_objet_table` (table `familles_objet` + amorçage). Aucune
dépendance composer, aucun asset compilé — le CSS et le JS restent statiques dans `public/`.**

> ⚠️ **Tout appel IA passe par la file** (`RunAiAnalysis`), jamais dans la requête web. Et l'IA
> payante reste réservée aux AO **étoilés** — la garde-coût du §F la resserre encore : quand le
> texte du dossier est déjà sur la fiche, plus aucune pièce ne part au modèle.

---

## Le problème

Ce lot répond aux demandes du gérant après usage réel (veille à **508 fiches**) et à un **audit au
pixel** de la prod (desktop + 375 px) :

1. **§A** — le filtre « Audiovisuel » ramenait « La maintenance des équipements et systèmes
   audiovisuels » (constaté en 1ʳᵉ ligne de la veille). Le gérant ne veut pas voir ça là — mais ne
   veut **rien rater** — et veut créer ses familles **sans repasser par un développeur** ;
2. **§B** — des libellés de menu bavards (« Veille (AO non soumissionnés) », « ⭐ AO étoile ») ;
3. **§C** — une colonne « Résultat » sans objet en veille, et une ligne de groupes
   (`fi-ta-table-head-groups-row`, **37 px** quasi vides, née du groupe « Liens ») sur toutes les
   tables AO ;
4. **§D** — des fiches d'un seul tenant (9 742 px sur /veille/73 en 375 px) et un « Analysé le »
   qui **ment** : il datait la PREMIÈRE analyse (`ai_analysis_at`, posé une seule fois — design
   v3.24) sur des fiches ré-analysées depuis ;
5. **§E** — en mobile, **une carte de veille = 637 px** : un AO par écran, triage impraticable ;
6. **§F** — « Analyser le dossier (IA) » avec pièces téléversées envoyait les fichiers au modèle
   même quand le texte du dossier était déjà sur la fiche.

## §A — Familles d'objet configurables

**La table** `familles_objet` : `nom`, `slug` (identifiant STABLE — les préréglages v3.28 et le
réglage `ao.familles_prioritaires` le référencent ; posé à la création, figé ensuite), `couleur`
(nom Filament, badge thème clair/sombre), `mots_inclus` / `mots_exclus` (json), `priorite` (plus
petit = évalué d'abord), `actif`, `sort`. Migration `Schema::hasTable` + `insertOrIgnore` sur le
slug : rejouable sans effet, et une famille retouchée par le gérant n'est jamais réécrite.

**L'amorçage** : les 8 familles historiques (mêmes slugs, mêmes mots-clés, même ordre relatif —
rien ne se reclasse tout seul) + les 4 nouvelles :

| Famille | Priorité | Mots inclus (aplatis) |
|---|---|---|
| Entretien & maintenance | **10** (prime) | entretien, maintenance, reparation, depannage, revision |
| Développement & informatique | 20 | developpement, logiciel, application, site web, plateforme, digitalisation, systeme d'information, informatique |
| Études & ingénierie | 30 | etude technique(s), suivi des travaux, topograph, amo technique, ingenierie |
| (les 8 historiques) | 40 → 110 | inchangés |
| Fournitures & équipements | 200 (ferme la marche) | acquisition, fourniture, dotation en materiel, =equipement(s) |

**`famille()` devient data-driven** (`FamillesObjetService`) : lecture des familles actives en
cache (invalidé par `FamilleObjet::saved/deleted`), évaluation par priorité croissante, matching
**borné au mot** inchangé (`mord()`), et un **mot exclu écarte l'objet de CETTE famille sans
l'écarter des suivantes** — rien n'est perdu. Résultat : « maintenance des équipements
audiovisuels » → **Entretien & maintenance**, plus jamais Audiovisuel ; l'AO reste visible sous sa
famille. Repli « Autre » inchangé ; les natures Fournitures/Travaux (v3.22.1) priment toujours
comme familles AFFICHÉES. `topograph` quitte `MARQUEURS_HORS_CIBLE` (Études & ingénierie le
revendique) ; le reste du hors-cible (BTP) tient.

**Le filet** : table absente (base pas migrée — règle du socle), requête en échec ou table vidée →
la constante historique répond, à l'identique d'avant. Libellés, couleurs de badge
(`ColonneFamille`), options des filtres multi-sélection (Veille, Clôturés), familles prioritaires
des alertes (`ReglagesAo`) et Select de l'interprétation lisent tous **la même source**. Un
préréglage v3.28 qui cite une famille disparue s'ignore en silence (testé).

**L'écran Réglages → « Familles d'objet »** (`FamilleObjetResource`, gardé `settings.manage`,
étanchéité testée par URL directe) : CRUD complet + **testeur en direct** (« Colle un objet
d'AO » → famille résolue instantanément, mêmes règles que la veille) + « Reclasser la veille ».

**Le reclassement** (`ReclasserVeille`) : la famille d'une fiche n'est JAMAIS stockée — elle se
recalcule à l'affichage —, donc enregistrer une famille reclasse déjà tout. Le job (déclenché à
chaque enregistrement, et à la demande) relit la veille entière **par paquets de 100, en tâche de
fond**, rend la répartition dans la cloche, et **n'écrit rien** : une saisie manuelle reste intacte
à l'octet près (testé).

## §B — Renommage professionnel

Partout — menu, titres, fils d'ariane, états vides, notifications, libellés d'actions — et **sans
changer une seule route** (testé : `/veille`, `/ao-etoile`, `/ao-clotures` inchangés) :

Veille (AO non soumissionnés) → **Veille du marché** · ⭐ AO étoile → **⭐ Étoilés** · AO à
préparer → **À préparer** · AO soumissionnés → **Soumissionnés** · AO clôturés → **Clôturés du
marché** · Marchés inchangé. Actions : « Écarter », « Réafficher », « Renvoyer en veille ». Le
fil d'ariane d'une fiche devient « Veille du marché › … › Afficher » (il suit le libellé pluriel).

## §C — Tables : dernières retouches

1. La colonne « Résultat » (`winner_name`) est **SUPPRIMÉE** de la Veille du marché — pas masquée :
   un AO ouvert n'a jamais de résultat. Elle reste sur Clôturés du marché et Soumissionnés ;
   « Concurrence » reste masquable (v3.29).
2. `LiensExternes::colonnes()` ne rend plus un `ColumnGroup` mais **deux colonnes à plat**
   (« Drive » / « PMMP ») : la ligne de groupes de 37 px disparaît de toutes les tables (Veille,
   Clôturés, Soumissionnés/À préparer, Étoilés, Marchés). Le test balaie les quatre vues AO et
   affirme `getColumnGroups() === []` — aucun autre groupe ne la fait renaître.

## §D — Fiches : sections rétractables + horodatage honnête

1. **Rétractables** : « Aide à la décision (GO/NO-GO) », « Projection IA — avant dépôt »,
   « Dossier de préparation », « Fiche portail » — sur les DEUX fiches (mêmes composants,
   `SectionsAo`/`SectionsDossierIa`). Ouvertes par défaut en desktop ; **« Fiche portail » repliée**
   (longue, consultée à la demande) ; « Texte intégral » garde son repli v3.29.
2. **Mobile < 3 000 px** : Filament ne différencie pas l'état replié par taille d'écran côté
   serveur → les sections secondaires portent la classe `socle-repli-mobile`, et un script statique
   (`public/js/socle-mobile.js`, injecté et versionné comme la feuille dense, réversible en
   retirant deux lignes) leur envoie l'événement `collapse-section` sous 768 px au chargement.
   Alpine fait le reste : chevrons cohérents, réouverture d'un geste. Restent ouverts : en-tête,
   Analyse IA, Aide à la décision.
3. **« Analysé le » ne ment plus** : `ai_analysis['derniere_analyse_at']` (`HorodatageAnalyse`) est
   avancé à CHAQUE passe — lecture du dossier (`DossierEtoileService`), analyse complète
   (`TenderAnalysisService`), création depuis analyse, projection (IA et déterministe). L'étiquette
   affiche la dernière ; « première analyse le … » en secondaire quand elle diffère.
   `ai_analysis_at` garde son rôle de première fois (et n'est plus écrasé par l'analyse complète).

## §E — Mobile : liste utilisable

En mode empilé (< `md`), **seules les colonnes essentielles se rendent** : Référence + badge
Famille, Objet (tronqué), Acheteur (tronqué), Date limite (compte à rebours) — plus les icônes de
triage. Estimation, Concurrence, Attributaire, Plis, Montant retenu, Écart, statuts, Drive/PMMP
passent en `visibleFrom('md')` sur les trois tables AO ; Concurrents vérifié (l'ICE devient
desktop). La feuille dense resserre les cartes (padding réduit, cellules bornées à l'écran) —
l'objectif ≤ 180 px/carte est visuel, le test affirme la configuration des colonnes. La ligne
« Trier par » mobile reste.

## §F — Garde-coût IA (« faire l'OCR sur le VPS »)

Constat préservé : la voie **étoilée** est déjà 100 % texte — l'OCR se fait sur le VPS
(pdftotext/tesseract), l'API ne reçoit que du texte. La voie qui coûtait : « Analyser le dossier
(IA) » avec pièces téléversées.

**La règle, désormais** : quand un bloc `===== DOSSIER =====` existe sur la fiche
(`VeilleImportService::dossierDesNotes()`), l'action part du texte — le run porte
`options['texte']`, `exigeDesPieces()` répond non (générique, plus seulement pour l'extraction de
veille), et `RunAiAnalysis` construit un document `kind: text` : **aucune pièce ne part au modèle**
(assertion sur le payload HTTP : uniquement des blocs `text`). Le formulaire le dit (« Le dossier
texte est déjà sur la fiche — l'analyse l'utilisera sans envoyer de fichiers. Téléverser des pièces
seulement si nécessaire. ») et l'upload ne s'affiche qu'en repli explicite (toggle), non
obligatoire. Sans texte (AO hors moissonneuse), l'upload reste la voie normale, obligatoire.

## Recette (verte)

- **Familles** : maintenance audiovisuelle → Entretien & maintenance ; mot exclu ; cache invalidé ;
  CRUD + testeur + reclassement (rien d'écrit, notification de répartition) ; migration rejouée
  sans effet, retouche du gérant préservée ; filet sans table ; étanchéité `/familles-objet`.
- **Renommages** : libellés partout, URLs intactes, menu testé.
- **Tables** : plus de « Résultat » en veille, `getColumnGroups() === []` sur les 4 vues AO.
- **Fiches** : 4 sections rétractables, « Fiche portail » repliée, marque mobile posée/absente au
  bon endroit, `derniere_analyse_at` avance passe après passe.
- **Mobile** : colonnes essentielles sans `visibleFrom`, secondaires en `md`, sur les 3 tables.
- **Garde-coût** : payload 100 % texte avec bloc DOSSIER (Http::assertSent), formulaire à repli
  explicite, upload obligatoire sans texte.
- **Non-régression** : suite complète **2 505 tests verts** (9 656 assertions, 1 skipped
  pré-existant) — parcours AO, presets v3.28, analyse complète étoilé v3.29, notifications,
  base vide RUWAD (`RefreshDatabase` part de zéro partout).

## Après déploiement

1. `php artisan migrate --force` (la mise à jour automatique le fait) : la table se crée et
   s'amorce ; vérifier **Réglages → Familles d'objet** (12 lignes).
2. Sur la veille : filtrer « Audiovisuel » — la maintenance n'y est plus ; filtrer « Entretien &
   maintenance » — elle y est.
3. En 375 px : la liste montre 4-5 cartes par écran ; une fiche s'ouvre repliée sauf en-tête,
   Analyse IA, Aide à la décision.
4. Navigateur : vider le cache n'est pas nécessaire — feuille et script sont versionnés par
   empreinte.
