DOCS/kanban.html
Thomas Fransolet a5a8ecdb20 Documentation interne MyInfoMate / Unov
Import initial de la documentation : statut, roadmap, plans V1/V2,
specs verticales (creche, sport), audits securite, plan de test,
analyse concurrentielle et maquettes de design.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 11:17:01 +02:00

1115 lines
66 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<title>MyInfoMate — Tableau de chantiers</title>
<style>
:root {
--ground: #F4F6F8;
--surface: #FFFFFF;
--surface-2: #EBEEF2;
--ink: #14202B;
--ink-2: #4A5A6B;
--ink-3: #7C8B9A;
--line: #D6DDE5;
--line-soft: #E4EAF0;
--brand: #264863;
--brand-soft: #C2C9D6;
--critical: #A8322A;
--warn: #9A6608;
--good: #1F6B4D;
--info: #2B5F86;
--gate: #6B3F8C;
--shadow: 0 1px 2px rgba(20, 32, 43, .06), 0 4px 12px rgba(20, 32, 43, .04);
--sans: ui-sans-serif, -apple-system, "Segoe UI", system-ui, "Helvetica Neue", Arial, sans-serif;
--mono: ui-monospace, "Cascadia Mono", "SF Mono", "Consolas", "Liberation Mono", monospace;
}
@media (prefers-color-scheme: dark) {
:root {
--ground: #0F161D;
--surface: #18222C;
--surface-2: #212D39;
--ink: #E6ECF2;
--ink-2: #A3B2C0;
--ink-3: #74879A;
--line: #2C3A48;
--line-soft: #243140;
--brand: #7FA8C9;
--brand-soft: #35506B;
--critical: #E08078;
--warn: #D6A343;
--good: #6FBF97;
--info: #7CB2DA;
--gate: #B48BD6;
--shadow: 0 1px 2px rgba(0, 0, 0, .3), 0 4px 14px rgba(0, 0, 0, .22);
}
}
:root[data-theme="dark"] {
--ground: #0F161D;
--surface: #18222C;
--surface-2: #212D39;
--ink: #E6ECF2;
--ink-2: #A3B2C0;
--ink-3: #74879A;
--line: #2C3A48;
--line-soft: #243140;
--brand: #7FA8C9;
--brand-soft: #35506B;
--critical: #E08078;
--warn: #D6A343;
--good: #6FBF97;
--info: #7CB2DA;
--gate: #B48BD6;
--shadow: 0 1px 2px rgba(0, 0, 0, .3), 0 4px 14px rgba(0, 0, 0, .22);
}
:root[data-theme="light"] {
--ground: #F4F6F8;
--surface: #FFFFFF;
--surface-2: #EBEEF2;
--ink: #14202B;
--ink-2: #4A5A6B;
--ink-3: #7C8B9A;
--line: #D6DDE5;
--line-soft: #E4EAF0;
--brand: #264863;
--brand-soft: #C2C9D6;
--critical: #A8322A;
--warn: #9A6608;
--good: #1F6B4D;
--info: #2B5F86;
--shadow: 0 1px 2px rgba(20, 32, 43, .06), 0 4px 12px rgba(20, 32, 43, .04);
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--ground);
color: var(--ink);
font-family: var(--sans);
font-size: 15px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
.wrap {
max-width: 1500px;
margin: 0 auto;
padding: 40px 28px 72px;
}
/* ---------- header ---------- */
.masthead {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
justify-content: space-between;
gap: 20px;
padding-bottom: 22px;
border-bottom: 2px solid var(--brand);
}
.masthead h1 {
margin: 0;
font-size: clamp(26px, 3.4vw, 38px);
font-weight: 700;
letter-spacing: -0.022em;
text-wrap: balance;
}
.masthead p {
margin: 8px 0 0;
color: var(--ink-2);
max-width: 62ch;
}
.stamp {
font-family: var(--mono);
font-size: 11.5px;
letter-spacing: .06em;
text-transform: uppercase;
color: var(--ink-3);
text-align: right;
line-height: 1.7;
}
/* ---------- summary ---------- */
.summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1px;
background: var(--line);
border: 1px solid var(--line);
border-radius: 6px;
overflow: hidden;
margin: 26px 0 0;
}
.stat {
background: var(--surface);
padding: 14px 16px 15px;
}
.stat .n {
font-family: var(--mono);
font-size: 27px;
font-weight: 600;
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
display: block;
line-height: 1.1;
}
.stat .k {
display: block;
margin-top: 5px;
font-size: 11.5px;
letter-spacing: .07em;
text-transform: uppercase;
color: var(--ink-3);
}
.n-critical { color: var(--critical); }
.n-warn { color: var(--warn); }
.n-good { color: var(--good); }
.n-info { color: var(--info); }
.n-gate { color: var(--gate); }
/* ---------- filters ---------- */
.filters {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin: 26px 0 20px;
}
.filters .lead {
font-family: var(--mono);
font-size: 11.5px;
letter-spacing: .07em;
text-transform: uppercase;
color: var(--ink-3);
margin-right: 4px;
}
.chip-btn {
font-family: var(--mono);
font-size: 12px;
color: var(--ink-2);
background: var(--surface);
border: 1px solid var(--line);
border-radius: 999px;
padding: 5px 13px;
cursor: pointer;
transition: background .13s ease, color .13s ease, border-color .13s ease;
}
.chip-btn:hover { border-color: var(--brand); color: var(--ink); }
.chip-btn:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
}
.chip-btn[aria-pressed="true"] {
background: var(--brand);
border-color: var(--brand);
color: var(--surface);
}
:root[data-theme="dark"] .chip-btn[aria-pressed="true"],
.chip-btn[aria-pressed="true"] { color: #fff; }
@media (prefers-color-scheme: dark) {
.chip-btn[aria-pressed="true"] { color: #0F161D; }
}
:root[data-theme="dark"] .chip-btn[aria-pressed="true"] { color: #0F161D; }
:root[data-theme="light"] .chip-btn[aria-pressed="true"] { color: #fff; }
/* ---------- board ---------- */
.board {
display: grid;
grid-auto-flow: column;
grid-auto-columns: minmax(288px, 1fr);
gap: 18px;
overflow-x: auto;
padding-bottom: 12px;
align-items: start;
}
.col { min-width: 0; }
.col-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 10px;
padding: 0 2px 10px;
border-bottom: 2px solid var(--stripe, var(--line));
margin-bottom: 14px;
}
.col-head h2 {
margin: 0;
font-size: 13px;
font-weight: 700;
letter-spacing: .08em;
text-transform: uppercase;
color: var(--stripe, var(--ink));
}
.col-head .count {
font-family: var(--mono);
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--ink-3);
}
.stack {
display: flex;
flex-direction: column;
gap: 10px;
}
.card {
background: var(--surface);
border: 1px solid var(--line-soft);
border-left: 3px solid var(--stripe, var(--line));
border-radius: 5px;
padding: 12px 14px 13px;
box-shadow: var(--shadow);
}
.card h3 {
margin: 0 0 5px;
font-size: 14.5px;
font-weight: 650;
line-height: 1.35;
letter-spacing: -0.008em;
text-wrap: balance;
}
.card p {
margin: 0;
font-size: 13px;
line-height: 1.5;
color: var(--ink-2);
}
.card-meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
margin-bottom: 7px;
}
.tag {
font-family: var(--mono);
font-size: 10.5px;
letter-spacing: .04em;
color: var(--ink-3);
background: var(--surface-2);
border-radius: 3px;
padding: 2px 6px;
white-space: nowrap;
}
.flag {
font-family: var(--mono);
font-size: 10.5px;
letter-spacing: .05em;
text-transform: uppercase;
border-radius: 3px;
padding: 2px 6px;
white-space: nowrap;
border: 1px solid currentColor;
}
.f-critical { color: var(--critical); }
.f-warn { color: var(--warn); }
.f-good { color: var(--good); }
.src {
display: block;
margin-top: 9px;
font-family: var(--mono);
font-size: 11px;
color: var(--ink-3);
word-break: break-word;
}
/* ---------- done ---------- */
.done {
margin-top: 46px;
padding-top: 26px;
border-top: 1px solid var(--line);
}
.done h2 {
margin: 0 0 4px;
font-size: 13px;
font-weight: 700;
letter-spacing: .08em;
text-transform: uppercase;
color: var(--good);
}
.done > p {
margin: 0 0 18px;
color: var(--ink-3);
font-size: 13px;
}
.done-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(268px, 1fr));
gap: 10px;
}
.done-item {
background: var(--surface);
border: 1px solid var(--line-soft);
border-left: 3px solid var(--good);
border-radius: 5px;
padding: 11px 13px;
}
.done-item strong {
display: block;
font-size: 13.5px;
font-weight: 650;
margin-bottom: 3px;
}
.done-item span {
font-size: 12.5px;
color: var(--ink-2);
}
/* ---------- footer ---------- */
.foot {
margin-top: 44px;
padding-top: 20px;
border-top: 1px solid var(--line);
font-size: 13px;
color: var(--ink-3);
display: flex;
flex-wrap: wrap;
gap: 8px 22px;
}
.foot code {
font-family: var(--mono);
font-size: 12px;
color: var(--ink-2);
}
.hidden { display: none !important; }
.filters-horizon { margin-top: -8px; }
.stat-muted .n { color: var(--ink-3); }
.empty-msg {
margin: 0 0 20px;
padding: 18px 20px;
background: var(--surface);
border: 1px dashed var(--line);
border-radius: 6px;
color: var(--ink-3);
font-size: 13.5px;
}
@media (prefers-reduced-motion: reduce) {
* { transition: none !important; }
}
@media (max-width: 720px) {
.wrap { padding: 28px 16px 56px; }
.stamp { text-align: left; }
}
</style>
<div class="wrap">
<header class="masthead">
<div>
<h1>MyInfoMate — tableau de chantiers</h1>
<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>
Postgres v3 · pas encore en prod
</div>
</header>
<section class="summary" aria-label="Chiffres clés">
<div class="stat"><span class="n n-critical">5</span><span class="k">Urgent</span></div>
<div class="stat"><span class="n n-info">5</span><span class="k">Migration v3</span></div>
<div class="stat"><span class="n n-warn">7</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">23</span><span class="k">Planifié</span></div>
<div class="stat"><span class="n n-gate">10</span><span class="k">Bascule prod</span></div>
<div class="stat"><span class="n n-good">19</span><span class="k">Fait récemment</span></div>
</section>
<div class="filters" role="group" aria-label="Filtrer par domaine">
<span class="lead">Domaine</span>
<button class="chip-btn" type="button" data-filter="all" aria-pressed="true">Tout</button>
<button class="chip-btn" type="button" data-filter="backend" aria-pressed="false">manager-service</button>
<button class="chip-btn" type="button" data-filter="manager" aria-pressed="false">manager-app</button>
<button class="chip-btn" type="button" data-filter="visitapp" aria-pressed="false">visitapp</button>
<button class="chip-btn" type="button" data-filter="infra" aria-pressed="false">infra &amp; ops</button>
<button class="chip-btn" type="button" data-filter="doc" aria-pressed="false">doc &amp; commercial</button>
</div>
<div class="filters filters-horizon" role="group" aria-label="Filtrer par horizon">
<span class="lead">Horizon</span>
<button class="chip-btn" type="button" data-horizon-filter="all" aria-pressed="true">Tout</button>
<button class="chip-btn" type="button" data-horizon-filter="v1" aria-pressed="false">V1 — reste à faire</button>
<button class="chip-btn" type="button" data-horizon-filter="v2" aria-pressed="false">V2 — reporté</button>
</div>
<p class="empty-msg hidden" role="status">Aucun chantier ne correspond à ces deux filtres.</p>
<div class="board">
<!-- URGENT -->
<section class="col" style="--stripe: var(--critical)">
<div class="col-head"><h2>Urgent</h2><span class="count">5</span></div>
<div class="stack">
<article class="card" data-area="infra" data-horizon="v1">
<div class="card-meta"><span class="tag">tous les repos</span><span class="flag f-warn">Revérifié 11/08</span></div>
<h3>Travail non committé — largement résorbé</h3>
<p>L'alerte du 09/08 (« rien depuis le 17/07 », 75 + 40 + 14 + 4 fichiers) est <strong>périmée</strong> : les quatre repos ont un commit du <strong>09/08</strong>. Reste en working tree le travail du 10-11/08 — <code>manager-service</code> ~41 fichiers (lot 3 RAG, <code>IngestionService</code>, colonnes médias, endpoint <code>knowledge</code>), <code>manager-app</code> (socle visuel, écran Guide IA), <code>myinfomate-landing</code> 7, plus un commit non poussé sur <code>visitapp-web</code>.</p>
<p>Reste à committer, mais ce n'est plus trois semaines sans filet.</p>
<span class="src">STATUS.md §4 — Infrastructure &amp; Ops</span>
</article>
<article class="card" data-area="backend infra">
<div class="card-meta"><span class="tag">manager-service</span><span class="flag f-critical">Sécurité</span></div>
<h3>Rotation des secrets committés en clair</h3>
<p>Clé de signature JWT, clés API Gemini et OpenWeather, connection strings prod, mot de passe MQTT, token Telegram. Le dossier <code>RELEASE/</code> republie d'anciens secrets.</p>
<span class="src">security/audit-securite-manager-service.md</span>
</article>
<article class="card" data-area="infra">
<div class="card-meta"><span class="tag">infra</span><span class="flag f-critical">Sécurité</span></div>
<h3>Port PostgreSQL exposé publiquement</h3>
<p>À fermer via Traefik/Docker. Accès DBeaver par tunnel SSH uniquement.</p>
<span class="src">todo-features.md — Sécurité réseau</span>
</article>
<article class="card" data-area="manager">
<div class="card-meta"><span class="tag">tablet-app</span><span class="flag f-warn">Build</span></div>
<h3>tablet-app ne compile pas</h3>
<p>Échec Gradle sur <code>flutter_tools/gradle/build.gradle.kts</code>. Seul build encore cassé des cinq. Le dernier APK date d'avril.</p>
<span class="src">parity-manager-visitapp.md §6 — B3</span>
</article>
<article class="card" data-area="manager">
<div class="card-meta"><span class="tag">manager_api_new</span><span class="flag f-warn">Dette</span></div>
<h3>Client généré désynchronisé</h3>
<p>Des fichiers modèle orphelins déclarent <code>part of</code> sans être inclus. <strong>Mesuré le 09/08 : 68 erreurs</strong>, quasi toutes de là (+ <code>lib/api/openApiTest.dart</code>) — moins que les ~183 relevées en juillet, mais <code>flutter analyze</code> global reste inexploitable comme feu vert : filtrer sur le dossier travaillé. <code>flutter build web</code> passe malgré elles, ces fichiers n'étant pas dans le graphe de compilation.</p>
<span class="src">security/audit-manager-app.md</span>
</article>
</div>
</section>
<!-- MIGRATION -->
<section class="col" style="--stripe: var(--info)">
<div class="col-head"><h2>Migration v3</h2><span class="count">5</span></div>
<div class="stack">
<article class="card" data-area="backend infra">
<div class="card-meta"><span class="tag">manager-service</span><span class="tag">reprise</span></div>
<h3>Backfill <code>StoragePath</code>/<code>SizeBytes</code> des 45 lignes</h3>
<p><strong>✅ Prérequis levé le 10/08</strong> : <code>Create</code> écrit désormais <code>StoragePath</code> (<code>pictures/{instanceId}/{resourceId}</code>) et <code>SizeBytes</code>, types URL exclus, et <code>manager-app</code> envoie la taille avant l'upload. Le backfill ne sera donc pas à refaire au prochain upload. Reste le backfill lui-même, en deux moitiés : <code>StoragePath</code> est un simple <code>UPDATE</code> SQL (chemin déterministe), seul <code>SizeBytes</code> exige de lister le bucket. ⚠️ <strong>Mesuré le 09/08 sur 45 lignes</strong> : 37 ont un blob, <strong>7 sont des types URL</strong> (Wikipedia, YouTube, <code>agenda.php</code>) sans aucun fichier — les inclure serait faux — et 1 est de type fichier sans URL. <code>SizeBytes</code> vaut <strong>0 partout</strong> : le quota de stockage ne veut rien dire pour personne.</p>
<span class="src">v2/media-storage-plan.md §5 — État mesuré · STATUS.md §1quater — Lot médias</span>
</article>
<article class="card" data-area="manager">
<div class="card-meta"><span class="tag">manager-app</span><span class="flag f-good">Gros gain</span></div>
<h3>Compression images — 2560 px / JPEG q82</h3>
<p>Aucune compression aujourd'hui : une photo 4K part entière. ~12× de gain sur le stockage <em>et</em> sur chaque consultation visiteur.</p>
<span class="src">v2/media-storage-plan.md</span>
</article>
<article class="card" data-area="backend">
<div class="card-meta"><span class="tag">manager-service</span></div>
<h3>Quota de stockage réellement appliqué</h3>
<p>Aujourd'hui contrôlé uniquement dans l'endpoint legacy. Pré-vol + contrôle autoritaire à <code>Create</code> avec la taille lue depuis Firebase + suppression du blob à <code>Delete</code>.</p>
<span class="src">v2/media-storage-plan.md</span>
</article>
<article class="card" data-area="backend manager">
<div class="card-meta"><span class="tag">manager-service</span></div>
<h3>Flag watermark sur Instance</h3>
<p>Remplace un <code>if</code> sur un id d'instance codé en dur. Doit remonter dans la config lue par manager-app, puisque le watermark s'applique côté client.</p>
<span class="src">v2/media-storage-plan.md</span>
</article>
<article class="card" data-area="infra">
<div class="card-meta"><span class="tag">GCP</span><span class="flag f-good">5 min</span></div>
<h3>Alerte de budget</h3>
<p>Bucket en Europe = aucun quota gratuit, facturation dès le premier octet. L'alerte est la vraie protection contre la surprise.</p>
<span class="src">v2/media-storage-plan.md</span>
</article>
</div>
</section>
<!-- BUGS -->
<section class="col" style="--stripe: var(--warn)">
<div class="col-head"><h2>Bugs ouverts</h2><span class="count">7</span></div>
<div class="stack">
<article class="card" data-area="backend visitapp">
<div class="card-meta"><span class="tag">back + visitapp</span><span class="flag f-critical">Bloquant produit</span></div>
<h3>Visite hors ligne quasi non fonctionnelle</h3>
<p>Le <code>switch</code> de collecte des ressources est commenté dans <code>Export</code> <em>et</em> côté visitapp. Une visite téléchargée n'embarque ni images d'articles ni audios.</p>
<span class="src">v2/offline-visit-plan.md</span>
</article>
<article class="card" data-area="visitapp">
<div class="card-meta"><span class="tag">visitapp</span><span class="flag f-warn">Fraîcheur</span></div>
<h3>Ressource modifiée jamais re-téléchargée</h3>
<p>Le filtre incrémental teste la présence du fichier, pas sa version. Une image remplacée dans le CMS ne remonte jamais sur le device. Invisible pour le client.</p>
<span class="src">v2/offline-visit-plan.md</span>
</article>
<article class="card" data-area="visitapp">
<div class="card-meta"><span class="tag">visitapp</span></div>
<h3>audio/mpeg absent de la table d'extensions</h3>
<p>La table mappe <code>audio/mp3</code>, qui n'est pas un MIME standard. Les MP3 atterrissent probablement en <code>.unknown</code>. À vérifier sur device.</p>
<span class="src">v2/offline-visit-plan.md</span>
</article>
<article class="card" data-area="visitapp">
<div class="card-meta"><span class="tag">visitapp</span></div>
<h3>Purge des fichiers obsolètes désactivée</h3>
<p>La liste est calculée puis le <code>deleteSync()</code> est commenté. Le stockage occupé sur le téléphone du visiteur ne diminue jamais.</p>
<span class="src">v2/offline-visit-plan.md</span>
</article>
<article class="card" data-area="visitapp">
<div class="card-meta"><span class="tag">visitapp</span></div>
<h3>Échecs de téléchargement silencieux</h3>
<p>Un fichier raté fait un <code>print</code> et passe. La visite est annoncée téléchargée alors qu'elle est incomplète.</p>
<span class="src">v2/offline-visit-plan.md</span>
</article>
<article class="card" data-area="visitapp">
<div class="card-meta"><span class="tag">mobile</span><span class="tag">M3</span></div>
<h3>meterZoneGPS ignoré</h3>
<p>Le rayon de déclenchement configuré par section n'a aucun effet — une constante en dur à 100 m le remplace.</p>
<span class="src">parity-manager-visitapp.md §2</span>
</article>
<article class="card" data-area="visitapp">
<div class="card-meta"><span class="tag">web</span><span class="tag">W1</span></div>
<h3>Fournisseur de carte ignoré en web</h3>
<p>Un client qui choisit « Google Hybrid » voit de l'OSM : <code>LeafletMap.tsx</code> a un <code>TileLayer</code> en dur.</p>
<span class="src">parity-manager-visitapp.md §3</span>
</article>
</div>
</section>
<!-- À TESTER -->
<section class="col" style="--stripe: var(--brand)">
<div class="col-head"><h2>À tester</h2><span class="count">6</span></div>
<div class="stack">
<article class="card" data-area="manager">
<div class="card-meta"><span class="tag">manager-app</span><span class="flag f-warn">Jamais lancé</span></div>
<h3>Écran Statistiques refondu + export PDF</h3>
<p>Écrit et analysé, jamais ouvert dans un navigateur. À vérifier sur données réelles : la règle mono-canal, le rendu des titres longs dans les barres. Sur le PDF : les accents, et le logo — <strong>s'il manque, c'est le CORS du bucket Firebase</strong>, le rapport doit se générer quand même. Et après <code>database update</code> : que les <em>instances</em> soient bien passées à 395 jours (pas seulement les plans), et que le job <code>visit-events-purge</code> déclenché à la main <strong>ne supprime rien</strong>.</p>
<span class="src">test-plan.md §8bis, §8ter, §8quater</span>
</article>
<article class="card" data-area="visitapp backend">
<div class="card-meta"><span class="tag">§21</span><span class="flag f-critical">En premier</span></div>
<h3>Visite hors ligne sur device</h3>
<p>15 cas : images d'articles, audios, extensions de fichiers, fraîcheur, dégradation des types non offline. Conditionne l'urgence de tous les bugs hors ligne.</p>
<span class="src">test-plan.md §21</span>
</article>
<article class="card" data-area="backend manager doc">
<div class="card-meta"><span class="tag">§18</span><span class="flag f-warn">Jamais exécuté</span></div>
<h3>Onboarding self-service complet</h3>
<p>Inscription, création d'instance, essai 14 j, Stripe, 9 emails Resend, job de cycle d'essai. Code écrit et compilé, aucun parcours joué. Reste l'alias <code>onboarding@</code> et le <code>whsec_</code> Stripe CLI.</p>
<span class="src">test-plan.md §18</span>
</article>
<article class="card" data-area="manager visitapp">
<div class="card-meta"><span class="tag">§19.13</span></div>
<h3>Chasse au trésor / carnaval — cas E</h3>
<p>Le scénario le plus complet : SectionMap + SectionParcours + géodéclenchement + questions + mode jeu, d'un seul tenant.</p>
<span class="src">test-plan.md §19.13</span>
</article>
<article class="card" data-area="manager visitapp">
<div class="card-meta"><span class="tag">§19</span></div>
<h3>Parité par type de section</h3>
<p>« Tout ce que je configure est-il visible ? » — le chapitre qui valide les 13 types sur les deux apps visiteur.</p>
<span class="src">test-plan.md §19</span>
</article>
<article class="card" data-area="infra">
<div class="card-meta"><span class="tag">§0</span></div>
<h3>Porte d'entrée build</h3>
<p>5 commandes, bloquant absolu. <code>flutter analyze</code> et <code>next dev</code> ne prouvent rien — seuls les vrais builds disent la vérité.</p>
<span class="src">test-plan.md §0</span>
</article>
</div>
</section>
<!-- PLANIFIÉ -->
<section class="col" style="--stripe: var(--ink-3)">
<div class="col-head"><h2>Planifié</h2><span class="count">23</span></div>
<div class="stack">
<article class="card" data-area="commercial" data-horizon="v1">
<div class="card-meta"><span class="tag">après prod</span><span class="tag">relationnel</span></div>
<h3>Reprendre contact avec Louise Smets — Musée d'Ixelles</h3>
<p><strong>Une fois la prod stabilisée</strong>, pas avant : on n'ouvre pas une conversation en montrant un produit en cours de bascule. Premier temps d'<strong>écoute</strong> — comprendre ce qu'est réellement son travail au quotidien et sur quoi elle bute — avant de parler de MyInfoMate. L'entraide se décide ensuite, pas dans le premier message. ⚠️ Rien n'est su de son rôle exact à ce stade : à ne pas supposer.</p>
<span class="src">STATUS.md §1quinquies — Après la bascule</span>
</article>
<article class="card" data-area="backend" data-horizon="v2">
<div class="card-meta"><span class="tag">priorité 1</span><span class="flag f-warn">V2</span></div>
<h3>Rapport de stats — envoi mensuel automatique</h3>
<p><strong>Le rapport à la demande est livré</strong> (07/08, généré côté client). Reste l'envoi automatique, seule partie qui exige vraiment le backend : personne n'a son navigateur ouvert le 1<sup>er</sup> à 6 h. QuestPDF + Hangfire, config destinataires/fréquence, et une méthode d'envoi <em>avec pièce jointe</em> — les 10 méthodes d'<code>IEmailService</code> sont des templates figés sans attachement. ⚠️ QuestPDF exige <code>libfontconfig1</code> + une police, absents du Dockerfile : ça marche en local sous Windows et casse dans le conteneur.</p>
<span class="src">plan-import-ia-stats-subsides.md §1</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">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 &amp; 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>
</article>
<article class="card" data-area="backend">
<div class="card-meta"><span class="tag">priorité 2</span><span class="flag f-good">Exécuté 10/08 — 2150 morceaux</span></div>
<h3>RAG sur le contenu CMS — vector store, jobs, outil <code>SearchKnowledge</code></h3>
<p>Persona simple : réponses sourcées sur le contenu existant. Pas d'ingestion documentaire, pas d'OCR, pas de TTS — c'est ce qui le rend tenable en V1.</p>
<p><strong>Livré le 10/08</strong> : <code>IngestionService</code> (chargement des collections filles par sous-type, un jeu de morceaux par langue), serveur Hangfire dédié à 2 workers, garde <code>AiTokensPerMonth &gt; 0</code><code>plan-starter</code> est à 0 — et <code>SearchKnowledge</code> ajouté aux outils d'<code>AssistantService</code> plutôt qu'un <code>/ask</code> concurrent, avec sa règle dans les <strong>4</strong> prompts.</p>
<p><strong>Joué contre la vraie base et la vraie API le 10/08 : 52 sections → 2150 morceaux en 53 s.</strong> Deux défauts que seule l'exécution pouvait montrer, corrigés dans la foulée. <strong>1)</strong> Les gabarits de <code>LanguageInit</code> étaient indexés : les <em>cinq</em> premiers résultats d'une question en néerlandais étaient des <code>NL - Title NL - Description</code>, le bonus de langue suffisant à les faire passer devant du vrai contenu français — le cas même que le cross-lingue devait servir. <strong>2)</strong> Le même paragraphe occupait trois des cinq résultats, au même score : <code>DistinctBy(Text)</code> avant le <code>Take</code>. ⚠️ Le post-filtrage HNSW reste non éprouvé <strong>à plusieurs instances</strong> — il n'y en a qu'une en base locale, soit le cas où le problème ne se voit pas.</p>
<p>Les deux angles morts du découpage sont fermés : HTML retiré avant l'embedding, et ligne trop longue recoupée à la fin de phrase — sans quoi un article dépassait l'entrée max de <code>gemini-embedding-001</code> et l'échec emportait les 50 morceaux du lot.</p>
<p><strong>Déclenchement unifié</strong> : l'<code>Enqueue</code> par contrôleur et un intercepteur EF ont coexisté une demi-journée. Retenu : <code>SectionIndexingInterceptor</code> seul — les 5 sous-contrôleurs totalisaient <strong>30 <code>SaveChanges</code> et 0 <code>Enqueue</code></strong>, donc ajouter 40 points d'intérêt à une carte ne réindexait rien.</p>
<span class="src">STATUS.md §1quater — lot 3 · v2/rag-indexing-trigger-decision.md · v2/rag-pgvector-integration-plan.md §3, §4</span>
</article>
<article class="card" data-area="manager backend" data-horizon="v1">
<div class="card-meta"><span class="tag">manager-app</span><span class="flag f-warn">Front fait · backend à faire</span></div>
<h3>RGPD &amp; conformité — lot J, dernier avant la bascule</h3>
<p><strong>Reporté en fin de backlog le 11/08.</strong> Tenable <em>parce que</em> la porte « rien en prod avant la fin du backlog » tient : d'ici là la journalisation ne tourne que sur des bases de développement, aucun visiteur réel n'est concerné. Si un déploiement anticipé était décidé, ce lot redeviendrait bloquant.</p>
<p><strong>Déjà fait le 11/08</strong> : CGU §8 réécrites (elles décrivaient un service qui ne collectait que des statistiques anonymes, citaient des durées de plans abandonnés, et ne mentionnaient <em>aucun sous-traitant</em> alors que la question du visiteur part chez Google), texte d'information visiteurs en FR/NL/EN, et purge à 90 jours active sans condition de configuration — une durée écrite dans un contrat n'est pas un réglage.</p>
<p><strong>Reste</strong> : la <strong>table d'agrégats de thèmes</strong> (les CGU promettent que les regroupements survivent à la purge, or <code>ThemeId</code> est une colonne de la ligne supprimée — au 91<sup>e</sup> jour le client perdrait tout), le job de regroupement, un <strong>interrupteur de collecte par instance</strong> (⚠️ le client est responsable de traitement mais ne peut pas refuser la collecte), la mention à afficher côté visiteur, la <strong>relecture juridique du §8</strong> et la vérification des conditions réelles de Google.</p>
<span class="src">v1-plan.md — lot J · cgu-myinfomate.md §8 · mention-information-visiteurs.md</span>
</article>
<article class="card" data-area="manager visitapp" data-horizon="v1">
<div class="card-meta"><span class="tag">SectionParcours</span><span class="flag f-critical">Perte de travail client</span></div>
<h3>Parcours — sauvegarde au fil de l'eau</h3>
<p>Chaque popup travaille sur une copie JSON profonde et ne remonte qu'à la fermeture : <strong>fermer la fenêtre du parcours par la croix jette les questions saisies dessous</strong>, sans un mot. C'est le seul des trois défauts de cet écran qui fait perdre du travail au client aujourd'hui.</p>
<p><strong>Indépendant de l'arbitrage A/B</strong> — à faire sans attendre la refonte de la forme. La maquette prévoit déjà son témoin : « Enregistré · il y a 3 s » en pied de fenêtre.</p>
<span class="src">todo-features.md — SectionParcours · v1-plan.md — DB2</span>
</article>
<article class="card" data-area="manager visitapp" data-horizon="v1">
<div class="card-meta"><span class="tag">manager-app</span><span class="flag f-warn">Arbitrage en attente</span></div>
<h3>Parcours — sortir de l'empilement de popups</h3>
<p>Cinq surfaces empilées pour poser une question sur une étape et l'écrire en NL. <strong>Option A recommandée</strong> (fenêtre unique, rail d'étapes toujours visible, profondeur max 2) : le seul gain réel de l'option B était une colonne d'aperçu visiteur, <em>déjà couverte</em> par l'aperçu de conversation du Guide IA. Maquette rapatriée dans <code>claude design/sectionparcours-refonte-flux.html</code>.</p>
<p>Extraire d'abord <code>ParcoursFields</code> / <code>EtapeFields</code> / <code>QuestionFields</code> en widgets autonomes : sert A et B à l'identique.</p>
<span class="src">todo-features.md — SectionParcours · v1-plan.md — DB4, lien L18</span>
</article>
<article class="card" data-area="manager" data-horizon="v1">
<div class="card-meta"><span class="tag">manager-app</span><span class="flag f-warn">Jamais ouvert</span></div>
<h3>Écran Guide IA — vérification à l'œil</h3>
<p>La coquille à onglets, l'aperçu de conversation et la carte de connaissance compilent (<code>flutter build web</code> ✅) mais <strong>n'ont jamais été affichés dans un navigateur</strong> — le défaut récurrent de ce tableau. Exige un <code>manager-service</code> qui tourne et une session ouverte : <code>flutter run -d chrome</code>, puis comparaison côte à côte avec <code>claude design/guide-ia-screen.html</code>.</p>
<span class="src">v1-plan.md — DB5</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-good">Tranché 11/08</span></div>
<h3>Nettoyage des champs backend</h3>
<p><code>SectionMap.MapResourceId</code> → à <em>renommer</em> en <code>IconResourceId</code>, pas à supprimer : il est branché de bout en bout sous un nom trompeur. Le renommage <strong>est</strong> l'écart (g) de la migration — le faire ici le fait disparaître là-bas.</p>
<p><code>SectionEvent.ParcoursIds</code><strong>supprimer</strong>. Vérifié le 11/08 : lu ni écrit nulle part, commentaire (<code>// Liens vers GeoPoints</code>) contredisant son nom, et le lien événement↔parcours existe déjà en sens inverse via <code>GuidedPath.SectionEventId</code>. Décisif : il est sur <code>SectionEvent</code> et non sur <code>ProgrammeBlock</code>, donc <strong>incapable du cas « parcours de tel jour »</strong> pour lequel on le gardait.</p>
<p><code>QuestionType</code><strong>ne pas toucher au schéma</strong> : le trio TextLibre/Digicode/ExpectedAnswer n'existe pas dans le code, et le digicode est un comportement dérivé. Reste une propreté front. <strong>Sort du lot « geler le schéma »</strong>.</p>
<span class="src">STATUS.md §6bis et §1sexies · v1-plan.md — lot B</span>
</article>
<article class="card" data-area="backend manager" data-horizon="v2">
<div class="card-meta"><span class="tag">priorité 2</span><span class="flag f-warn">V2</span></div>
<h3>Ingestion documentaire + OCR</h3>
<p>PdfPig avec cascade OCR Gemini (~0,03 $ / 100 pages). Word, PowerPoint, images. Legacy <code>.doc</code>/<code>.ppt</code> refusés à l'upload.</p>
<span class="src">v2/rag-pgvector-integration-plan.md</span>
</article>
<article class="card" data-area="backend" data-horizon="v2">
<div class="card-meta"><span class="tag">priorité 3</span><span class="flag f-warn">V2</span></div>
<h3>TTS pré-généré — audioguide multilingue</h3>
<p>Le produit que les musées achètent depuis toujours, à coût marginal quasi nul. Aucun concurrent de cette gamme ne le fait.</p>
<span class="src">v2/tts-pregenerated-plan.md</span>
</article>
<article class="card" data-area="backend visitapp" data-horizon="v2">
<div class="card-meta"><span class="tag">priorité 4</span><span class="flag f-warn">V2</span></div>
<h3>Génération de visites à la demande</h3>
<p>« Un parcours de 45 min pour familles ». Marche sur le smartphone du visiteur, aucun hardware. Vendable aux villes comme balade sonore IA.</p>
<span class="src">plan-import-ia-stats-subsides.md §3</span>
</article>
<article class="card" data-area="manager backend" data-horizon="v2">
<div class="card-meta"><span class="tag">guide IA</span><span class="flag f-warn">V2</span></div>
<h3>Guides multiples &amp; guide thématique</h3>
<p>Plusieurs personas, assignables à une section ou une configuration. Le <em>modèle</em> part en V1 (liste, pas champ unique) ; le comportement attend — qui répond depuis l'accueil, et sur quel périmètre de contenu, est une vraie question de conception.</p>
<span class="src">v2/tts-pregenerated-plan.md — PersonaConfig</span>
</article>
<article class="card" data-area="visitapp infra" data-horizon="v1">
<div class="card-meta"><span class="tag">visitapp-web</span><span class="flag f-critical">Bloque l'offre Essentiel</span></div>
<h3>Déployer le visiteur web — <code>app.myinfomate.be</code></h3>
<p>L'app <strong>existe</strong> : build ✅, 13 types de section rendus, assistant IA inclus depuis le 06/08. Il ne manque que le Dockerfile et l'entrée Traefik dans le compose. Le plan Essentiel vendu en self-service est web-only — sans ce déploiement, l'onboarding vend un produit inaccessible.</p>
<span class="src">architecture-web-saas.md — À faire</span>
</article>
<article class="card" data-area="visitapp manager" data-horizon="v2">
<div class="card-meta"><span class="tag">tablet-app</span><span class="flag f-warn">V2</span></div>
<h3>Kiosk web (Flutter Web)</h3>
<p>Extension web de <code>tablet-app</code> pour les clients ne voulant pas d'installation native. <strong>Reporté en V2 le 07/08</strong> : aucun client demandeur, et la cible native ne compile même plus — il faudrait réparer ce build avant d'en dériver une version web.</p>
<span class="src">architecture-web-saas.md — Kiosk web</span>
</article>
<article class="card" data-area="backend manager visitapp" data-horizon="v2">
<div class="card-meta"><span class="tag">produit</span><span class="flag f-warn">V2</span></div>
<h3>SectionForm — formulaires personnalisés</h3>
<p>Nouveau type de section + table de réponses anonymes, constructeur de formulaire et page de résultats agrégés. <strong>Reporté en V2 le 07/08</strong> : purement additif, aucun impact sur le schéma existant — rien n'oblige à le faire passer avec la migration.</p>
<span class="src">todo-features.md — SectionForm</span>
</article>
<article class="card" data-area="backend manager visitapp" data-horizon="v2">
<div class="card-meta"><span class="tag">produit</span><span class="flag f-warn">V2</span></div>
<h3>Ressource 360° — images panoramiques</h3>
<p>Une valeur d'enum (<code>Panorama360</code>) et un branchement dans l'affichage des ressources : s'affiche partout où une ressource s'affiche, sans toucher aux sections. <strong>Reporté en V2 le 07/08</strong> — le plus petit des trois, à reprendre en premier.</p>
<span class="src">todo-features.md — Ressource 360°</span>
</article>
<article class="card" data-area="backend manager visitapp" data-horizon="v2">
<div class="card-meta"><span class="tag">produit</span><span class="flag f-warn">V2</span></div>
<h3>AR image tracking (Mind AR)</h3>
<p>Modèle <code>ArAnchor</code>, microservice Node de compilation des <code>.mind</code>, et remplacement du scanner visiteur par une WebView unifiée QR + image. Le plus lourd des trois, fort effet démo. <strong>Reporté en V2 le 07/08</strong> — ni la migration ni la mise en prod n'en dépendent.</p>
<span class="src">todo-features.md — AR</span>
</article>
<article class="card" data-area="manager">
<div class="card-meta"><span class="tag">produit</span></div>
<h3>Écran d'audit log &amp; gestion des users</h3>
<p>Backend fait, front absent. Users : scoping backend fait, manque le plafond de 5 et le compteur dans l'UI.</p>
<span class="src">todo-features.md</span>
</article>
<article class="card" data-area="manager visitapp" data-horizon="v1">
<div class="card-meta"><span class="tag">SectionParcours</span><span class="flag f-good">Petit · arbitré 09/08</span></div>
<h3>PDF en média d'étape</h3>
<p>Retenu <em>à la place</em> de faire de SectionParcours un conteneur de sections — idée écartée le 09/08 : la moitié des types (Agenda, Météo, Event) n'a pas de sens dans un point de parcours, et le lien vers une section existante existe déjà là où il est cohérent (<code>baseSectionMapId</code>). Le vrai manque tient à un type de ressource : une étape accepte déjà image, vidéo et audio, <strong>PDF est le seul absent</strong>. Trois points : liste de types paramétrable côté étape, cas <code>PDF</code> dans <code>getElementForResource</code>, cas PDF dans <code>ResourceViewer.tsx</code>. Sans les deux derniers, le PDF s'afficherait en image cassée — le bug corrigé pour la vidéo le 09/08.</p>
<span class="src">todo-features.md — Parcours guidés</span>
</article>
<article class="card" data-area="visitapp" data-horizon="v2">
<div class="card-meta"><span class="tag">polish</span><span class="flag f-warn">V2</span></div>
<h3>Talking head — avatar animé</h3>
<p>Trois frames par persona, lipsync sur les timestamps Google TTS. Quelques jours, effet démo. Bouche-trou entre deux chantiers.</p>
<span class="src">v2/talking-head-plan.md</span>
</article>
<article class="card" data-area="doc" data-horizon="v2">
<div class="card-meta"><span class="tag">XR</span><span class="flag f-warn">V2 · subventionné</span></div>
<h3>Ray-Ban Meta &amp; Meta Quest</h3>
<p>Pilote presse uniquement, quand le SDK sort de preview. 90 % de l'expérience est livrable en audio smartphone — profil d'appel à projets, pas de ligne produit.</p>
<span class="src">roadmap.md — section XR</span>
</article>
</div>
</section>
<!-- BASCULE PROD -->
<section class="col" style="--stripe: var(--gate)">
<div class="col-head"><h2>Bascule prod</h2><span class="count">10</span></div>
<div class="stack">
<article class="card" data-area="backend">
<div class="card-meta"><span class="tag">MigrationController</span><span class="flag f-critical">Silencieux</span></div>
<h3>Instance : 4 champs migrés sur ~35</h3>
<p>Le plus dangereux, parce qu'il <em>ne produit aucune erreur</em> : <code>SubscriptionPlanId</code> null → quotas à 0, <code>PublicApiKey</code> null → <strong>les apps visiteur ne s'authentifient plus</strong>, <code>WebSlug</code> null → visitapp-web injoignable, <code>IsMobile</code>/<code>IsTablet</code>/<code>IsWeb</code>/<code>IsAssistant</code> à false. La migration se déclare réussie et le lieu est inaccessible.</p>
<span class="src">MigrationController.cs:106-112 · STATUS.md §1quinquies c</span>
</article>
<article class="card" data-area="backend">
<div class="card-meta"><span class="tag">MigrationController</span><span class="flag f-critical">Perte de données</span></div>
<h3>11 types de section sur 13</h3>
<p><code>BuildSection</code> a été écrit avant <code>SectionEvent</code> et <code>SectionParcours</code> : les deux tombent dans le <code>default</code>, une ligne dans le rapport d'erreurs et la section n'est pas migrée. Ce sont précisément les types du scénario carnaval.</p>
<span class="src">MigrationController.cs:458-613 · §1quinquies a</span>
</article>
<article class="card" data-area="backend">
<div class="card-meta"><span class="tag">MigrationController</span><span class="flag f-critical">Perte de données</span></div>
<h3>Collections filles jamais remplies</h3>
<p><code>QuizQuestions = new()</code>, <code>EventAgendas = new()</code>, aucun <code>GuidedPath</code>/<code>GuidedStep</code> : un quiz arrive sans ses questions, un agenda sans ses événements. Et <code>Role</code> est forcé à <code>ContentEditor</code><strong>plus aucun administrateur</strong> après la bascule.</p>
<span class="src">MigrationController.cs:225, :526, :591 · §1quinquies b, d</span>
</article>
<article class="card" data-area="backend">
<div class="card-meta"><span class="tag">MigrationController</span><span class="flag f-warn">Robustesse</span></div>
<h3>Aucune transaction globale</h3>
<p>Neuf étapes, un <code>SaveChangesAsync</code> chacune, et un <code>catch</code> qui renvoie <strong>200 OK</strong> avec l'erreur dans le corps. Un échec au milieu laisse la base à moitié remplie ; la reprise ne tient qu'aux tests d'idempotence — qui existent et sont corrects. Idem : <code>StoragePath</code> et les colonnes IA ne sont pas renseignés, et <code>SizeBytes</code> sort d'un <code>HEAD</code> dont l'échec est avalé.</p>
<span class="src">MigrationController.cs:68-86, :172-181 · §1quinquies e, h</span>
</article>
<article class="card" data-area="backend infra">
<div class="card-meta"><span class="tag">étape 15</span><span class="flag f-good">Sans risque</span></div>
<h3>Dry run + comparaison des comptages</h3>
<p><code>dryRun=true</code> n'écrit rien. Rejouer sur l'export Mongo déjà présent dans <code>manager-service/migration-data/</code> et comparer entité par entité aux comptages Mongo. C'est ce qui révèle les quatre cartes ci-dessus sur des données réelles plutôt que par lecture de code.</p>
<span class="src">STATUS.md §1quinquies — étape 15</span>
</article>
<article class="card" data-area="infra">
<div class="card-meta"><span class="tag">étape 16-17</span><span class="flag f-critical">Après rotation des secrets</span></div>
<h3>Créer l'environnement prod Postgres</h3>
<p><code>Deployment/Dockerfile.postgres</code> (digest épinglé), compose prod, Traefik, <strong>port 5432 fermé</strong>. Puis <code>dotnet ef database update</code> sur la base vide et vérifier que <code>postgis</code> <em>et</em> <code>vector</code> répondent. Ne pas créer la prod avec les clés encore committées.</p>
<span class="src">STATUS.md §1quinquies — étapes 16-17</span>
</article>
<article class="card" data-area="infra">
<div class="card-meta"><span class="tag">étape 18</span><span class="flag f-critical">Filet du jour J</span></div>
<h3>pg_dump avant / après + cron quotidien</h3>
<p>Noté comme « à faire » depuis juillet et jamais fait. Ici ça cesse d'être une bonne pratique : c'est la seule chose qui permette de recommencer si la bascule tourne mal.</p>
<span class="src">STATUS.md §4 · §1quinquies — étape 18</span>
</article>
<article class="card" data-area="backend infra">
<div class="card-meta"><span class="tag">étape 19</span></div>
<h3>Rejouer pour de vrai, instance par instance</h3>
<p>Le paramètre <code>instanceId</code> existe précisément pour ça. Commencer par la plus petite instance : si quelque chose casse, ça casse sur le plus petit périmètre possible.</p>
<span class="src">STATUS.md §1quinquies — étape 19</span>
</article>
<article class="card" data-area="backend manager visitapp">
<div class="card-meta"><span class="tag">étape 20</span><span class="flag f-warn">Le vrai test</span></div>
<h3>Vérifications post-bascule</h3>
<p>Login manager-app, une app visiteur avec sa clé API, un parcours, une carte, un PDF, <strong>un quiz avec ses questions</strong>. Les défauts « silencieux » ne se voient qu'ici — pas dans le rapport de migration.</p>
<span class="src">STATUS.md §1quinquies — étape 20</span>
</article>
<article class="card" data-area="infra">
<div class="card-meta"><span class="tag">étape 21-22</span></div>
<h3>DNS, Mongo en lecture seule, retour arrière</h3>
<p>Bascule DNS/API, Mongo conservé en lecture seule quelques jours, et <strong>le plan de rollback écrit avant le jour J</strong> — un retour arrière qu'on improvise à 23 h n'est pas un retour arrière.</p>
<span class="src">STATUS.md §1quinquies — étapes 21-22</span>
</article>
</div>
</section>
</div>
<section class="done">
<h2>Fait récemment</h2>
<p>Seize chantiers clos entre le 5 et le 10 août 2026.</p>
<div class="done-grid">
<div class="done-item">
<strong>Les maquettes sortent du compte Claude et entrent dans le repo</strong>
<span>Cause diagnostiquée d'un défaut qui se répétait : les trois maquettes vivaient dans des artifacts d'un <strong>autre compte</strong>, illisibles depuis la machine de dev — chaque session redessinait. D'où un écran Guide IA « livré » à la moitié de sa spec et des stats d'assistant à zéro. Le HTML est rapatrié dans <code>claude design/</code>, JS interactif compris. Même découpage que ce tableau : <strong>contenu dans le repo, diffusion par artifact</strong>. Constat en ouvrant les fichiers : Guide IA et Statistiques déclarent des tokens <strong>rigoureusement identiques</strong> — c'était déjà un design system, jamais porté en Dart — et <code>--brand #264863</code> <em>est</em> <code>kPrimaryColor</code>.</span>
</div>
<div class="done-item">
<strong>Socle visuel — 11 tailles de police deviennent 11 rôles</strong>
<span>Mesuré dans <code>lib/Components/</code> : onze tailles différentes (9 à 25 px), aucune échelle, <code>constants.dart</code> portant encore <code>// TO FILL WITH CORRECT COLOR</code> et deux styles. L'échelle, les espacements et les rayons sont extraits du CSS des maquettes ; <strong>les couleurs restent celles de l'app</strong> — un écran neuf se fond dans manager-app, il n'y ouvre pas une seconde palette. Seules les valeurs sans équivalent existant sont reprises : remplissages discrets, bordures, gris atténué, ambre. Pas de thème sombre. La reprise des écrans existants reste son propre chantier, pas un effet de bord.</span>
</div>
<div class="done-item">
<strong>Écran Guide IA — la coquille à onglets, et ce que le guide sait vraiment</strong>
<span>L'écran passe de cinq cartes en colonne à la structure de la maquette : deux onglets, deux colonnes. <strong>L'aperçu de conversation interroge le vrai <code>/api/AI/chat</code></strong>, pas une simulation — au passage, <code>AIApi</code> était dans le client généré mais <strong>exposé nulle part</strong> dans <code>client.dart</code>, où les 18 autres façades sont. Câblé. Nouveau <code>GET /api/Ai/knowledge/{id}</code> pour la carte « Ce que connaît votre guide » : tout est agrégé sur <code>ContentEmbedding</code>, donc sur ce qui est <em>réellement indexé</em> — compter les sections publiées donnerait un chiffre plus flatteur et faux, une section désactivée étant purgée de l'index. Le « points d'intérêt » de la maquette devient « morceaux de contenu » : les points d'une carte sont indexés dans le texte de leur SectionMap, les compter à part répondrait à une autre question. 34 clés i18n FR/EN/NL, <code>dotnet test</code> 130/130, <code>flutter build web</code> ✅.</span>
</div>
<div class="done-item">
<strong>Le changement de plan donne enfin ce qu'il vend</strong>
<span>Trois défauts qui se tenaient la main. <strong>1)</strong> <code>Updateinstance</code> changeait <code>SubscriptionPlanId</code> mais ne recopiait jamais les quotas du nouveau plan — <code>CreateInstance</code> le faisait, pas lui : passer un client de Starter à Premium ne lui donnait ni stockage ni jetons IA. <strong>2)</strong> <code>CheckQuota</code> ne bloquait que si <code>quota &gt; 0</code> : à 0 jeton, <strong>ni blocage ni compteur</strong> — l'ambiguïté vient du code lui-même, où <code>0</code> veut dire <em>illimité</em> pour le stockage et <em>pas d'IA</em> pour les jetons. Une instance Starter avec <code>IsAssistant</code> à true consommait donc gratuitement et sans compteur, et <strong>toute instance migrée depuis Mongo arrive à 0</strong> (écart c du §1quinquies). Corrigé en 403 avant tout appel au modèle. <strong>3)</strong> <code>BackfillInstanceAsync</code> n'était appelé par personne : branché automatiquement quand <code>AiTokensPerMonth</code> passe de 0 à une valeur, plus un bouton de relance <strong>réservé au SuperAdmin</strong> dans l'écran Guide IA — outil de réparation, pas fonctionnalité : exposé au client il serait cliqué à chaque réponse décevante, pour un coût d'embedding complet et sans rien améliorer. <code>IBackgroundJobClient</code> injecté au lieu de la façade statique, qui lève sans <code>JobStorage</code> et cassait les tests. 130/130.</span>
</div>
<div class="done-item">
<strong>La dimension 768 n'est plus un pari</strong>
<span><code>IEmbeddingService</code> + <code>GoogleEmbeddingService</code> (batch par 50), et surtout <strong>vérifiés contre l'API réelle</strong>. Trois choses qu'aucun plan ne disait : le modèle sort en <strong>3072 dimensions par défaut</strong><code>dimensions: 768</code> est obligatoire ; les vecteurs réduits <strong>ne sont pas normalisés</strong> (norme mesurée 0,578, pas 1) et sans normalisation côté client le classement cosinus serait faussé <em>sans lever d'erreur</em> ; et Google ne renvoie pas le champ <code>index</code> du contrat OpenAI. Recherche cross-lingue jouée en base : question NL → contenu NL 0,8145, FR pertinent 0,7982, FR hors-sujet 0,4818. L'écart NL/FR n'étant que de 0,016, le <strong>bonus de score par langue</strong> est bien nécessaire.</span>
</div>
<div class="done-item">
<strong>L'historique des stats passe à 13 mois pour tous</strong>
<span>L'axe « durée » disparaît du découpage commercial : <code>HasAdvancedStats</code> devient le seul différenciateur stats, et <strong>l'export PDF est inclus dès qu'il y a les stats</strong>. Motif : un dossier de subside est annuel — plafonné à 30 jours le rapport n'était envoyable à personne. Et la landing ne promettait aucune durée (la chaîne « 30 jours » existe dans <code>translations.ts</code> mais n'est affichée nulle part). Migration <code>UnifyStatsRetentionTo13Months</code> — elle met à jour les plans <em>et reprend les instances en SQL</em>, sans quoi les clients actuels seraient restés à 30 jours : c'est <code>Instance.StatsHistoryDays</code> que lit le contrôleur. <code>dotnet test</code> 124/124. ⛔ La purge associée est codée mais <strong>inactive</strong> jusqu'à la mise en place du pg_dump.</span>
</div>
<div class="done-item">
<strong>Configuration des parcours — 4 correctifs</strong>
<span>Relecture du 09/08 de l'écran refondu le 06/08. Le plus sérieux : un parcours créé sans toucher à la question de progression <em>affichait</em> « Dans l'ordre » et <em>enregistrait</em> « Libre ». Les 12 tests de <code>progression_mode</code> ne pouvaient pas le voir — ils couvrent la relecture des booléens, pas le DTO fabriqué par la popup. Aussi : vidéos et audios d'étape rendus en dur comme des images dans les deux apps visiteur, « Quelle ambiance ? » restée en case à cocher, menu « Carte de base » disparaissant sans un mot. À valider — test-plan §19.13 cas 0.</span>
</div>
<div class="done-item">
<strong>Le schéma du RAG est en base</strong>
<span>Image <code>postgis + pgvector</code> (0.8.6, base <strong>épinglée par digest</strong>), table <code>ContentEmbedding</code> avec index HNSW, 7 colonnes IA/stockage sur <code>Resource</code>, table <code>VisitorQuestion</code>. 62 migrations, <code>dotnet test</code> 124/124, données intactes. Deux surprises : la base locale avait <strong>une migration de retard</strong> — les colonnes <code>Guide*</code> du lot 1 n'existaient pas, toute requête sur <code>Instances</code> aurait planté ; et un <strong>warning de collation</strong> (glibc 2.36→2.31) qui désalignait les index texte, corrigé par <code>REINDEX</code> puis <code>REFRESH COLLATION VERSION</code> — dans cet ordre. D'où l'épinglage par digest.</span>
</div>
<div class="done-item">
<strong>Le guide lit enfin sa configuration</strong>
<span>L'écran Guide IA ne pilote plus dans le vide. En ouvrant le fichier : <strong>quatre</strong> blocs de prompt et non deux — le relevé s'était arrêté au scope configuration. Les quatre lisent le nom, la personnalité et les messages de repli du client ; le ton imposé (« tu es chaleureux ») est retiré, la politique de repli divergente de la ligne 550 réconciliée. <strong>Deux prompts sur quatre n'avaient aucune règle hors-sujet</strong> — sur web et mobile, un visiteur pouvait demander une recette de tarte. Le tirage du repli est fait côté serveur pour que la phrase du client soit reprise mot pour mot. <code>dotnet test</code> 124/124, jamais vu tourner.</span>
</div>
<div class="done-item">
<strong>Écran Statistiques refondu, export PDF compris</strong>
<span>Les 7 défauts corrigés, <em>aucun changement backend</em> : barres horizontales monochromes, filtres sur une seule ligne avec le volume de chaque canal, règle mono-canal, 4 KPI portant chacun leur variation, bandeau « à retenir », courbe en aire. La période précédente s'obtient en rappelant le même endpoint. <strong>Le rapport PDF se génère dans le navigateur</strong> (paquet <code>pdf</code> Dart déjà présent) et partage les valeurs calculées de l'écran — un chiffre ne peut pas diverger entre l'écran et le document envoyé à la commune. Le test de génération a attrapé deux plantages qui seraient sortis au premier clic. Au passage : <code>VisitEvent</code> porte bien l'<code>AppType</code>, le canal vocal est branché ; et <strong>deux puces du sommaire promettaient des données inexistantes</strong> (parcours terminés, questions au guide IA), retirées.</span>
</div>
<div class="done-item">
<strong>Les builds sont débloqués</strong>
<span>3 apps Flutter + visitapp-web + <code>dotnet test</code> 124/124. Un seul fichier sans <code>// @dart=2.18</code> cassait les trois apps Flutter d'un coup.</span>
</div>
<div class="done-item">
<strong>Bug backend révélé par les tests</strong>
<span><code>BuildAuditEntries()</code> modifiait le <code>ChangeTracker</code> pendant son énumération — exception sur toute écriture d'entité auditée. Invisible tant que les tests ne compilaient plus.</span>
</div>
<div class="done-item">
<strong>Assistant IA en web + alignement mobile</strong>
<span>Débloque l'add-on IA sur le plan Essentiel, qui est web-only. Suggestions dérivées du contenu réel, zéro configuration client.</span>
</div>
<div class="done-item">
<strong>Refonte de la progression des parcours</strong>
<span>9 booléens sur 3 niveaux remplacés par 3 questions. <code>IsStepLocked</code> supprimé : il rendait une étape définitivement infranchissable même après réussite.</span>
</div>
<div class="done-item">
<strong>Centrage de carte, horaires, icônes</strong>
<span>M1, M2 et W2 corrigés — des champs que le client saisissait sans qu'ils aient le moindre effet.</span>
</div>
<div class="done-item">
<strong>Audit de cohérence de la doc</strong>
<span>Le LLM annoncé était Claude à 4 endroits (c'est Gemini), l'infra email « inexistante » (Resend est en place), et deux docs se contredisaient sur l'état des builds.</span>
</div>
<div class="done-item">
<strong>CGU reprises et landing corrigée</strong>
<span>Les CGU décrivaient des plans abandonnés (Starter/Standard/Premium 69/99/199 €). Alignées sur la landing, avec un quota IA défini en tokens renvoyant au back-office plutôt qu'un chiffre figé dans un contrat. Landing : « req/mois » → « questions de visiteurs / mois » en 4 langues.</span>
</div>
<div class="done-item">
<strong>Écran Guide IA — onglet Configuration livré</strong>
<span>Migration additive (4 colonnes nullable), champs sur <code>Instance</code>, client généré étendu à la main, écran avec 35 clés i18n FR/EN/NL. <code>dotnet build</code> et <code>flutter build web</code> passent. Menu conditionné à <code>isAssistant</code>, le même drapeau que la garde d'<code>AiController</code>.</span>
</div>
<div class="done-item">
<strong>Trois plans écrits, deux écrans maquettés</strong>
<span>Médias &amp; stockage, visite hors ligne, écran Guide IA, écran Statistiques. Le découpage V1/V2 est tranché : le RAG sur contenu CMS part en V1, l'ingestion documentaire en V2. <strong>Les deux écrans maquettés sont désormais implémentés.</strong></span>
</div>
</div>
</section>
<footer class="foot">
<span>Source de vérité : <code>DOCS/STATUS.md §1ter</code> · chantier Guide IA : <code>§1quater</code></span>
<span>Maquettes : <a href="https://claude.ai/code/artifact/c43fe02d-3283-48c2-992f-d687e18acdfa">Guide IA</a> · <a href="https://claude.ai/code/artifact/052fe8f3-0288-4847-b63f-4c7de06068ec">Statistiques</a></span>
<span>Règle de découpage : ce qui touche aux données écrites part avec la migration, ce qui est additif attend.</span>
<span>Rien ne part en prod avant que ce tableau soit vide.</span>
</footer>
</div>
<script>
(function () {
var areaButtons = Array.prototype.slice.call(document.querySelectorAll('[data-filter]'));
var horizonButtons = Array.prototype.slice.call(document.querySelectorAll('[data-horizon-filter]'));
var cards = Array.prototype.slice.call(document.querySelectorAll('.card'));
var cols = Array.prototype.slice.call(document.querySelectorAll('.col'));
var stats = Array.prototype.slice.call(document.querySelectorAll('.stat'));
var done = document.querySelector('.done');
var emptyMsg = document.querySelector('.empty-msg');
var area = 'all';
var horizon = 'all';
function apply() {
cards.forEach(function (card) {
var areas = (card.getAttribute('data-area') || '').split(/\s+/);
var cardHorizon = card.getAttribute('data-horizon') || 'v1';
var matchesArea = area === 'all' || areas.indexOf(area) !== -1;
var matchesHorizon = horizon === 'all' || cardHorizon === horizon;
card.classList.toggle('hidden', !(matchesArea && matchesHorizon));
});
var total = 0;
cols.forEach(function (col, i) {
var visible = col.querySelectorAll('.card:not(.hidden)').length;
total += visible;
var count = col.querySelector('.count');
if (count) { count.textContent = visible; }
col.classList.toggle('hidden', visible === 0);
var stat = stats[i];
if (stat) {
var n = stat.querySelector('.n');
if (n) { n.textContent = visible; }
stat.classList.toggle('stat-muted', visible === 0);
}
});
// « Fait récemment » n'a pas d'horizon : c'est du passé, pas du reste-à-faire.
if (done) { done.classList.toggle('hidden', horizon !== 'all'); }
if (stats[5]) { stats[5].classList.toggle('hidden', horizon !== 'all'); }
if (emptyMsg) { emptyMsg.classList.toggle('hidden', total !== 0); }
}
function bind(buttons, set) {
buttons.forEach(function (btn) {
btn.addEventListener('click', function () {
buttons.forEach(function (b) { b.setAttribute('aria-pressed', String(b === btn)); });
set(btn);
apply();
});
});
}
bind(areaButtons, function (btn) { area = btn.getAttribute('data-filter'); });
bind(horizonButtons, function (btn) { horizon = btn.getAttribute('data-horizon-filter'); });
apply();
})();
</script>