C4 et K8 livrés, et le travail DOCS de la session manager-service
Ce commit porte deux sessions à la fois — voir la note de collision en fin de message. C4 — compression des images à l'upload, flutter build web vert. Un seul helper appelé par les deux chemins d'upload de resources_screen, le motif exact qui a fait diverger Create et Upload côté serveur en C1 puis en C3. La compression se fait avant resourceCreate parce que le pré-vol de quota de C3 porte sur sizeBytes à la création. Écart assumé à l'énoncé « JPEG q82 » : un PNG à canal alpha reste un PNG, sinon la transparence se remplit de noir. Défaut préexistant trouvé au passage : le second chemin d'upload ne renseignait sizeBytes ni avant ni après, donc tout ce qui est passé par là compte 0 octet au quota. K8 — retour à l'accueil après 5 min, flutter build apk vert. Écrit une seule fois dans section_page_detail, qui enveloppe les treize types. Le retour passe par le même dispose, donc il émet un sectionLeave avec sa durée réelle : K8 répare une statistique en plus d'un écran. Checklist D0 ajoutée — DOCS/claude design/checklist-d0-offline.html. Les 15 cas du §21, chaque groupe relié au bug qu'il décide : 21.2 tranche D2 et D4, 21.1.4 tranche D3, 21.4.1 tranche D5. Les cas 21.1.2/3/5/6 sont la première vérification RÉELLE de D1, corrigé le 11/08 mais validé par l'analyse seule. Si tout passe, le lot D se ferme et le plan perd un à deux jours — c'est le lien L11. COLLISION SUR DOCS, à retenir. La session manager-service avait des modifications stagées et non commitées sur STATUS.md, kanban.html et v1-plan.md, alors que son prompt lui demandait de ne pas toucher à DOCS/. Rien n'est perdu : ses changements et les miens coexistent, et ils sont tous dans ce commit — deux cartes ferées de son côté (tests du RAG, dettes serveur audit & plafond users) plus ses done-items. Ce que ça a produit : mes compteurs de kanban étaient calculés par arithmétique à partir d'un état déjà périmé, donc faux de trois cartes. Ils sont désormais recalés sur la MESURE — on compte les <article> et on écrit le résultat, on ne raisonne plus par delta. Planifié 19, six colonnes cohérentes, 44 done-items, bandeau et libellé alignés. Leçon pour la suite : un compteur dérivé d'un delta est faux dès qu'une autre main touche le fichier. Mesurer, toujours. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
3fe18e088b
commit
396debb4ff
38
STATUS.md
38
STATUS.md
@ -412,10 +412,42 @@ Remplacé par `GetReferencedResourceIds()`, déjà implémentée sur les 13 sous
|
||||
- **Compteur utilisateurs** : « X / 5 » et bouton d'ajout désactivé au plafond. **Le SuperAdmin en est exclu** — son `GET /api/User` renvoie toutes les instances, compter cette liste contre un plafond *par instance* n'aurait aucun sens. Le point « masquer le rôle SuperAdmin à un InstanceAdmin », resté en question dans `todo-features.md`, **était déjà fait** (`_allowedRoles` filtre sur `r >= callerRole`) : vérifié, pas réimplémenté.
|
||||
- **Trois pièges relevés en câblant, à ne pas re-découvrir** : `AuditController` renvoie les entités **brutes**, pas un DTO (champs d'`AuditLog` en camelCase) ; `invokeAPI` **ne lève pas** sur un code d'erreur et son résultat était ignoré dans `users_screen`, donc un e-mail déjà pris (409) ne produisait aucun message — corrigé, ce qui rendra le futur 422 du plafond visible sans retouche ; et `DropdownButtonFormField` **ne relit pas `initialValue`** sur reconstruction (`FormField.didUpdateWidget` ne traite que `forceErrorText`), donc « Réinitialiser les filtres » vidait la requête sans vider l'affichage — remplacé par un `DropdownButton` piloté.
|
||||
|
||||
⚠️ **Deux dettes backend ouvertes par ce chantier, laissées ouvertes à dessein** (le périmètre était `manager-app` seul) :
|
||||
✅ **Les deux dettes backend qu'il avait ouvertes sont fermées le 2026-08-12** — voir le volet `manager-service` ci-dessous.
|
||||
|
||||
1. **Le plafond de 5 n'existe pas côté serveur.** `UserController.CreateUser` ne compte rien, pas de 422, et `SubscriptionPlan` ne porte aucun champ utilisateur. Ce qui est livré est un **garde-fou d'interface** : un POST direct sur l'API passe toujours. S'il devait varier par plan, c'est une colonne — donc un changement de schéma **après** le gel du lot B.
|
||||
2. **Aucune section n'est jamais journalisée.** `AuditedTypes.Contains(entry.Entity.GetType())` (`MyInfoMateDbContext.cs:126`) exige l'égalité **exacte** de type, or `Section` est **abstraite** (`Section.cs:17`) : le type runtime est toujours `SectionMap`, `SectionQuiz`… La ligne `typeof(Section)` d'`AuditedTypes` ne matche donc rien. Le journal couvre `Resource`, `Configuration`, `Device`, `User`, `Instance` — mais **pas le contenu**, précisément ce que l'usage cible voulait tracer. Correctif : `Any(t => t.IsInstanceOfType(entry.Entity))`. ⚠️ `EntityType` portera alors `SectionMap` et non `Section` : soit normaliser côté serveur, soit élargir la liste du filtre front aux 13 sous-types. Le filtre « Section » est **déjà dans l'écran** et ne rend aucune ligne d'ici là.
|
||||
**Lot F — le volet `manager-service` est livré le 2026-08-12 : les quatre chantiers de dette backend.** `dotnet test` **163 → 200**, aucun test sauté. Quatre commits, `manager-service` seul.
|
||||
|
||||
**1. Le journal d'audit voyait tout sauf le contenu.** `AuditedTypes.Contains(entry.Entity.GetType())` exigeait l'égalité **exacte** de type, or `Section` est **abstraite** : le type runtime est toujours `SectionMap`, `SectionQuiz`… Aucune des 13 sortes de section n'était journalisée — soit précisément ce que l'écran du 12/08 devait tracer. `Resource`, `Configuration`, `Device`, `User` et `Instance` passaient, eux : ils sont concrets, et **c'est ce qui rendait le trou invisible**. Remplacé par une remontée à la classe de base auditée (`IsInstanceOfType`).
|
||||
|
||||
> **Tranché : normalisation côté serveur.** `EntityType` porte `Section`, pas `SectionMap`. Trois raisons : le filtre « Section » **déjà présent** dans l'écran se met à rendre des lignes sans toucher `manager-app`, donc sans coordonner deux repos ; élargir le filtre front aurait coûté 13 entrées de liste et 39 clés i18n **et** laissé tout futur sous-type sortir du filtre en silence — la classe de panne exacte qu'on ferme ici ; et le sous-type concret **n'est pas perdu**, le discriminateur TPH est une propriété du modèle, donc sérialisée dans `NewValues` (`"Discriminator": "Article"`). Un test le vérifie — ce n'était pas une supposition.
|
||||
|
||||
8 tests, dont un **par réflexion** qui affirme que les 13 sous-types concrets résolvent vers `Section` : la liste de types d'origine n'obligeait personne à la suivre, c'est ce qui l'a laissée devenir fausse.
|
||||
|
||||
**2. Le plafond de 5 utilisateurs existe enfin côté serveur.** `CreateUser` rend **422**. Deux décisions, écrites dans le code : **5 en dur** — le faire varier par plan serait une colonne sur `SubscriptionPlan`, donc une migration après le gel du lot B, **dette V1 assumée** ; et **SuperAdmin non soumis au plafond**, seule porte de service tant qu'aucun champ ne permet de relever la limite d'un client, ce qui s'aligne sur le front qui ne lui montre déjà pas le compteur. L'inscription self-service n'est pas concernée (premier utilisateur d'une instance neuve). Rien à retoucher côté front : le résultat d'`invokeAPI` est déjà lu depuis le correctif du 409.
|
||||
|
||||
**3. `GetSummary` agrège en SQL** au lieu de charger 13 mois en mémoire. Les six agrégats qui se lisent dans le JSON de `Metadata` ne remontent plus que **deux colonnes**, et seulement pour leur type d'événement. Effet de bord voulu : les stats avancées ne sont plus calculées puis effacées pour les plans qui n'y ont pas droit. Un changement de sémantique délibéré : « la langue de la session » était prise sur le premier événement rendu par la base, **dans un ordre indéfini** — c'est désormais le plus ancien horodatage.
|
||||
|
||||
⚠️ **La branche « stats avancées » n'était couverte par aucun test** : les cas existants ne seedent pas d'`Instance`, donc `hasAdvancedStats` était toujours faux et la moitié de la méthode n'était jamais exécutée. 11 tests ajoutés, **plus 4 contre un vrai Postgres** — le provider InMemory évalue tout côté client, il ne dit rien de la traduction SQL et une requête intraduisible y passerait au vert.
|
||||
|
||||
**4. Le vector store est éprouvé à deux instances, sur un vrai Postgres.** 14 tests Testcontainers sur l'image de `Deployment/Dockerfile.postgres`. Ils se sautent proprement (`SkippableFact`) sans démon Docker — vérifié : **186 passés / 14 sautés / 0 échec**. Établi : pgvector **0.8.6** (donc `SET hnsw.iterative_scan`, posé à chaque recherche par `SearchAsync`, est accepté — sur une version antérieure **toute recherche échouait en production**) ; extensions `vector` et `postgis` bien créées par les migrations ; et à deux instances déséquilibrées 30 contre 1, `SearchAsync` rend le bon nombre de résultats, tous de la bonne instance, aucune fuite.
|
||||
|
||||
> ⚠️ **Deux résultats contraires à ce que le plan supposait — à ne pas re-supposer à l'envers.**
|
||||
>
|
||||
> - **Ce qui protège du post-filtrage n'est pas le parcours itératif, c'est l'index sur `(InstanceId, ContentType)`.** Le planificateur filtre par instance d'abord et trie exactement : l'index HNSW n'est jamais touché, donc il n'y a rien à post-filtrer. Vérifié à 620 lignes (test) et à **22 000** hors suite, avec 2 000 lignes pour l'instance cible. Un test fige ce plan — si cet index disparaissait, la recherche se dégraderait **sans qu'aucune erreur ne le dise**.
|
||||
> - **Et cet index retiré, `hnsw.iterative_scan = relaxed_order` ne rattrape rien** : le parcours s'épuise après ~335 lignes (`Rows Removed by Filter` à l'`EXPLAIN ANALYZE`) sans atteindre l'instance minoritaire, et rend zéro. Le même jeu de données avec l'index HNSW construit **après** l'insertion rend bien ses 20 lignes : c'est la **connectivité du graphe** qui décide, et nos migrations créent l'index sur une table vide. Le SET est correct et sans coût, on le garde — mais il ne couvre pas ce qu'on croyait.
|
||||
|
||||
**Outillage, pour ne pas le redécouvrir** : `Testcontainers.PostgreSql` est épinglé en **3.10.0** — la 4.x parle l'API Docker 1.44 et l'engine local plafonne à 1.43. Et l'image est construite par le **CLI docker**, pas par le constructeur d'images de Testcontainers : celui-ci relit le `FROM` pour pré-tirer l'image de base et ne sait pas parser `tag@sha256:`. Ce digest protège la base d'un changement de glibc sous ses index — il ne se retire pas pour arranger un test.
|
||||
|
||||
**5. Le journal ne suit plus les écritures de la machine — régression du point 1, corrigée le même jour.** `WeatherSyncService` écrit `section.WeatherResult` sur cron, à **6 h et 13 h**. Le job était bénin tant que les sections n'étaient pas auditées ; depuis, chaque rafraîchissement produisait une ligne portant la **prévision OpenWeather complète en avant ET en après** — quelques dizaines de Ko, deux fois par jour, par section météo, indéfiniment. Le stockage est le moindre problème : **ce bruit noie les modifications humaines** que l'écran d'audit existe pour montrer.
|
||||
|
||||
Correctif : une liste de colonnes machine (`WeatherResult`, `WeatherUpdatedDate`, `DateUpdate`) exclues du journal, et **aucune ligne produite** quand une modification ne touche qu'elles — plutôt qu'une ligne au diff vide, qui aurait déplacé le bruit sans le retirer. `DateUpdate` y est pour une raison distincte de la météo : estampillé à chaque `SaveChanges`, il figurait dans tous les diffs sans jamais rien y apprendre. **Le signal était déjà là** — un test écrit le matin même devait l'écarter à la main (`newValues.Keys.Where(k => k != "DateUpdate")`) pour rester lisible. Quand un test doit filtrer une donnée pour être lisible, la donnée n'a rien à y faire. Vérifié au passage : `AgendaSyncService` écrit des `EventAgenda`, non audités, et ne touche pas la ligne `Section` — lui n'est pas concerné.
|
||||
|
||||
> ⚠️ **Comment il a été trouvé, parce que la méthode compte plus que le bug** : en cherchant à justifier un index sur `Timestamp` que j'avais signalé par réflexe. La question « pourquoi un index ? » a montré que ce qui faisait grossir la table n'était pas ce que je croyais.
|
||||
|
||||
⚠️ **Deux dettes nouvelles, et une fausse alerte retirée** (détail dans [v1-plan.md lot F](v1-plan.md)) :
|
||||
|
||||
1. **`AuditLog` n'a aucune purge.** `VisitEvent` purge à 13 mois, `VisitorQuestion` à 90 jours, `AuditLog` à jamais. **C'est un sujet RGPD et rien d'autre** — la table porte `UserId`, et les valeurs avant-après d'une modification de `User` contiennent e-mail, prénom et nom. Ce n'est **pas** un sujet de volume, le flot machine étant coupé. **Donc à traiter au lot J.** Recommandation : **12 mois, uniforme**, avec le même verrou que les deux purges existantes — inerte tant qu'`Audit:RetentionDays` n'est pas défini, **le pg_dump quotidien n'étant toujours pas en place** : supprimer des lignes d'audit sans restauration fine est la pire combinaison.
|
||||
2. **La bascule écrira ~2 700 lignes d'audit dans la transaction globale** de `MigrationController` (2 374 ressources + 307 sections + le reste), chacune avec un instantané JSON complet. Pas fatal, mais c'est neuf pour les sections et c'est la transaction qui ne doit pas échouer. À surveiller au prochain dry run.
|
||||
3. ⛔ **`AuditLog` sans index sur `Timestamp` — fausse alerte, retirée.** Signalée par réflexe sur « la table va grossir » ; elle allait grossir à cause de la météo. Le flot machine coupé, il reste les éditions humaines — de l'ordre de quelques dizaines de milliers de lignes par an sur 4 clients, qu'un `ORDER BY Timestamp DESC LIMIT 50` trie en millisecondes sans index. **Pas d'index, pas de migration, gel du lot B intact.**
|
||||
|
||||
**Lot C2 et C3 — livrés le 2026-08-12. Le lot médias est terminé côté serveur.** `dotnet build` vert, `dotnet test` **163/163** (148 au départ, +7 pour C2, +8 pour C3).
|
||||
|
||||
|
||||
467
claude design/checklist-d0-offline.html
Normal file
467
claude design/checklist-d0-offline.html
Normal file
@ -0,0 +1,467 @@
|
||||
<title>D0 — Visite hors ligne, checklist terrain</title>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--paper: #FBFAF7;
|
||||
--card: #FFFFFF;
|
||||
--ink: #1B1D1A;
|
||||
--ink-2: #4E534C;
|
||||
--ink-3: #868C83;
|
||||
--line: #DFDDD4;
|
||||
--line-soft: #ECEAE2;
|
||||
--accent: #3F5D3B;
|
||||
--accent-soft:#E4EBE1;
|
||||
--alert: #A4442A;
|
||||
--alert-soft:#F6E6E0;
|
||||
--ok: #2F6B4F;
|
||||
|
||||
--display: ui-serif, "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
|
||||
--body: ui-sans-serif, "Segoe UI", system-ui, -apple-system, sans-serif;
|
||||
--mono: ui-monospace, "Cascadia Mono", Consolas, "SF Mono", monospace;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) {
|
||||
--paper: #141613;
|
||||
--card: #1C1F1B;
|
||||
--ink: #E9EBE5;
|
||||
--ink-2: #AFB5AA;
|
||||
--ink-3: #7B8177;
|
||||
--line: #2E322C;
|
||||
--line-soft: #262A24;
|
||||
--accent: #9CBE93;
|
||||
--accent-soft:#232C21;
|
||||
--alert: #E39075;
|
||||
--alert-soft:#2C1F19;
|
||||
--ok: #7FC0A0;
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] {
|
||||
--paper: #141613;
|
||||
--card: #1C1F1B;
|
||||
--ink: #E9EBE5;
|
||||
--ink-2: #AFB5AA;
|
||||
--ink-3: #7B8177;
|
||||
--line: #2E322C;
|
||||
--line-soft: #262A24;
|
||||
--accent: #9CBE93;
|
||||
--accent-soft:#232C21;
|
||||
--alert: #E39075;
|
||||
--alert-soft:#2C1F19;
|
||||
--ok: #7FC0A0;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
font-family: var(--body);
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
padding: 2.5rem 1rem 5rem;
|
||||
}
|
||||
|
||||
.page { max-width: 56rem; margin: 0 auto; display: flex; flex-direction: column; gap: 3rem; }
|
||||
|
||||
header { display: flex; flex-direction: column; gap: 0.7rem; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--mono); font-size: 0.7rem;
|
||||
letter-spacing: 0.14em; text-transform: uppercase; color: var(--ink-3);
|
||||
display: flex; flex-wrap: wrap; gap: 0.5rem 1.2rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--display);
|
||||
font-size: clamp(1.9rem, 4.6vw, 2.8rem);
|
||||
line-height: 1.1; font-weight: 600; margin: 0;
|
||||
letter-spacing: -0.012em; text-wrap: balance;
|
||||
}
|
||||
|
||||
.standfirst { font-size: 1.08rem; color: var(--ink-2); margin: 0; max-width: 60ch; text-wrap: pretty; }
|
||||
|
||||
h2 {
|
||||
font-family: var(--display);
|
||||
font-size: 1.45rem; font-weight: 600; margin: 0;
|
||||
line-height: 1.2; letter-spacing: -0.008em;
|
||||
}
|
||||
|
||||
section { display: flex; flex-direction: column; gap: 1.2rem; }
|
||||
|
||||
p { margin: 0; max-width: 68ch; text-wrap: pretty; }
|
||||
.lede { color: var(--ink-2); }
|
||||
|
||||
code {
|
||||
font-family: var(--mono); font-size: 0.86em;
|
||||
background: var(--line-soft); padding: 0.1em 0.35em; border-radius: 3px;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family: var(--mono); font-size: 0.8rem; line-height: 1.6;
|
||||
background: var(--card); border: 1px solid var(--line);
|
||||
border-radius: 6px; padding: 1rem; margin: 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* ── Cas de test ────────────────────────────────── */
|
||||
|
||||
.group { display: flex; flex-direction: column; gap: 0.65rem; }
|
||||
|
||||
.group-head {
|
||||
display: flex; align-items: baseline; gap: 0.7rem;
|
||||
border-bottom: 2px solid var(--line); padding-bottom: 0.5rem;
|
||||
}
|
||||
.group-head h3 { font-size: 1.05rem; margin: 0; font-weight: 650; }
|
||||
.group-head .verdict {
|
||||
font-family: var(--mono); font-size: 0.7rem;
|
||||
letter-spacing: 0.06em; text-transform: uppercase;
|
||||
color: var(--ink-3); margin-left: auto; text-align: right;
|
||||
}
|
||||
|
||||
.case {
|
||||
display: grid;
|
||||
grid-template-columns: 3.6rem 1fr 1.6rem;
|
||||
gap: 0.9rem;
|
||||
align-items: start;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
padding: 0.85rem 0.95rem;
|
||||
}
|
||||
|
||||
.case .id {
|
||||
font-family: var(--mono); font-size: 0.78rem; font-weight: 700;
|
||||
color: var(--accent); padding-top: 0.15rem;
|
||||
}
|
||||
.case .what { display: flex; flex-direction: column; gap: 0.3rem; }
|
||||
.case .act { font-weight: 600; font-size: 0.97rem; }
|
||||
.case .exp { font-size: 0.9rem; color: var(--ink-2); }
|
||||
.case .exp b { color: var(--ink); font-weight: 650; }
|
||||
|
||||
.box {
|
||||
width: 1.5rem; height: 1.5rem;
|
||||
border: 2px solid var(--line);
|
||||
border-radius: 4px;
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
font-family: var(--mono); font-size: 0.68rem;
|
||||
letter-spacing: 0.04em;
|
||||
padding: 0.12rem 0.4rem; border-radius: 3px;
|
||||
background: var(--accent-soft); color: var(--accent);
|
||||
font-weight: 700;
|
||||
}
|
||||
.tag.alert { background: var(--alert-soft); color: var(--alert); }
|
||||
|
||||
/* ── Verdicts ───────────────────────────────────── */
|
||||
|
||||
.verdicts { display: flex; flex-direction: column; gap: 0; border-top: 1px solid var(--line); }
|
||||
.verdict-row {
|
||||
display: grid; grid-template-columns: 5rem 1fr;
|
||||
gap: 1.2rem; padding: 0.95rem 0;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.verdict-row .bug {
|
||||
font-family: var(--mono); font-size: 0.8rem; font-weight: 700; color: var(--alert);
|
||||
}
|
||||
.verdict-row p { font-size: 0.93rem; color: var(--ink-2); }
|
||||
.verdict-row b { color: var(--ink); }
|
||||
|
||||
.callout {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--line);
|
||||
border-left: 3px solid var(--accent);
|
||||
border-radius: 0 6px 6px 0;
|
||||
padding: 1rem 1.1rem;
|
||||
display: flex; flex-direction: column; gap: 0.5rem;
|
||||
}
|
||||
.callout h3 { margin: 0; font-size: 0.95rem; }
|
||||
.callout p { font-size: 0.92rem; color: var(--ink-2); }
|
||||
|
||||
footer {
|
||||
font-family: var(--mono); font-size: 0.72rem; color: var(--ink-3);
|
||||
border-top: 1px solid var(--line); padding-top: 1rem;
|
||||
}
|
||||
|
||||
@media print {
|
||||
body { background: #fff; padding: 1rem; font-size: 11pt; }
|
||||
.case, pre, .callout { break-inside: avoid; }
|
||||
}
|
||||
|
||||
@media (max-width: 34rem) {
|
||||
.case { grid-template-columns: 3rem 1fr 1.4rem; gap: 0.6rem; }
|
||||
.verdict-row { grid-template-columns: 1fr; gap: 0.3rem; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="page">
|
||||
|
||||
<header>
|
||||
<div class="eyebrow">
|
||||
<span>D0 · test-plan §21</span>
|
||||
<span>mymuseum-visitapp</span>
|
||||
<span>15 cas</span>
|
||||
<span>12 août 2026</span>
|
||||
</div>
|
||||
<h1>Visite hors ligne — ce qu'on va vraiment mesurer</h1>
|
||||
<p class="standfirst">
|
||||
Cette passe ne cherche pas à valider l'app : elle cherche à savoir
|
||||
<strong>lesquels des quatre bugs offline restants existent encore</strong>. Chaque groupe de cas
|
||||
ci-dessous pointe vers un bug précis. Un groupe qui passe élimine son bug ; un groupe qui échoue
|
||||
lui donne sa priorité. C'est le seul moyen de trier D2 à D5 autrement qu'au jugé.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<h2>Avant de commencer</h2>
|
||||
<p class="lede">
|
||||
Les trois APK sont construits depuis le 12 août — c'est ce que K5 a débloqué. Prendre le flavor
|
||||
du lieu testé, pas <code>dev</code>, pour être sur les vraies données.
|
||||
</p>
|
||||
<pre>cd mymuseum-visitapp
|
||||
adb install -r build/app/outputs/flutter-apk/app-mdlf-debug.apk
|
||||
# ou app-fortsaintheribert-debug.apk
|
||||
|
||||
# 21.1.4 demande d'inspecter les fichiers téléchargés :
|
||||
adb shell run-as be.unov.mymuseum.mdlf ls -l files/</pre>
|
||||
<div class="callout">
|
||||
<h3>Ce qui a changé depuis la dernière fois qu'on a regardé</h3>
|
||||
<p>
|
||||
<strong>D1 est corrigé.</strong> Le <code>switch</code> de collecte des ressources était
|
||||
commenté des deux côtés — 157 lignes mortes dans <code>Export</code>, 126 dans
|
||||
<code>Import</code>. Une visite téléchargée n'embarquait que l'image de la configuration,
|
||||
celle du loader et l'image de chaque section : ni contenus d'articles, ni audios, ni icônes de
|
||||
carte, ni images de quiz. C'est réparé, mais <strong>vérifié par l'analyse seulement</strong>.
|
||||
Les cas 21.1.2, 21.1.3, 21.1.5 et 21.1.6 sont donc la <em>première</em> vérification réelle
|
||||
de ce correctif — s'ils échouent, ce n'est pas un bug de plus, c'est D1 qui n'a pas tenu.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>21.1 — État réel du téléchargement</h2>
|
||||
|
||||
<div class="group">
|
||||
<div class="group-head">
|
||||
<h3>Ce que la visite embarque</h3>
|
||||
<span class="verdict">valide D1 · révèle D3</span>
|
||||
</div>
|
||||
|
||||
<div class="case">
|
||||
<span class="id">21.1.1</span>
|
||||
<div class="what">
|
||||
<span class="act">Télécharger une visite, puis passer en mode avion</span>
|
||||
<span class="exp">La visite s'ouvre sans erreur.</span>
|
||||
</div>
|
||||
<span class="box"></span>
|
||||
</div>
|
||||
|
||||
<div class="case">
|
||||
<span class="id">21.1.2</span>
|
||||
<div class="what">
|
||||
<span class="act">Ouvrir un Article contenant des images de contenu</span>
|
||||
<span class="exp">Les images s'affichent. <b>Premier test réel de D1.</b></span>
|
||||
</div>
|
||||
<span class="box"></span>
|
||||
</div>
|
||||
|
||||
<div class="case">
|
||||
<span class="id">21.1.3</span>
|
||||
<div class="what">
|
||||
<span class="act">Lancer un audio dans cet Article</span>
|
||||
<span class="exp">L'audio se lit. Un échec ici peut venir de D1 <em>ou</em> de D3 — l'extension du fichier le départage, voir 21.1.4.</span>
|
||||
</div>
|
||||
<span class="box"></span>
|
||||
</div>
|
||||
|
||||
<div class="case">
|
||||
<span class="id">21.1.4</span>
|
||||
<div class="what">
|
||||
<span class="act">Inspecter le dossier local de la configuration</span>
|
||||
<span class="exp">Un fichier par ressource, <b>avec une vraie extension</b>. Un <code>.unknown</code> sur un audio, c'est <span class="tag alert">D3</span> — <code>audio/mp3</code> n'est pas un MIME standard, c'est <code>audio/mpeg</code>.</span>
|
||||
</div>
|
||||
<span class="box"></span>
|
||||
</div>
|
||||
|
||||
<div class="case">
|
||||
<span class="id">21.1.5</span>
|
||||
<div class="what">
|
||||
<span class="act">Ouvrir un Quiz avec images de questions et de réponses</span>
|
||||
<span class="exp">Les images s'affichent. <b>Test réel de D1.</b></span>
|
||||
</div>
|
||||
<span class="box"></span>
|
||||
</div>
|
||||
|
||||
<div class="case">
|
||||
<span class="id">21.1.6</span>
|
||||
<div class="what">
|
||||
<span class="act">Ouvrir une SectionMap téléchargée</span>
|
||||
<span class="exp">Fond de carte, icônes et points présents. Les icônes d'annotation sont précisément ce que D1 ne collectait pas.</span>
|
||||
</div>
|
||||
<span class="box"></span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>21.2 — Fraîcheur du contenu</h2>
|
||||
|
||||
<div class="group">
|
||||
<div class="group-head">
|
||||
<h3>Ce qui se met à jour, et ce qui se nettoie</h3>
|
||||
<span class="verdict">décide D2 et D4</span>
|
||||
</div>
|
||||
|
||||
<div class="case">
|
||||
<span class="id">21.2.1</span>
|
||||
<div class="what">
|
||||
<span class="act">Remplacer une image dans manager-app (même ressource), re-télécharger</span>
|
||||
<span class="exp">L'image mise à jour remplace l'ancienne. Sinon <span class="tag alert">D2</span> — le filtre incrémental teste la <em>présence</em> du fichier, pas sa <b>version</b> : une ressource déjà là n'est jamais re-téléchargée.</span>
|
||||
</div>
|
||||
<span class="box"></span>
|
||||
</div>
|
||||
|
||||
<div class="case">
|
||||
<span class="id">21.2.2</span>
|
||||
<div class="what">
|
||||
<span class="act">Relancer un téléchargement sans aucune modification</span>
|
||||
<span class="exp">Rien n'est re-téléchargé, message « à jour ». C'est le contrôle inverse de 21.2.1 : si D2 est corrigé trop grossièrement, tout se re-télécharge à chaque fois.</span>
|
||||
</div>
|
||||
<span class="box"></span>
|
||||
</div>
|
||||
|
||||
<div class="case">
|
||||
<span class="id">21.2.3</span>
|
||||
<div class="what">
|
||||
<span class="act">Supprimer une ressource dans manager-app, re-télécharger</span>
|
||||
<span class="exp">Le fichier local est purgé. Sinon <span class="tag alert">D4</span> — le <code>deleteSync()</code> est commenté. ⚠️ D1 a rebranché <code>usedImageOrAudioIds</code>, qui pilote cette purge : elle <b>peut être réparée sans qu'on l'ait touchée</b>.</span>
|
||||
</div>
|
||||
<span class="box"></span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>21.3 — Types non disponibles hors ligne</h2>
|
||||
|
||||
<div class="group">
|
||||
<div class="group-head">
|
||||
<h3>Ce qui doit dire non proprement</h3>
|
||||
<span class="verdict">aucun bug connu — cas de contrôle</span>
|
||||
</div>
|
||||
|
||||
<div class="case">
|
||||
<span class="id">21.3.1</span>
|
||||
<div class="what">
|
||||
<span class="act">En mode avion, ouvrir une section Weather</span>
|
||||
<span class="exp">Message explicite « nécessite une connexion ». <b>Ni écran blanc, ni spinner infini.</b></span>
|
||||
</div>
|
||||
<span class="box"></span>
|
||||
</div>
|
||||
|
||||
<div class="case">
|
||||
<span class="id">21.3.2</span>
|
||||
<div class="what">
|
||||
<span class="act">Idem sur une section Web</span>
|
||||
<span class="exp">Message explicite.</span>
|
||||
</div>
|
||||
<span class="box"></span>
|
||||
</div>
|
||||
|
||||
<div class="case">
|
||||
<span class="id">21.3.3</span>
|
||||
<div class="what">
|
||||
<span class="act">Idem sur une section Agenda</span>
|
||||
<span class="exp">Message explicite.</span>
|
||||
</div>
|
||||
<span class="box"></span>
|
||||
</div>
|
||||
|
||||
<div class="case">
|
||||
<span class="id">21.3.4</span>
|
||||
<div class="what">
|
||||
<span class="act">Ouvrir une SectionVideo pointant sur YouTube, en mode avion</span>
|
||||
<span class="exp">Message explicite, <b>distinct d'une vidéo téléversée</b> qui, elle, doit se lire. C'est le cas qui sépare « pas de réseau » de « pas téléchargé ».</span>
|
||||
</div>
|
||||
<span class="box"></span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>21.4 — Robustesse</h2>
|
||||
|
||||
<div class="group">
|
||||
<div class="group-head">
|
||||
<h3>Ce qui se passe quand ça rate</h3>
|
||||
<span class="verdict">décide D5</span>
|
||||
</div>
|
||||
|
||||
<div class="case">
|
||||
<span class="id">21.4.1</span>
|
||||
<div class="what">
|
||||
<span class="act">Couper le réseau pendant un téléchargement</span>
|
||||
<span class="exp">Le nombre d'échecs est remonté et la visite <b>n'est pas annoncée « téléchargée »</b>. Sinon <span class="tag alert">D5</span> — les échecs partent dans un <code>print</code>. C'est le pire des quatre : le visiteur croit avoir sa visite.</span>
|
||||
</div>
|
||||
<span class="box"></span>
|
||||
</div>
|
||||
|
||||
<div class="case">
|
||||
<span class="id">21.4.2</span>
|
||||
<div class="what">
|
||||
<span class="act">Relancer après un téléchargement partiel</span>
|
||||
<span class="exp">Seules les ressources manquantes sont récupérées.</span>
|
||||
</div>
|
||||
<span class="box"></span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Comment lire le résultat</h2>
|
||||
<p class="lede">
|
||||
Le but n'est pas une colonne de coches vertes. C'est de rentrer avec quatre verdicts.
|
||||
</p>
|
||||
|
||||
<div class="verdicts">
|
||||
<div class="verdict-row">
|
||||
<span class="bug">D2</span>
|
||||
<p><b>Fraîcheur.</b> Décidé par 21.2.1 et 21.2.2. Si une image remplacée ne remonte pas, le filtre doit porter sur la version et non la présence. <b>Conséquence produit :</b> une correction de contenu ne parvient jamais aux visites déjà téléchargées.</p>
|
||||
</div>
|
||||
<div class="verdict-row">
|
||||
<span class="bug">D3</span>
|
||||
<p><b>Extensions.</b> Décidé par 21.1.4, confirmé par 21.1.3. Un <code>.unknown</code> sur un audio suffit à trancher — inutile d'attendre que la lecture échoue.</p>
|
||||
</div>
|
||||
<div class="verdict-row">
|
||||
<span class="bug">D4</span>
|
||||
<p><b>Purge.</b> Décidé par 21.2.3. <b>Peut déjà être réparé</b> par effet de bord de D1 : à vérifier avant de planifier quoi que ce soit.</p>
|
||||
</div>
|
||||
<div class="verdict-row">
|
||||
<span class="bug">D5</span>
|
||||
<p><b>Échecs silencieux.</b> Décidé par 21.4.1. Le plus grave des quatre parce qu'il est invisible : une visite incomplète s'annonce complète, et l'erreur ne se découvre que dans le fort, sans réseau.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="callout">
|
||||
<h3>Si tout passe</h3>
|
||||
<p>
|
||||
Ce n'est pas un résultat décevant, c'est un résultat qui <strong>ferme le lot D</strong> :
|
||||
D2 à D5 tombent, et le plan perd un à deux jours de travail non nécessaire. Le lien
|
||||
<strong>L11</strong> dit exactement cela — le §21 conditionne l'<em>urgence</em> des quatre
|
||||
bugs, pas leur existence. Noter aussi la plainte d'origine, « l'appli marche mal dans le
|
||||
fort » : si rien ne se reproduit ici, c'est qu'elle vient d'ailleurs, et ça vaut d'être su.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
DOCS/claude design/checklist-d0-offline.html · dérivée de test-plan.md §21 · reporter les
|
||||
résultats dans le §21 et dans STATUS.md
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
59
kanban.html
59
kanban.html
@ -448,7 +448,7 @@
|
||||
<p>Vue d'état consolidée depuis <code>DOCS/</code>. Source de vérité : <code>STATUS.md</code> — cette page en est le reflet, pas le remplaçant. Filtrez sur <strong>V1</strong> pour ne voir que ce qui reste avant la mise en prod. <strong>L'ordre d'exécution</strong>, lui, est dans <code>v1-plan.md</code> : ce tableau dit où en est chaque chantier, pas lequel bloque lequel.</p>
|
||||
</div>
|
||||
<div class="stamp">
|
||||
Mise à jour · 2026-08-11<br>
|
||||
Mise à jour · 2026-08-12<br>
|
||||
Postgres v3 · pas encore en prod
|
||||
</div>
|
||||
</header>
|
||||
@ -458,9 +458,9 @@
|
||||
<div class="stat"><span class="n n-info">2</span><span class="k">Migration v3</span></div>
|
||||
<div class="stat"><span class="n n-warn">5</span><span class="k">Bugs ouverts</span></div>
|
||||
<div class="stat"><span class="n">6</span><span class="k">À tester</span></div>
|
||||
<div class="stat"><span class="n">22</span><span class="k">Planifié</span></div>
|
||||
<div class="stat"><span class="n">19</span><span class="k">Planifié</span></div>
|
||||
<div class="stat"><span class="n n-gate">8</span><span class="k">Bascule prod</span></div>
|
||||
<div class="stat"><span class="n n-good">41</span><span class="k">Fait récemment</span></div>
|
||||
<div class="stat"><span class="n n-good">44</span><span class="k">Fait récemment</span></div>
|
||||
</section>
|
||||
|
||||
<div class="filters" role="group" aria-label="Filtrer par domaine">
|
||||
@ -617,15 +617,8 @@
|
||||
|
||||
<!-- PLANIFIÉ -->
|
||||
<section class="col" style="--stripe: var(--ink-3)">
|
||||
<div class="col-head"><h2>Planifié</h2><span class="count">22</span></div>
|
||||
<div class="col-head"><h2>Planifié</h2><span class="count">19</span></div>
|
||||
|
||||
<article class="card" data-area="manager" data-horizon="v1">
|
||||
<div class="card-meta"><span class="tag">tablet-app</span><span class="flag f-warn">Lot K · K8 — nouveau 12/08</span></div>
|
||||
<h3>Retour automatique à l'accueil après inactivité</h3>
|
||||
<p>Un visiteur part sans fermer l'écran ; le suivant trouve l'article du précédent. Délai proposé : <strong>5 minutes</strong>. Sorti de la maquette K3/K7 parce que ce n'est ni l'un ni l'autre — c'est un comportement de borne qui vaut pour <strong>les treize types</strong>.</p>
|
||||
<p><strong>À écrire une seule fois</strong>, dans <code>section_page_detail</code> : il enveloppe déjà toutes les sections et porte déjà les crochets <code>initState</code>/<code>dispose</code> des statistiques. <strong>Bénéfice de bord</strong> : le retour passant par le même <code>dispose</code>, il émet un <code>sectionLeave</code> avec sa <em>durée réelle</em> au lieu de laisser une consultation ouverte jusqu'au prochain visiteur — la statistique devient juste, en plus de l'écran.</p>
|
||||
<span class="src">v1-plan.md lot K — K8 · maquette kiosk-paysage</span>
|
||||
</article>
|
||||
|
||||
<div class="stack">
|
||||
|
||||
@ -644,17 +637,10 @@
|
||||
</article>
|
||||
|
||||
<article class="card" data-area="backend" data-horizon="v1">
|
||||
<div class="card-meta"><span class="tag">manager-service</span><span class="flag f-warn">Devenu pressant 09/08</span></div>
|
||||
<h3><code>GetSummary</code> agrège en mémoire</h3>
|
||||
<p><code>eventsQuery.ToList()</code> charge <strong>tous</strong> les événements de la période avant d'agréger. Tenable tant que la fenêtre était de 30 jours ; elle vient de passer à <strong>13 mois</strong>. À passer en SQL avant de vendre le rapport annuel à un gros site — sinon c'est la mémoire du conteneur qui arbitre.</p>
|
||||
<span class="src">todo-features.md — Plans & Quotas</span>
|
||||
</article>
|
||||
|
||||
<article class="card" data-area="backend" data-horizon="v1">
|
||||
<div class="card-meta"><span class="tag">manager-service</span><span class="flag f-warn">Dette · découvert 09/08</span></div>
|
||||
<h3>Aucun test ne couvrira le RAG</h3>
|
||||
<p>Ajouter <code>ContentEmbedding</code> a cassé <strong>116 des 124 tests</strong> d'un coup : la suite tourne sur <strong>EF InMemory</strong>, qui ignore le type <code>Vector</code> et refuse de valider le modèle. Contourné en excluant l'entité hors Npgsql — les tests repassent, mais vector store, recherche cosinus, index HNSW et contrainte anti-doublon Hangfire restent <em>structurellement</em> non testables. Piste : <code>Testcontainers.PostgreSql</code> sur l'image <code>Dockerfile.postgres</code>.</p>
|
||||
<span class="src">STATUS.md §1quater — Dette ouverte</span>
|
||||
<div class="card-meta"><span class="tag">manager-service</span><span class="flag f-warn">RGPD · lot J</span></div>
|
||||
<h3><code>AuditLog</code> n'a aucune purge</h3>
|
||||
<p><code>VisitEvent</code> purge à 13 mois, <code>VisitorQuestion</code> à 90 jours, <code>AuditLog</code> <strong>à jamais</strong>. <strong>C'est un sujet RGPD et rien d'autre</strong> : la table porte <code>UserId</code>, et les valeurs avant-après d'une modification de <code>User</code> contiennent e-mail, prénom et nom. <strong>Ce n'est pas un sujet de volume</strong> — l'inondation par <code>WeatherSyncService</code> est coupée depuis le 12/08, la table croît désormais au seul rythme des éditions humaines. <strong>Donc à traiter au lot J, pas au lot F.</strong> Recommandation : <strong>12 mois, uniforme</strong>, via un <code>AuditLogPurgeService</code> calqué sur <code>VisitEventPurgeService</code> — 12 et non 13, le 13 mois des stats existe pour comparer une saison à la précédente et le recopier ici serait du mimétisme. ⚠️ <strong>Reprendre le même verrou</strong> : purge inerte tant qu'<code>Audit:RetentionDays</code> n'est pas défini, <strong>le pg_dump quotidien n'étant toujours pas en place</strong> — supprimer des lignes d'audit sans restauration fine est la pire combinaison. Écarté : garder les <code>Delete</code> plus longtemps que les <code>Create</code>/<code>Update</code>, ça double les règles pour un cas qu'une sauvegarde couvre déjà.</p>
|
||||
<span class="src">v1-plan.md lot F · STATUS.md §1sexies</span>
|
||||
</article>
|
||||
|
||||
<article class="card" data-area="backend">
|
||||
@ -757,13 +743,6 @@
|
||||
<span class="src">todo-features.md — AR</span>
|
||||
</article>
|
||||
|
||||
<article class="card" data-area="backend">
|
||||
<div class="card-meta"><span class="tag">backend</span><span class="flag f-warn">ouvert par le front du 12/08</span></div>
|
||||
<h3>Audit & plafond users — les deux dettes serveur</h3>
|
||||
<p><strong>Le front est livré (12/08), ces deux points ne le sont pas.</strong> (1) <strong>Aucune section n'est journalisée</strong> : <code>AuditedTypes.Contains(entry.Entity.GetType())</code> exige l'égalité exacte, or <code>Section</code> est <strong>abstraite</strong> — le type runtime est toujours <code>SectionMap</code>, <code>SectionQuiz</code>… donc <code>typeof(Section)</code> ne matche jamais. Le journal couvre ressources, configurations, devices, users et instances, mais <strong>pas le contenu</strong> — précisément ce que l'écran devait tracer. Correctif : <code>Any(t => t.IsInstanceOfType(…))</code>, en décidant si <code>EntityType</code> porte alors <code>SectionMap</code> ou reste normalisé à <code>Section</code>. (2) <strong>Plafond de 5 utilisateurs</strong> : <code>UserController.CreateUser</code> ne compte rien, pas de 422 — le compteur livré est un garde-fou d'interface, un POST direct sur l'API passe toujours. Le retour d'erreur du POST est déjà branché côté front : le 422 sera visible sans retouche.</p>
|
||||
<span class="src">todo-features.md · v1-plan.md lot F · STATUS.md §1sexies</span>
|
||||
</article>
|
||||
|
||||
<article class="card" data-area="manager" data-horizon="v2">
|
||||
<div class="card-meta"><span class="tag">produit</span><span class="flag f-warn">V2 · nouveau 12/08</span></div>
|
||||
<h3>Écran SuperAdmin — add-on IA & canaux d'une instance</h3>
|
||||
@ -866,9 +845,21 @@
|
||||
|
||||
<section class="done">
|
||||
<h2>Fait récemment</h2>
|
||||
<p>Quarante et un chantiers clos entre le 5 et le 12 août 2026.</p>
|
||||
<p>Quarante-quatre chantiers clos entre le 5 et le 12 août 2026.</p>
|
||||
<div class="done-grid">
|
||||
|
||||
<div class="done-item">
|
||||
<strong>C4 — compression des images à l'upload, 2560 px / JPEG q82</strong>
|
||||
<span><code>flutter build web</code> vert. Un seul helper appelé par les <strong>deux</strong> chemins d'upload de <code>resources_screen</code> — le motif exact qui a fait diverger <code>Create</code> et <code>Upload</code> côté serveur, sur <code>StoragePath</code> en C1 puis sur le pré-vol de quota en C3. ⚠️ <strong>La compression se fait avant <code>resourceCreate</code>, pas avant l'upload</strong> : le quota de C3 porte sur <code>sizeBytes</code> à la création, donc compresser après aurait fait décider le serveur sur la taille d'origine — et un quota qui compte 12× trop se remplit 12× trop vite.</span>
|
||||
<span>⚠️ <strong>Écart assumé à l'énoncé</strong> : un PNG à canal alpha reste un PNG. Le passer en JPEG remplirait la transparence de noir et abîmerait logos et filigranes ; seul l'encodage diffère, le redimensionnement s'applique aux deux, et le type MIME suit. Si le ré-encodage grossit l'original — ce qui arrive sur une petite image déjà bien encodée — l'original est conservé. ⚠️ <strong>Défaut préexistant trouvé au passage</strong> : le second chemin d'upload ne renseignait <code>sizeBytes</code> ni avant ni après. Tout ce qui est passé par là compte <strong>0 octet au quota</strong> sur l'existant.</span>
|
||||
</div>
|
||||
|
||||
<div class="done-item">
|
||||
<strong>K8 — la borne revient à l'accueil après 5 min sans interaction</strong>
|
||||
<span>Un visiteur part sans fermer l'écran ; le suivant trouvait le contenu du précédent. <strong>Écrit une seule fois</strong>, dans <code>section_page_detail</code>, qui enveloppe les treize types et portait déjà les crochets des statistiques — dans les écrans, il aurait été écrit treize fois.</span>
|
||||
<span><strong>Bénéfice de bord qui n'en est pas un</strong> : le retour passe par le même <code>dispose</code>, donc il émet un <code>sectionLeave</code> avec sa durée réelle. Sans lui, une consultation abandonnée restait ouverte jusqu'au visiteur suivant et gonflait la durée mesurée d'autant — <strong>K8 répare une statistique en plus d'un écran</strong>. <code>Listener</code> plutôt que <code>GestureDetector</code> pour réarmer : il observe les pointeurs sans concurrencer les gestes des écrans en dessous. ⚠️ Non vérifié : le comportement après 5 minutes réelles, et le fait que l'accueil soit bien la première route de la pile.</span>
|
||||
</div>
|
||||
|
||||
<div class="done-item">
|
||||
<strong>La borne couvre 12 des 13 types — SectionArticle (K7) et SectionEvent (K3) livrés</strong>
|
||||
<span><code>flutter build apk --debug</code> <strong>vert</strong>, APK produit. Seul <code>SectionParcours</code> reste dehors, et <strong>définitivement</strong> — un parcours guidé fait marcher le visiteur. Les deux écrans suivent la maquette paysage : rail média ancré + colonne de lecture plafonnée pour l'article, bande héros à 26 % + programme + carte vive pour l'événement, détail d'un bloc <strong>sous la carte</strong> et non en <code>showModalBottomSheet</code>.</span>
|
||||
@ -908,6 +899,14 @@
|
||||
<span><strong>Trois de ces crans sont des alignements sur <code>mymuseum-visitapp</code></strong>, qui utilise le même plugin mapbox sans rencontrer aucun de ces problèmes. Réflexe à garder : comparer les deux <code>gradle.properties</code> et les deux <code>build.gradle</code> avant de chercher. Ce que ça débloque : <code>compileSdkVersion 36</code>, sans lequel plus aucune mise à jour n'est publiable sur le store. ⚠️ Le compte de « six crans » annoncé plus tôt dans la journée était provisoire — il datait du moment où la chaîne Gradle passait, avant que la compilation Kotlin ne révèle la suite.</span>
|
||||
</div>
|
||||
|
||||
<div class="done-item">
|
||||
<strong>Lot F côté manager-service : les quatre chantiers de dette backend, en une passe</strong>
|
||||
<span><code>dotnet test</code> <strong>163 → 200</strong>, aucun test sauté, quatre commits, <code>manager-service</code> seul. <strong>1) Le journal d'audit voyait tout sauf le contenu</strong> : <code>AuditedTypes.Contains(entry.Entity.GetType())</code> exigeait l'égalité <strong>exacte</strong>, or <code>Section</code> est abstraite — aucune des 13 sortes de section n'était journalisée, soit précisément ce que l'écran du 12/08 devait tracer. <code>Resource</code>, <code>Configuration</code>, <code>Device</code>, <code>User</code> et <code>Instance</code> passaient, eux : ils sont concrets, <strong>et c'est ce qui rendait le trou invisible</strong>. <strong>Tranché : normalisation côté serveur</strong> — <code>EntityType</code> porte <code>Section</code>, donc le filtre déjà présent dans l'écran se met à rendre des lignes <strong>sans toucher <code>manager-app</code></strong> ; élargir le filtre front aurait coûté 13 entrées et 39 clés i18n <em>et</em> laissé tout futur sous-type sortir du filtre en silence — la classe de panne exacte qu'on ferme. Le sous-type n'est pas perdu : le discriminateur TPH est sérialisé dans <code>NewValues</code>, un test le vérifie. 8 tests, dont un <strong>par réflexion</strong> sur les 13 sous-types — la liste d'origine n'obligeait personne à la suivre, c'est ce qui l'a laissée devenir fausse.</span>
|
||||
<span><strong>2) Plafond de 5 utilisateurs</strong> : <code>CreateUser</code> rend <strong>422</strong>. <strong>5 en dur</strong> (le faire varier par plan serait une colonne, donc une migration après le gel du lot B — dette V1 assumée) et <strong>SuperAdmin exempté</strong>, seule porte de service tant qu'aucun champ ne permet de relever la limite d'un client. Rien à retoucher côté front. <strong>3) <code>GetSummary</code> agrège en SQL</strong> au lieu de charger 13 mois en mémoire ; les six agrégats qui se lisent dans le JSON de <code>Metadata</code> ne remontent plus que <strong>deux colonnes</strong>, pour leur seul type d'événement, et les stats avancées ne sont plus calculées puis effacées pour les plans qui n'y ont pas droit. ⚠️ <strong>La branche « stats avancées » n'était couverte par aucun test</strong> : les cas existants ne seedent pas d'<code>Instance</code>, donc <code>hasAdvancedStats</code> était toujours faux et la moitié de la méthode n'était jamais exécutée.</span>
|
||||
<span><strong>4) Le vector store est éprouvé à DEUX instances, sur un vrai Postgres</strong> — 14 tests Testcontainers sur l'image de <code>Dockerfile.postgres</code>, qui se sautent proprement sans démon Docker (186 passés / 14 sautés / 0 échec). pgvector <strong>0.8.6</strong> : le <code>SET hnsw.iterative_scan</code> que pose <code>SearchAsync</code> est accepté — sur une version antérieure, <strong>toute recherche échouait en production</strong>. À 30 contre 1, la recherche rend le bon nombre de résultats, tous de la bonne instance, aucune fuite. ⚠️ <strong>Deux résultats contraires à ce que le plan supposait</strong> : ce qui protège du post-filtrage <strong>n'est pas le parcours itératif mais l'index sur <code>(InstanceId, ContentType)</code></strong> — le planificateur filtre d'abord et trie exactement, l'index HNSW n'est jamais touché (vérifié à 620 lignes et à 22 000) ; et cet index retiré, <code>relaxed_order</code> <strong>ne rattrape rien</strong>, le parcours s'épuisant après ~335 lignes sans atteindre l'instance minoritaire. Le même jeu de données avec l'index HNSW construit <em>après</em> l'insertion rend bien ses 20 lignes : c'est la connectivité du graphe qui décide, et nos migrations créent l'index sur une table vide. <strong>Outillage</strong> : Testcontainers épinglé en 3.10.0 (la 4.x parle l'API Docker 1.44, l'engine local plafonne à 1.43) et image construite par le CLI docker — le constructeur de Testcontainers ne sait pas parser le <code>tag@sha256:</code> du Dockerfile, digest qui protège la base d'un changement de glibc sous ses index et ne se retire pas pour un test.</span>
|
||||
<span>⚠️ <strong>Et une régression du point 1, trouvée et corrigée le même jour.</strong> <code>WeatherSyncService</code> écrit <code>section.WeatherResult</code> sur cron, à <strong>6 h et 13 h</strong>. Le job était bénin tant que les sections n'étaient pas auditées ; depuis, chaque rafraîchissement produisait une ligne portant la <strong>prévision OpenWeather complète en avant ET en après</strong> — quelques dizaines de Ko, deux fois par jour, par section météo. Le stockage est le moindre problème : <strong>ce bruit noie les modifications humaines</strong> que l'écran existe pour montrer. Correctif : colonnes machine (<code>WeatherResult</code>, <code>WeatherUpdatedDate</code>, <code>DateUpdate</code>) exclues du journal, et <strong>aucune ligne produite</strong> quand une modification ne touche qu'elles — plutôt qu'une ligne au diff vide, qui aurait déplacé le bruit sans le retirer. <code>DateUpdate</code> y est pour une raison distincte : estampillé à chaque <code>SaveChanges</code>, il figurait dans tous les diffs sans rien y apprendre — <strong>et le signal était déjà là</strong>, un test écrit le matin même devait l'écarter à la main pour rester lisible. <code>AgendaSyncService</code> vérifié au passage : il écrit des <code>EventAgenda</code>, non audités. ⚠️ <strong>Trouvé en cherchant à justifier un index sur <code>Timestamp</code> signalé par réflexe</strong> — la question « pourquoi un index ? » a montré que ce qui faisait grossir la table n'était pas ce que je croyais. <strong>L'index est retiré du backlog</strong> : pas de migration, gel du lot B intact. <code>dotnet test</code> <strong>203/203</strong>.</span>
|
||||
</div>
|
||||
|
||||
<div class="done-item">
|
||||
<strong>Écran d'audit log et compteur d'utilisateurs — le lot F côté manager-app</strong>
|
||||
<span>Deux chantiers dans la même passe, <code>manager-app</code> seul. <strong>Écran « Activité »</strong> sur <code>GET /api/Audit</code> : filtres instance / type d'entité / utilisateur / plage de dates, pagination 50, clic sur une ligne → détail avant-après en table, entrée de menu réservée à <code>role.value == 0</code> comme la policy de l'endpoint. Les ids sont résolus en noms, un id inconnu restant affiché tel quel — le journal doit rester lisible après la disparition de ce qu'il décrit. <strong>Compteur « X / 5 utilisateurs »</strong> et bouton d'ajout désactivé au plafond, <strong>SuperAdmin exclu</strong> : sa liste couvre toutes les instances, la compter contre un plafond <em>par instance</em> n'aurait aucun sens. <strong><code>manager_api_new</code> n'a pas été touché</strong> — <code>http.get</code> direct au Bearer, comme le Guide IA : une lecture seule ne justifie pas d'étendre un client qui s'édite à la main. 40 clés i18n FR/EN/NL, <code>flutter build web</code> ✅. ⚠️ <strong>Trois pièges relevés en câblant</strong> : <code>AuditController</code> renvoie les entités <strong>brutes</strong>, pas un DTO ; <code>invokeAPI</code> <strong>ne lève pas</strong> sur code d'erreur et son résultat était ignoré, donc un e-mail déjà pris (409) ne produisait aucun message — corrigé ; et <code>DropdownButtonFormField</code> <strong>ne relit pas <code>initialValue</code></strong> sur reconstruction, donc « Réinitialiser les filtres » vidait la requête sans vider l'affichage — remplacé par un <code>DropdownButton</code> piloté. Les deux dettes serveur qu'il ouvre sont sur leur propre carte, en Planifié.</span>
|
||||
|
||||
18
v1-plan.md
18
v1-plan.md
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user