Update docs, kanban etc

This commit is contained in:
Thomas Fransolet 2026-09-04 16:48:04 +02:00
parent 489ccc6c22
commit 851b062668
14 changed files with 2688 additions and 7 deletions

View File

@ -120,7 +120,7 @@ Ordre conseillé :
→ Audits : **[security/audit-securite-manager-service.md](security/audit-securite-manager-service.md)** et **[security/audit-manager-app.md](security/audit-manager-app.md)** (2026-07-13/14)
- **manager-service** — le point critique est la **rotation des secrets committés** (JWT signing key, clés API, connection strings, MQTT, Telegram). Gravité inchangée, mais **déplacée en fin de backlog le 2026-08-11** : les 6 repos sont privés sur un Gitea auto-hébergé. ⚠️ Contrainte à ne pas perdre — **la rotation doit précéder I3** (compose/Traefik prod), sinon la prod naît avec les clés de l'historique et il faut rotationner deux fois. Puis quatre niveaux Haute → Basse.
- **manager-app** — le critique (`manager_api_new` désynchronisé, 68 erreurs de bruit) est ✅ **résolu le 2026-08-11** : `flutter analyze` passe à 3 issues et redevient un feu vert utilisable. Au passage, les 3 déclencheurs de génération OpenAPI ont été supprimés, ce qui **verrouille la règle « `manager_api_new` s'édite à la main »**. Reste Haute → Basse : duplication du shell de dialog, `build()` monolithiques, i18n contournée, clé AES en dur.
- **manager-app** — le critique (`manager_api_new` désynchronisé, 68 erreurs de bruit) est ✅ **résolu le 2026-08-11** : `flutter analyze` passe à 3 issues et redevient un feu vert utilisable. Au passage, les 3 déclencheurs de génération OpenAPI ont été supprimés, ce qui **verrouille la règle « `manager_api_new` s'édite à la main »**. Reste Haute → Basse : duplication du shell de dialog, `build()` monolithiques, i18n contournée, clé AES en dur.**La duplication du shell de dialog a désormais sa maquette et sa carte** : `DOCS/claude design/refonte-configuration-sections.html` §6 (coquille, inventaire des 29 dialogues, deux dialogues dessinés) et `kanban/cards/5-planifie/290-…`. ⚠️ **Ordre imposé** : les 8 `showNewOrUpdate*` sont *absorbés* par l'éditeur de collection du gabarit B — extraire un scaffold pour eux d'abord, ce serait refactoriser du code à supprimer. Voir la note dans `security/audit-manager-app.md`.
→ **Détail complet, par priorité et avec les cases à cocher : [status/securite-dette-technique.md](status/securite-dette-technique.md)**

View File

@ -0,0 +1,416 @@
<!-- Maquette : dialogue d'ajout d'une ressource (Médiathèque).
Artifact de diffusion e029b74d-40d7-43f7-9f63-f89d3a47c7c2, publie le 2026-09-03.
Source de verite du design : ce fichier. L'artifact ne sert qu'a la diffusion.
Contexte : DOCS/v1-mediatheque-plan.md — hors perimetre du lot V1, a arbitrer. -->
<title>Ajouter une ressource — maquette</title>
<style>
:root {
--ground: #EDF0F4;
--surface: #FFFFFF;
--surface-2: #F3F5F8;
--surface-3: #E7EBF1;
--ink: #14202B;
--ink-2: #4A5A6B;
--ink-3: #7C8B9A;
--line: #D6DDE5;
--line-soft: #E6EBF1;
--brand: #264863;
--brand-2: #3C6C90;
--brand-soft: #C2C9D6;
--on-brand: #FFFFFF;
--warning: #9A6608;
--warning-bg: #FDF6E3;
--serious: #A8322A;
--serious-bg: #FBEDEC;
--good: #1F6B4D;
--shadow: 0 1px 2px rgba(20,32,43,.05), 0 4px 14px rgba(20,32,43,.05);
--shadow-lg: 0 2px 6px rgba(20,32,43,.08), 0 18px 42px rgba(20,32,43,.14);
--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:not([data-theme="light"]) {
--ground:#0E141B; --surface:#18222C; --surface-2:#1E2A35; --surface-3:#26333F;
--ink:#E6ECF2; --ink-2:#A3B2C0; --ink-3:#74879A;
--line:#2C3A48; --line-soft:#243140;
--brand:#7FA8C9; --brand-2:#A3C4DD; --brand-soft:#35506B; --on-brand:#0E141B;
--warning:#D6A343; --warning-bg:#2A2417; --serious:#E08078; --serious-bg:#2B1B1A; --good:#6FBF97;
--shadow: 0 1px 2px rgba(0,0,0,.3), 0 4px 14px rgba(0,0,0,.22);
--shadow-lg: 0 2px 6px rgba(0,0,0,.4), 0 18px 42px rgba(0,0,0,.34);
}
}
:root[data-theme="dark"] {
--ground:#0E141B; --surface:#18222C; --surface-2:#1E2A35; --surface-3:#26333F;
--ink:#E6ECF2; --ink-2:#A3B2C0; --ink-3:#74879A;
--line:#2C3A48; --line-soft:#243140;
--brand:#7FA8C9; --brand-2:#A3C4DD; --brand-soft:#35506B; --on-brand:#0E141B;
--warning:#D6A343; --warning-bg:#2A2417; --serious:#E08078; --serious-bg:#2B1B1A; --good:#6FBF97;
--shadow: 0 1px 2px rgba(0,0,0,.3), 0 4px 14px rgba(0,0,0,.22);
--shadow-lg: 0 2px 6px rgba(0,0,0,.4), 0 18px 42px rgba(0,0,0,.34);
}
* { 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; }
.page { max-width:1240px; margin:0 auto; padding:34px 22px 72px; }
h1 { margin:0 0 6px; font-size:25px; font-weight:700; letter-spacing:-.02em; text-wrap:balance; }
.lede { margin:0 0 24px; font-size:14.5px; color:var(--ink-2); max-width:72ch; }
h2 { margin:38px 0 4px; font-size:16px; font-weight:700; letter-spacing:-.01em; }
h2 + .sub { margin:0 0 16px; font-size:13px; color:var(--ink-3); max-width:74ch; }
.note { display:flex; gap:12px; align-items:flex-start; background:var(--surface); border:1px solid var(--line); border-left:3px solid var(--brand); border-radius:6px; padding:13px 16px; margin-bottom:26px; }
.note strong { display:block; font-size:13.5px; margin-bottom:2px; }
.note span { font-size:13px; color:var(--ink-2); }
code { font-family:var(--mono); font-size:.88em; background:var(--surface-3); border-radius:3px; padding:1px 5px; color:var(--ink-2); }
/* ── deux colonnes de maquette ─────────────────────────────────────── */
.compare { display:grid; grid-template-columns:1fr 1fr; gap:26px; align-items:start; }
@media (max-width:1080px){ .compare { grid-template-columns:1fr; } }
.stage { background:var(--surface-2); border:1px solid var(--line); border-radius:10px; padding:22px 20px 26px; overflow-x:auto; }
.stage > header { display:flex; align-items:center; gap:9px; margin-bottom:16px; }
.stage > header b { font-size:13.5px; }
.pill { font-family:var(--mono); font-size:10px; letter-spacing:.09em; text-transform:uppercase; border-radius:3px; padding:3px 8px; }
.pill.now { background:var(--serious-bg); color:var(--serious); border:1px solid var(--serious); }
.pill.next { background:var(--brand); color:var(--on-brand); }
/* ── coquille de dialogue ──────────────────────────────────────────── */
.dlg { width:512px; background:var(--surface); box-shadow:var(--shadow-lg); overflow:hidden; }
.dlg.old { border-radius:20px; padding:20px 24px 24px; }
.dlg.new { border-radius:10px; border:1px solid var(--line); }
.dlg-title-old { text-align:center; font-size:22px; font-weight:400; margin:0 0 10px; }
.tabbar { display:flex; border-bottom:1px solid var(--line); }
.tabbar div { flex:1; text-align:center; padding:11px 0; font-size:14px; color:var(--ink); }
.tabbar div.on { color:var(--brand); font-weight:600; box-shadow:inset 0 -2px 0 var(--brand); }
.oldbody { height:238px; display:grid; place-items:center; position:relative; }
.bigbtn { background:var(--brand); color:var(--on-brand); border-radius:15px; padding:15px 25px; font-size:14px; }
.oldfoot { display:flex; justify-content:flex-end; gap:8px; margin-top:16px; }
.rb { display:inline-flex; align-items:center; gap:7px; border-radius:999px; padding:9px 17px; font-size:15px; }
.rb.second { background:var(--brand-soft); color:var(--ink); }
.rb.prim { background:var(--brand); color:var(--on-brand); }
/* la carte muette d'aujourd'hui */
.mute { width:100%; margin:0 12px; background:var(--surface-2); border-radius:8px; box-shadow:var(--shadow); padding:12px 14px; }
.mute b { display:block; font-size:14px; }
.mute span { font-size:12.5px; color:var(--ink-3); }
/* ── dialogue proposé ──────────────────────────────────────────────── */
.nhead { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:17px 18px 14px; }
.nhead h3 { margin:0; font-size:15px; font-weight:700; }
.xbtn { width:28px; height:28px; border-radius:5px; border:1px solid transparent; background:none; color:var(--ink-3); display:grid; place-items:center; cursor:pointer; }
.xbtn:hover { background:var(--surface-2); border-color:var(--line); color:var(--ink); }
.xbtn:focus-visible { outline:2px solid var(--brand); outline-offset:2px; }
.nbody { padding:0 18px 4px; }
.drop { border:1.5px dashed var(--line); border-radius:8px; background:var(--surface-2); padding:26px 18px; text-align:center; }
.drop.hot { border-color:var(--brand); background:color-mix(in srgb, var(--brand) 7%, var(--surface-2)); }
.drop .ico { color:var(--ink-3); margin-bottom:8px; }
.drop b { display:block; font-size:13.5px; font-weight:600; }
.drop .browse { color:var(--brand); text-decoration:underline; text-underline-offset:2px; }
.drop small { display:block; margin-top:5px; font-size:12px; color:var(--ink-3); }
.orurl { display:flex; align-items:center; gap:10px; margin-top:12px; }
.orurl .bar { flex:1; height:1px; background:var(--line-soft); }
.linkbtn { display:inline-flex; align-items:center; gap:6px; background:none; border:none; font-family:inherit; font-size:12.5px; color:var(--ink-2); cursor:pointer; padding:2px 4px; border-radius:4px; }
.linkbtn:hover { color:var(--brand); }
.linkbtn:focus-visible { outline:2px solid var(--brand); outline-offset:2px; }
.urlrow { display:none; gap:8px; margin-top:11px; }
.urlrow.open { display:flex; }
.inp { flex:1; background:var(--surface-2); border:1px solid var(--line); border-radius:5px; padding:8px 11px; font-family:inherit; font-size:13px; color:var(--ink); }
.inp:focus-visible { outline:2px solid var(--brand); outline-offset:-1px; }
.flist { margin:14px 0 0; padding:0; list-style:none; border-top:1px solid var(--line-soft); }
.frow { display:grid; grid-template-columns:26px 1fr auto 28px; gap:10px; align-items:center; padding:9px 0; border-bottom:1px solid var(--line-soft); }
.frow .ft { color:var(--ink-3); display:grid; place-items:center; }
.frow .fn { min-width:0; font-size:13px; }
.frow .fn b { display:block; font-weight:500; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.frow .fn span { font-size:11.5px; color:var(--ink-3); }
.chip { font-family:var(--mono); font-size:10px; letter-spacing:.06em; text-transform:uppercase; color:var(--ink-2); background:var(--surface-3); border-radius:3px; padding:3px 7px; }
.frow .sz { font-size:12px; color:var(--ink-3); font-variant-numeric:tabular-nums; }
.del { width:28px; height:28px; border:1px solid var(--line); background:var(--surface-2); border-radius:5px; color:var(--ink-3); display:grid; place-items:center; cursor:pointer; }
.del:hover { border-color:var(--serious); color:var(--serious); }
.del:focus-visible { outline:2px solid var(--brand); outline-offset:2px; }
.totals { display:flex; justify-content:space-between; align-items:baseline; padding:11px 0 3px; font-size:12.5px; color:var(--ink-3); }
.totals b { color:var(--ink-2); font-weight:600; font-variant-numeric:tabular-nums; }
.nfoot { display:flex; align-items:center; justify-content:flex-end; gap:10px; padding:14px 18px; margin-top:10px; background:var(--surface-2); border-top:1px solid var(--line-soft); }
.btn { display:inline-flex; align-items:center; gap:7px; border-radius:999px; padding:8px 16px; font-family:inherit; font-size:13px; font-weight:600; cursor:pointer; border:1px solid transparent; }
.btn.ghost { background:none; color:var(--ink-2); }
.btn.ghost:hover { color:var(--ink); }
.btn.primary { background:var(--brand); color:var(--on-brand); border-color:var(--brand); }
.btn.primary:hover { background:var(--brand-2); border-color:var(--brand-2); }
.btn:focus-visible { outline:2px solid var(--brand); outline-offset:2px; }
.btn[disabled] { opacity:.45; cursor:not-allowed; }
/* ── pastilles de renvoi vers le tableau ───────────────────────────── */
.cx { display:inline-grid; place-items:center; width:17px; height:17px; border-radius:50%; background:var(--serious); color:#fff; font-family:var(--mono); font-size:10px; font-weight:700; vertical-align:middle; }
.anno { display:flex; align-items:flex-start; gap:7px; margin-top:9px; font-size:12px; color:var(--serious); }
.anno .cx { flex:0 0 auto; margin-top:1px; }
/* ── tableau des défauts ───────────────────────────────────────────── */
.tblwrap { overflow-x:auto; border:1px solid var(--line); border-radius:8px; background:var(--surface); }
table { width:100%; border-collapse:collapse; min-width:720px; }
th, td { text-align:left; padding:11px 14px; border-bottom:1px solid var(--line-soft); font-size:13px; vertical-align:top; }
th { font-size:11px; font-family:var(--mono); letter-spacing:.08em; text-transform:uppercase; color:var(--ink-3); background:var(--surface-2); border-bottom:1px solid var(--line); }
tr:last-child td { border-bottom:none; }
td.n { width:34px; }
td .where { display:block; margin-top:3px; font-family:var(--mono); font-size:11px; color:var(--ink-3); }
.bugflag { display:inline-block; font-family:var(--mono); font-size:10px; letter-spacing:.06em; text-transform:uppercase; color:var(--serious); background:var(--serious-bg); border:1px solid var(--serious); border-radius:3px; padding:1px 6px; margin-right:6px; }
.cols { display:grid; grid-template-columns:1fr 1fr; gap:18px; margin-top:16px; }
@media (max-width:820px){ .cols { grid-template-columns:1fr; } }
.card { background:var(--surface); border:1px solid var(--line); border-radius:8px; padding:16px 18px 18px; }
.card h3 { margin:0 0 10px; font-size:13.5px; font-weight:700; }
.card ul { margin:0; padding-left:18px; }
.card li { font-size:13px; color:var(--ink-2); margin-bottom:6px; }
.card li:last-child { margin-bottom:0; }
.warnbox { display:flex; gap:11px; align-items:flex-start; background:var(--warning-bg); border:1px solid var(--warning); border-radius:6px; padding:13px 16px; margin-top:26px; }
.warnbox strong { display:block; font-size:13.5px; margin-bottom:2px; color:var(--warning); }
.warnbox span { font-size:13px; color:var(--ink-2); }
@media (prefers-reduced-motion: reduce) { * { transition:none !important; animation:none !important; } }
</style>
<div class="page">
<h1>Ajouter une ressource</h1>
<p class="lede">Le dialogue d'ajout est le dernier écran de la Médiathèque à ne pas avoir été repris. Il ne dénote pas seulement : deux de ses défauts sont des bugs de dimensionnement, et un troisième fait qu'on ne voit pas ce qu'on vient de choisir.</p>
<div class="note">
<div>
<strong>La thèse : les deux onglets sont le problème.</strong>
<span>« Local » / « En ligne » est une distinction <em>technique</em> — où vivent les octets — imposée avant que l'utilisateur ait fait quoi que ce soit. Un seul corps suffit : une zone de dépôt, et dessous une ligne discrète « ou coller une URL ».</span>
</div>
</div>
<div class="compare">
<!-- ─────────────────────────── AUJOURD'HUI ─────────────────────────── -->
<section class="stage">
<header><b>Aujourd'hui</b><span class="pill now">tel quel dans le code</span></header>
<div class="dlg old">
<p class="dlg-title-old">Nouvelle ressource</p>
<div style="border:1px solid var(--line-soft); border-radius:4px; overflow:hidden;">
<div class="tabbar">
<div class="on">Local</div>
<div>En ligne</div>
</div>
<div class="oldbody">
<div class="mute">
<b>Fichiers</b>
<span>Aucun aperçu possible</span>
</div>
</div>
</div>
<div class="oldfoot">
<span class="rb second">↺ Annuler</span>
<span class="rb prim">✓ Créer</span>
</div>
</div>
<p class="anno"><span class="cx">1</span> Rayon 20 — l'échelle de l'app va de 5 à 10.</p>
<p class="anno"><span class="cx">2</span> Le corps se dimensionne sur l'écran, pas sur le dialogue : 960 × 540 demandés dans 512 de large.</p>
<p class="anno"><span class="cx">4</span> Cinq fichiers choisis, et voilà tout ce qu'on en voit.</p>
<p class="anno"><span class="cx">5</span> « Local » / « En ligne » écrits en dur, en français.</p>
<p class="anno"><span class="cx">6</span> Deux boutons de même poids ; le primaire accepte le clic à vide.</p>
</section>
<!-- ─────────────────────────── PROPOSÉ ─────────────────────────────── -->
<section class="stage">
<header><b>Proposé</b><span class="pill next">maquette</span></header>
<div class="dlg new">
<div class="nhead">
<h3>Ajouter une ressource</h3>
<button class="xbtn" type="button" aria-label="Fermer">
<svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><path d="M3.5 3.5l9 9M12.5 3.5l-9 9"/></svg>
</button>
</div>
<div class="nbody">
<div class="drop" id="drop">
<div class="ico">
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M12 15V4M12 4L8 8M12 4l4 4"/><path d="M4 15v3a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-3"/>
</svg>
</div>
<b>Glissez vos fichiers ici ou <span class="browse">parcourez</span></b>
<small>JPG, PNG, GIF, MP3, MP4, WEBM, PDF, JSON</small>
</div>
<div class="orurl">
<span class="bar"></span>
<button class="linkbtn" type="button" id="urltoggle" aria-expanded="false" aria-controls="urlrow">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d="M6.5 9.5a2.8 2.8 0 0 0 4 0l2-2a2.8 2.8 0 0 0-4-4l-.8.8"/><path d="M9.5 6.5a2.8 2.8 0 0 0-4 0l-2 2a2.8 2.8 0 0 0 4 4l.8-.8"/></svg>
ou coller une URL
</button>
<span class="bar"></span>
</div>
<div class="urlrow" id="urlrow">
<input class="inp" type="url" placeholder="https://… image, vidéo YouTube ou fichier JSON" aria-label="URL de la ressource">
<button class="btn primary" type="button">Ajouter</button>
</div>
<ul class="flist">
<li class="frow">
<span class="ft"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="8.5" cy="9.5" r="1.5"/><path d="M21 16l-5-5-6 6-2-2-5 5" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
<span class="fn"><b>salle-des-fossiles.jpg</b><span>2560 × 1707</span></span>
<span class="chip">Image</span>
<button class="del" type="button" aria-label="Retirer salle-des-fossiles.jpg">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><path d="M3.5 3.5l9 9M12.5 3.5l-9 9"/></svg>
</button>
</li>
<li class="frow">
<span class="ft"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 18V5l10-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="16" cy="16" r="3"/></svg></span>
<span class="fn"><b>commentaire-salle-2-nl.mp3</b><span>3 min 12 s</span></span>
<span class="chip">Audio</span>
<button class="del" type="button" aria-label="Retirer commentaire-salle-2-nl.mp3">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><path d="M3.5 3.5l9 9M12.5 3.5l-9 9"/></svg>
</button>
</li>
<li class="frow">
<span class="ft"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/><path d="M14 3v5h5"/></svg></span>
<span class="fn"><b>brochure-2026.pdf</b><span>12 pages</span></span>
<span class="chip">PDF</span>
<button class="del" type="button" aria-label="Retirer brochure-2026.pdf">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><path d="M3.5 3.5l9 9M12.5 3.5l-9 9"/></svg>
</button>
</li>
</ul>
<div class="totals">
<span>3 fichier(s) · <b>4,8 Mo</b></span>
</div>
</div>
<div class="nfoot">
<button class="btn ghost" type="button">Annuler</button>
<button class="btn primary" type="button">Ajouter 3 fichiers</button>
</div>
</div>
<p class="anno" style="color:var(--ink-3)"><span class="cx" style="background:var(--brand)"></span> Le gain principal n'est pas l'habillage : c'est la liste. On voit ce qu'on a choisi, et on peut en retirer un.</p>
</section>
</div>
<h2>Les défauts, relevés dans le code</h2>
<p class="sub">Numérotés comme sur la maquette de gauche. Les trois marqués <span class="bugflag">bug</span> ne relèvent pas du goût.</p>
<div class="tblwrap">
<table>
<thead>
<tr><th class="n">#</th><th>Ce qui cloche</th><th>Effet</th></tr>
</thead>
<tbody>
<tr>
<td class="n"><span class="cx">1</span></td>
<td>Rayon 20 sur le dialogue<span class="where">new_resource_popup.dart:22</span></td>
<td>Hors de l'échelle de l'app (5 / 8 / 10). Exactement le défaut qu'on vient de retirer de la modale de détail.</td>
</tr>
<tr>
<td class="n"><span class="cx">2</span></td>
<td><span class="bugflag">bug</span>Le corps se dimensionne sur <code>MediaQuery</code><span class="where">upload_content_container.dart:33-35</span></td>
<td><code>width * 0.5</code> × <code>height * 0.5</code> de l'<em>écran</em> : sur 1920 × 1080, 960 × 540 demandés dans un dialogue de 512 de large utile. Le contenu déborde et se fait rogner.</td>
</tr>
<tr>
<td class="n"><span class="cx">3</span></td>
<td><span class="bugflag">bug</span>Le champ URL aussi<span class="where">upload_online_resources_container.dart:107 — <code>// TODO GET SIZE</code></span></td>
<td>Même cause : <code>width * 0.35</code> de l'écran. Le TODO est resté.</td>
</tr>
<tr>
<td class="n"><span class="cx">4</span></td>
<td><span class="bugflag">bug</span>On ne voit pas ce qu'on a choisi<span class="where">upload_content_container.dart:57-58</span></td>
<td>Sur le web, après sélection : une carte titrée « Fichiers », sous-titrée « Aucun aperçu possible ». Cinq fichiers ou un seul, même affichage. Aucun moyen d'en retirer un — il faut tout recommencer.</td>
</tr>
<tr>
<td class="n"><span class="cx">5</span></td>
<td>Onglets écrits en dur<span class="where">resource_tab.dart:30-31</span></td>
<td>« Local » et « En ligne » restent en français en anglais et en néerlandais — le trou qu'on vient de combler ailleurs dans la Médiathèque.</td>
</tr>
<tr>
<td class="n"><span class="cx">6</span></td>
<td>Deux boutons de même poids, et une erreur évitable<span class="where">new_resource_popup.dart:60-92</span></td>
<td>Le primaire accepte le clic à vide, puis affiche une notification orange « aucun fichier chargé ». Désactiver le bouton supprime l'erreur au lieu de l'expliquer.</td>
</tr>
<tr>
<td class="n"></td>
<td>Type forcé à <code>Image</code> pour tout fichier local<span class="where">resource_tab.dart:93 et :97</span></td>
<td>Code mort et trompeur : <code>create()</code> re-déduit le type depuis l'extension. À lire, on croit que le MP3 sera enregistré comme image.</td>
</tr>
<tr>
<td class="n"></td>
<td><span class="bugflag">bug</span><code>filesToSendWeb!.length</code> sur une variable nulle<span class="where">new_resource_popup.dart:82</span></td>
<td>Branche non-web uniquement : cliquer « Créer » sans rien choisir plante. Invisible en production tant que l'app reste web.</td>
</tr>
</tbody>
</table>
</div>
<h2>Ce que ça touche</h2>
<div class="cols">
<div class="card">
<h3>Le geste change</h3>
<ul>
<li>Glisser-déposer réel, plus seulement un bouton bleu.</li>
<li>Une ligne par fichier : type, nom, poids, et une croix pour en retirer un.</li>
<li>Le poids total des fichiers choisis. <em>Avant</em> compression : la calculer pour l'afficher referait tout le travail de l'upload une seconde fois, et l'estimation haute est du bon côté pour un quota.</li>
<li>« Ajouter 3 fichiers » plutôt que « Créer » : le bouton dit ce qui va se passer.</li>
<li>Le type se déduit de l'extension et s'affiche en pastille, ce que le code fait déjà sans le montrer.</li>
</ul>
</div>
<div class="card">
<h3>Les fichiers</h3>
<ul>
<li><code>new_resource_popup.dart</code> — réécrit</li>
<li><code>upload_content_container.dart</code> — réécrit (zone de dépôt + liste)</li>
<li><code>resource_tab.dart</code><strong>supprimé</strong>, il ne portait que la barre d'onglets</li>
<li><code>upload_online_resources_container.dart</code><strong>supprimé</strong>, remplacé par le champ URL dépliant</li>
<li>Une poignée de clés l10n, dans la même série <code>media*</code></li>
</ul>
</div>
</div>
<div class="note" style="margin-top:26px; border-left-color:var(--good)">
<div>
<strong>Implémenté le 2026-09-03.</strong>
<span>Glisser-déposer réel (écoute des événements du document — Flutter web ne reçoit pas les fichiers du bureau), liste retirable, champ URL dépliant, bouton désactivé à vide. <code>resource_tab.dart</code>, <code>upload_content_container.dart</code> et <code>upload_online_resources_container.dart</code> sont supprimés ; le nouveau sélecteur vit dans <code>Components/resource_picker.dart</code>. Le dialogue de confirmation partagé (12 appelants) a été repris dans la foulée : « Oui »/« Non » en dur remplacés, et le bouton de confirmation passe en rouge quand l'action détruit.</span>
</div>
</div>
<div class="warnbox">
<div>
<strong>C'était hors périmètre du lot V1 Médiathèque.</strong>
<span><code>v1-mediatheque-plan.md</code> ne mentionne ce dialogue nulle part : le lot s'arrête à la grille, aux facettes et au panneau de détail. Ce chantier-ci est adjacent, pas inclus — à faire maintenant dans la foulée, ou à poser comme carte de suivi. C'est un arbitrage, pas une évidence.</span>
</div>
</div>
</div>
<script>
(function () {
var toggle = document.getElementById('urltoggle');
var row = document.getElementById('urlrow');
toggle.addEventListener('click', function () {
var open = row.classList.toggle('open');
toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
if (open) row.querySelector('.inp').focus();
});
// La zone de depot reagit au survol comme elle reagira au glisser.
var drop = document.getElementById('drop');
drop.addEventListener('mouseenter', function () { drop.classList.add('hot'); });
drop.addEventListener('mouseleave', function () { drop.classList.remove('hot'); });
})();
</script>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,514 @@
<title>Le canevas de Map et Événement</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap">
<style>
:root {
--ground: #f4f6f8;
--surface: #ffffff;
--surface-2: #eef1f5;
--surface-3: #e3e9ef;
--line: #d8dfe6;
--line-soft: #e8edf2;
--ink: #16222c;
--ink-2: #3f4e5a;
--ink-3: #788793;
--brand: #264863;
--brand-tint: #e3eaf1;
--go: #5d7f33;
--go-tint: #e9efdf;
--stop: #a24a47;
--stop-tint: #f6e6e5;
--map: #cfdcd2;
--map-2: #bccfc2;
--water: #b9cddb;
--r: 8px;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--ground: #11171c;
--surface: #19212a;
--surface-2: #212b35;
--surface-3: #2a3641;
--line: #2f3c48;
--line-soft: #26313a;
--ink: #e9eef2;
--ink-2: #b4c1cc;
--ink-3: #7e8e9a;
--brand: #8fb6d6;
--brand-tint: #22303d;
--go: #a3c471;
--go-tint: #232d1e;
--stop: #e0918e;
--stop-tint: #33211f;
--map: #2c3a33;
--map-2: #35473c;
--water: #26384a;
}
}
:root[data-theme="dark"] {
--ground: #11171c;
--surface: #19212a;
--surface-2: #212b35;
--surface-3: #2a3641;
--line: #2f3c48;
--line-soft: #26313a;
--ink: #e9eef2;
--ink-2: #b4c1cc;
--ink-3: #7e8e9a;
--brand: #8fb6d6;
--brand-tint: #22303d;
--go: #a3c471;
--go-tint: #232d1e;
--stop: #e0918e;
--stop-tint: #33211f;
--map: #2c3a33;
--map-2: #35473c;
--water: #26384a;
}
* { box-sizing: border-box; }
body {
background: var(--ground);
color: var(--ink);
font-family: "IBM Plex Sans", system-ui, -apple-system, sans-serif;
font-size: 16px;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
.wrap { max-width: 1000px; margin: 0 auto; padding: 52px 22px 88px; display: flex; flex-direction: column; gap: 44px; }
code {
font-family: "IBM Plex Mono", ui-monospace, monospace;
font-size: .84em; background: var(--surface-2); border-radius: 3px;
padding: .1em .34em; color: var(--ink-2);
}
.eyebrow {
font-family: "IBM Plex Mono", monospace; font-size: 11px;
letter-spacing: .14em; text-transform: uppercase; color: var(--ink-3);
}
header { display: flex; flex-direction: column; gap: 9px; }
h1 {
font-size: clamp(29px, 4.6vw, 40px); font-weight: 700; line-height: 1.08;
letter-spacing: -.024em; margin: 0; text-wrap: balance;
}
.lede { font-size: 17.5px; color: var(--ink-2); max-width: 64ch; margin: 0; }
section { display: flex; flex-direction: column; gap: 15px; }
.head { display: flex; gap: 15px; align-items: baseline; }
.head .num {
font-family: "IBM Plex Mono", monospace; font-size: 12px; font-weight: 600;
color: var(--brand); background: var(--brand-tint); border-radius: 4px;
padding: 3px 7px; flex: none;
}
h2 { font-size: 21px; font-weight: 600; letter-spacing: -.014em; margin: 0; text-wrap: balance; }
.sub { font-size: 15px; color: var(--ink-2); margin: 4px 0 0; max-width: 66ch; }
p { margin: 0; max-width: 68ch; color: var(--ink-2); }
p strong { color: var(--ink); font-weight: 600; }
.callout {
background: var(--surface); border: 1px solid var(--line);
border-left: 3px solid var(--brand); border-radius: 0 var(--r) var(--r) 0;
padding: 15px 19px; display: flex; flex-direction: column; gap: 8px;
}
.callout.warn { border-left-color: var(--stop); }
.callout p { font-size: 15px; }
table { width: 100%; border-collapse: collapse; font-size: 14.5px; background: var(--surface); border: 1px solid var(--line); border-radius: var(--r); overflow: hidden; }
th {
text-align: left; font-family: "IBM Plex Mono", monospace; font-weight: 500;
font-size: 10.5px; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-3);
padding: 10px 15px; background: var(--surface-2); border-bottom: 1px solid var(--line);
}
td { padding: 12px 15px; border-bottom: 1px solid var(--line-soft); color: var(--ink-2); vertical-align: top; }
tr:last-child td { border-bottom: none; }
td:first-child { color: var(--ink); font-weight: 600; white-space: nowrap; }
.scroller { overflow-x: auto; }
.go { color: var(--go); font-weight: 600; }
.stop { color: var(--stop); font-weight: 600; }
/* ─── Maquette ─── */
figure { margin: 0; display: flex; flex-direction: column; gap: 11px; }
.stage {
background: var(--surface-2); border: 1px solid var(--line);
border-radius: var(--r); padding: 18px;
}
figcaption { font-size: 14px; color: var(--ink-2); }
figcaption b { color: var(--ink); }
.app { background: var(--surface); border: 1px solid var(--line); border-radius: 6px; overflow: hidden; }
.app-head {
display: flex; align-items: center; gap: 9px; padding: 9px 13px;
border-bottom: 1px solid var(--line); font-size: 12.5px; font-weight: 600;
}
.app-head .chip {
font-family: "IBM Plex Mono", monospace; font-size: 9.5px; letter-spacing: .08em;
text-transform: uppercase; background: var(--surface-3); color: var(--brand);
padding: 3px 7px; border-radius: 4px; font-weight: 600;
}
.app-head .spacer { flex: 1; }
.app-body { padding: 13px; display: flex; flex-direction: column; gap: 11px; }
.pane { border: 1px solid var(--line); border-radius: 6px; overflow: hidden; }
.pane-t {
font-size: 12px; font-weight: 600; padding: 8px 11px;
border-bottom: 1px solid var(--line-soft); display: flex; align-items: center; gap: 7px;
}
.pane-t .spacer { flex: 1; }
.pane-t .hint { font-weight: 400; font-size: 10.5px; color: var(--ink-3); }
/* trois zones du gabarit C */
.c3 { display: grid; grid-template-columns: 150px minmax(0, 1fr) 300px; }
@media (max-width: 860px) { .c3 { grid-template-columns: 1fr; } }
.zlist { background: var(--ground); border-right: 1px solid var(--line); padding: 8px; display: flex; flex-direction: column; gap: 4px; }
@media (max-width: 860px) { .zlist { border-right: none; border-bottom: 1px solid var(--line); } }
.li {
display: flex; align-items: center; gap: 6px; padding: 6px 7px;
border-radius: 4px; font-size: 11.5px; color: var(--ink-2);
}
.li .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--ink-3); flex: none; }
.li .n { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.li.sel { background: var(--surface); border: 1px solid var(--brand); color: var(--ink); font-weight: 500; }
.li.sel .dot { background: var(--brand); }
.li.new { border: 1px dashed var(--ink-3); color: var(--brand); justify-content: center; font-weight: 500; }
.li .cat { width: 7px; height: 7px; border-radius: 2px; flex: none; }
.zform { padding: 12px 14px; display: flex; flex-direction: column; gap: 11px; }
.fgrid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 9px 11px; }
.f { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.f > label { font-size: 10.5px; font-weight: 600; color: var(--ink-2); }
.f .in {
border: 1px solid var(--line); background: var(--surface-2); border-radius: 4px;
padding: 5px 8px; font-size: 11px; color: var(--ink); min-height: 26px;
display: flex; align-items: center; gap: 6px;
}
.f .in .v { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.f .in .st { font-family: "IBM Plex Mono", monospace; font-size: 9px; color: var(--ink-3); }
.fold {
border: 1px solid var(--line); border-radius: 5px; padding: 7px 10px;
font-size: 11.5px; color: var(--ink-2); display: flex; align-items: center; gap: 7px;
}
.fold .caret { color: var(--ink-3); font-size: 10px; }
.fold .spacer { flex: 1; }
.fold .count { font-family: "IBM Plex Mono", monospace; font-size: 9.5px; color: var(--ink-3); }
.strip { display: flex; gap: 6px; }
.strip .th {
width: 42px; height: 32px; border-radius: 4px; background: var(--surface-3);
border: 1px solid var(--line); flex: none;
}
.strip .th.img { background: linear-gradient(135deg, #4a7fa5, #9dc0d8); border-color: transparent; }
.strip .th.add { display: grid; place-items: center; color: var(--brand); font-size: 15px; border-style: dashed; }
/* le canevas */
.zmap { border-left: 1px solid var(--line); position: relative; min-height: 268px; background: var(--map); overflow: hidden; }
@media (max-width: 860px) { .zmap { border-left: none; border-top: 1px solid var(--line); } }
.zmap .roads { position: absolute; inset: 0; }
.zmap .tools {
position: absolute; top: 8px; left: 8px; display: flex; gap: 3px;
background: var(--surface); border: 1px solid var(--line); border-radius: 5px; padding: 3px;
}
.zmap .tool {
width: 24px; height: 24px; display: grid; place-items: center; border-radius: 3px;
font-size: 12px; color: var(--ink-2);
}
.zmap .tool.on { background: var(--brand); color: #fff; }
.zmap .swatch { width: 24px; height: 24px; border-radius: 3px; background: #c0623f; border: 2px solid var(--surface); }
.zmap .pin {
position: absolute; width: 13px; height: 13px; border-radius: 50% 50% 50% 0;
transform: rotate(-45deg); background: var(--brand); border: 2px solid #fff;
box-shadow: 0 1px 3px rgba(0,0,0,.35);
}
.zmap .pin.ghost { background: var(--ink-3); opacity: .55; }
.zmap .pin.sel { background: #c0623f; width: 17px; height: 17px; }
.zmap .zone {
position: absolute; border: 2px solid #c0623f; background: rgba(192, 98, 63, .18);
border-radius: 3px;
}
.zmap .foot {
position: absolute; left: 0; right: 0; bottom: 0; padding: 6px 9px;
background: var(--surface); border-top: 1px solid var(--line);
font-family: "IBM Plex Mono", monospace; font-size: 9.5px; color: var(--ink-3);
display: flex; align-items: center; gap: 8px;
}
.zmap .foot .spacer { flex: 1; }
.zmap .foot .live { color: var(--go); font-weight: 600; }
/* avant / après */
.ba { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
@media (max-width: 780px) { .ba { grid-template-columns: 1fr; } }
.stack { background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r); padding: 16px; display: flex; flex-direction: column; gap: 9px; }
.stack .lab { font-family: "IBM Plex Mono", monospace; font-size: 10px; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-3); }
.layer {
background: var(--surface); border: 1px solid var(--line); border-radius: 5px;
padding: 9px 11px; font-size: 12px; color: var(--ink-2);
}
.layer .t { font-weight: 600; color: var(--ink); font-size: 12.5px; }
.layer.l2 { margin-left: 16px; }
.layer.l3 { margin-left: 32px; border-color: var(--stop); }
.layer.flat { border-color: var(--go); }
ol.steps { display: flex; flex-direction: column; gap: 9px; margin: 0; padding: 0; list-style: none; }
ol.steps li {
display: flex; gap: 13px; align-items: baseline; font-size: 15px; color: var(--ink-2);
padding-bottom: 9px; border-bottom: 1px solid var(--line-soft);
}
ol.steps li:last-child { border-bottom: none; }
ol.steps .n { font-family: "IBM Plex Mono", monospace; font-size: 11.5px; color: var(--ink-3); flex: none; }
footer { border-top: 1px solid var(--line); padding-top: 16px; font-size: 13.5px; color: var(--ink-3); }
</style>
<div class="wrap">
<header>
<div class="eyebrow">Refonte manager-app · gabarit C</div>
<h1>Le canevas de Map et Événement</h1>
<p class="lede">Tout le reste de la refonte est codé. Il manque une seule chose, et c'est la même dans les deux écrans : poser un point ou une zone <em>sur</em> la carte de l'écran, au lieu d'ouvrir une carte dans une modale ouverte depuis une modale.</p>
</header>
<section>
<div class="head">
<span class="num">01</span>
<div>
<h2>Trois niveaux de modale pour placer un point</h2>
<p class="sub">C'est le geste le plus contraint que l'app propose aujourd'hui, et c'est aussi le plus fréquent sur une section Map.</p>
</div>
</div>
<div class="ba">
<div class="stack">
<div class="lab">Aujourd'hui</div>
<div class="layer"><div class="t">Écran de la section</div>La grille des points, 8 colonnes, sous les réglages de carte.</div>
<div class="layer l2"><div class="t">Modale « Point / Zone »</div><code>0,88 × largeur</code> · neuf champs traduits, une image, une catégorie, une liste de contenus.</div>
<div class="layer l3"><div class="t">Modale « Géométrie »</div>La carte où l'on dessine enfin. On ne voit plus ni la fiche, ni les autres points.</div>
</div>
<div class="stack">
<div class="lab">Proposé</div>
<div class="layer flat"><div class="t">Écran de la section</div>La liste des points, la fiche du point sélectionné, et la carte — les trois en même temps. Cliquer sur la carte place le point ouvert dans la fiche.</div>
<div style="font-size: 13px; color: var(--ink-3); padding: 4px 2px">Aucune modale. La carte est le troisième volet, pas une fenêtre.</div>
</div>
</div>
<div class="callout">
<span class="eyebrow">Ce que ça règle, concrètement</span>
<p>On place un point <strong>en voyant les autres</strong> — impossible aujourd'hui, alors que c'est la seule chose qui compte quand on répartit des points d'intérêt sur un plan. Et la fiche reste lisible pendant qu'on dessine, donc on sait quel point on est en train de déplacer.</p>
</div>
</section>
<section>
<div class="head">
<span class="num">02</span>
<div>
<h2>Le gabarit C, dessiné</h2>
<p class="sub">Liste, fiche, canevas. C'est le gabarit B auquel on ajoute une troisième colonne — et le rail droit de l'écran (Habillage, Diffusion) repasse au-dessus, comme annoncé.</p>
</div>
</div>
<figure>
<div class="stage">
<div class="app">
<div class="app-head">
<span>Plan du parc</span>
<span class="chip">Map</span>
<span class="spacer"></span>
<span style="font-size: 11px; color: var(--ink-3); font-weight: 400">Annuler · Enregistrer</span>
</div>
<div class="app-body">
<div class="pane">
<div class="pane-t">Réglages de la carte<span class="spacer"></span><span class="hint">déjà codé</span></div>
<div class="zform">
<div class="fgrid">
<div class="f" style="grid-column: span 2"><label>Service</label><div class="in"><span class="v">Google</span></div></div>
<div class="f" style="grid-column: span 2"><label>Type</label><div class="in"><span class="v">hybrid</span></div></div>
<div class="f" style="grid-column: span 2"><label>Zoom</label><div class="in"><span class="v">18</span></div></div>
<div class="f" style="grid-column: span 2"><label>Icône</label><div class="in"><span class="v">pin-parc.svg</span></div></div>
<div class="f" style="grid-column: span 2"><label>Catégories</label><div class="in"><span class="v">4 catégories</span></div></div>
<div class="f" style="grid-column: span 2"><label>Vue liste</label><div class="in"><span class="v">Activée</span></div></div>
</div>
</div>
</div>
<div class="pane">
<div class="pane-t">Points d'intérêt<span class="spacer"></span><span class="hint">12 points · le centre de la carte se règle ici aussi</span></div>
<div class="c3">
<div class="zlist">
<div class="li"><span class="cat" style="background:#5d7f33"></span><span class="n">Entrée nord</span></div>
<div class="li sel"><span class="cat" style="background:#c0623f"></span><span class="n">Serre tropicale</span></div>
<div class="li"><span class="cat" style="background:#c0623f"></span><span class="n">Volière</span></div>
<div class="li"><span class="cat" style="background:#264863"></span><span class="n">Buvette</span></div>
<div class="li"><span class="cat" style="background:#264863"></span><span class="n">Toilettes est</span></div>
<div class="li"><span class="cat" style="background:#5d7f33"></span><span class="n">Aire de jeux</span></div>
<div class="li new">+ Nouveau point</div>
</div>
<div class="zform">
<div class="fgrid">
<div class="f" style="grid-column: span 4"><label>Titre *</label><div class="in"><span class="v">Serre tropicale</span><span class="st">3/3</span></div></div>
<div class="f" style="grid-column: span 2"><label>Catégorie</label><div class="in"><span class="v">À voir</span></div></div>
<div class="f" style="grid-column: span 4"><label>Description</label><div class="in"><span class="v">Palmiers, orchidées et bassin à nénuphars.</span><span class="st">3/3</span></div></div>
<div class="f" style="grid-column: span 2"><label>Image</label><div class="in"><span class="v">serre.jpg</span></div></div>
</div>
<div class="fold">
<span class="caret"></span>Informations pratiques
<span class="spacer"></span>
<span class="count">2 / 5 remplies</span>
</div>
<div class="f">
<label>Contenus du point</label>
<div class="strip">
<span class="th img"></span>
<span class="th"></span>
<span class="th add">+</span>
</div>
</div>
</div>
<div class="zmap">
<svg class="roads" viewBox="0 0 300 268" preserveAspectRatio="none" aria-hidden="true">
<rect x="0" y="0" width="300" height="268" fill="var(--map)"/>
<path d="M-10 90 C 60 70, 120 130, 200 100 S 300 60, 320 80" stroke="var(--map-2)" stroke-width="16" fill="none"/>
<path d="M70 -10 C 90 80, 60 150, 110 280" stroke="var(--map-2)" stroke-width="11" fill="none"/>
<ellipse cx="235" cy="190" rx="52" ry="33" fill="var(--water)"/>
</svg>
<div class="tools">
<span class="tool on">📍</span>
<span class="tool"></span>
<span class="tool"></span>
<span class="swatch"></span>
</div>
<span class="pin ghost" style="left: 42px; top: 62px"></span>
<span class="pin ghost" style="left: 118px; top: 44px"></span>
<span class="pin ghost" style="left: 96px; top: 152px"></span>
<span class="pin ghost" style="left: 205px; top: 88px"></span>
<span class="pin ghost" style="left: 158px; top: 196px"></span>
<span class="zone" style="left: 128px; top: 96px; width: 66px; height: 52px"></span>
<span class="pin sel" style="left: 152px; top: 112px"></span>
<div class="foot">
<span>50.42933, 4.89143</span>
<span class="spacer"></span>
<span class="live">Serre tropicale</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<figcaption>
<b>Comment ça se lit</b> — Les points non sélectionnés restent visibles en gris : c'est l'information qui manquait. Le point ouvert dans la fiche est en couleur, et c'est lui que le clic sur la carte déplace. La barre d'outils en haut à gauche est celle de <code>MapGeometryPicker</code>, déplacée telle quelle : point, ligne, zone, couleur. Le pied de carte dit ce qu'on modifie et où, pour qu'aucun clic ne soit ambigu.
</figcaption>
</figure>
<div class="callout">
<span class="eyebrow">Neuf champs traduits, ça ne tient pas à plat — et ça n'a pas à tenir</span>
<p>Un point porte titre, description, site, prix, téléphone, e-mail, horaires, image et catégorie. Les quatre premiers d'un musée sont presque toujours remplis, les cinq autres presque jamais. La fiche montre donc <strong>titre, description, catégorie, image</strong>, et replie les <strong>informations pratiques</strong> derrière une ligne qui dit combien sont remplies. Rien n'est retiré, seulement rangé par fréquence d'usage réelle.</p>
</div>
<div class="callout">
<span class="eyebrow">Faut-il tout centrer sur la carte ? Non — mais deux modes, oui</span>
<p>Tentant, et je pense que ce serait une erreur en permanence : un point porte <strong>neuf champs traduits</strong>, une image, une catégorie et une liste de contenus ; la carte n'en porte qu'un seul, la géométrie. Donner en permanence le plus de place au champ qu'on modifie le moins ferait de la saisie — le vrai travail de cet écran — le parent pauvre. Le back-office est un outil de saisie ; la belle carte, le visiteur l'a déjà dans l'app.</p>
<p><strong>En revanche, 300 px ne suffisent pas</strong> pour juger d'une répartition de points. Le canevas passe donc à <strong>420 px de haut</strong> par défaut, et gagne un bouton <strong>« agrandir »</strong> qui lui donne toute la largeur : la liste reste en rail étroit, la fiche se replie. Deux moments distincts, deux dispositions — on écrit une fiche, ou on répartit des points, rarement les deux dans la même minute.</p>
</div>
<div class="callout">
<span class="eyebrow">Le centre de la carte n'a plus besoin de son propre sélecteur</span>
<p>Le canevas sait déjà afficher une carte et recevoir un clic. Le point central se règle donc par un mode du canevas, et <code>geoloc_input_container</code> — repris à la coquille standard cette semaine — n'a plus d'appelant sur cet écran. Il reste utile ailleurs tant que d'autres écrans le référencent.</p>
</div>
</section>
<section>
<div class="head">
<span class="num">03</span>
<div>
<h2>Événement : le même canevas, un autre fond</h2>
<p class="sub">Un événement pose des annotations sur le plan du site plutôt que des points d'intérêt sur une carte. La mécanique ne change pas, le fond de plan si.</p>
</div>
</div>
<p>Le canevas d'Événement affiche la <strong>carte de base</strong> choisie dans <code>baseSectionMapId</code>, et les annotations globales se posent dessus, exactement comme les points. Trois différences, et elles sont toutes dans le contenu, pas dans la forme :</p>
<div class="scroller">
<table>
<thead><tr><th>Ce qu'on pose</th><th>Gabarit</th><th>Pourquoi</th></tr></thead>
<tbody>
<tr><td>Annotations du plan</td><td class="go">C — canevas</td><td>Elles ont une position : elles se posent sur le plan, comme les points de Map. Même liste, même fiche, même canevas.</td></tr>
<tr><td>Blocs de programme</td><td>B — liste</td><td>Ils ont une heure de début et de fin, pas une position. Une liste ordonnée suffit — et c'est déjà ce qu'ils sont.</td></tr>
<tr><td>Parcours de l'événement</td><td>déjà fait</td><td>L'éditeur à fenêtre unique est raccordé et reste tel quel.</td></tr>
</tbody>
</table>
</div>
<div class="callout warn">
<span class="eyebrow">⚠️ Une question à laquelle le code ne répond pas</span>
<p>Quand aucune carte de base n'est choisie (<code>baseSectionMapId</code> vaut <code>null</code>), le canevas n'a pas de fond. Deux réponses possibles : afficher une carte vide comme sur Map, ou afficher un message qui invite à choisir un plan d'abord. <strong>Je propose la seconde</strong> — poser des annotations sur un fond qui va changer, c'est du travail à refaire — mais c'est un choix produit, pas technique.</p>
</div>
</section>
<section>
<div class="head">
<span class="num">04</span>
<div>
<h2>Ce qui disparaît</h2>
<p class="sub">Le canevas n'ajoute pas une brique : il en absorbe quatre.</p>
</div>
</div>
<div class="scroller">
<table>
<thead><tr><th>Fichier</th><th>Sort</th><th>Détail</th></tr></thead>
<tbody>
<tr><td><code>showNewOrUpdateGeoPoint.dart</code><br><span style="font-weight:400;color:var(--ink-3);font-size:13px">362 lignes</span></td><td class="go">supprimé</td><td>Absorbé par la fiche du gabarit C. Ses champs ne changent pas, ils changent de contenant.</td></tr>
<tr><td><code>geometry_input_container.dart</code></td><td class="go">supprimé</td><td>Le champ qui ouvrait la carte de dessin n'a plus lieu d'être quand la carte est à l'écran. ⚠️ Deux autres appelants à traiter d'abord : <code>etape_fields</code> et <code>showNewOrUpdateEventAgenda</code>.</td></tr>
<tr><td><code>map_geometry_picker.dart</code></td><td>déplacé</td><td><strong>Le dessin existe déjà et fonctionne.</strong> Il sort de son dialogue pour devenir le canevas. C'est le cœur du lot, et c'est du déplacement, pas de l'écriture.</td></tr>
<tr><td><code>showNewOrUpdateMapAnnotation.dart</code><br><span style="font-weight:400;color:var(--ink-3);font-size:13px">182 lignes</span></td><td class="go">supprimé</td><td>Même chose côté Événement.</td></tr>
<tr><td><code>geoloc_input_container.dart</code></td><td>conservé</td><td>Plus d'appelant sur Map, mais il reste référencé ailleurs. On le garde tant qu'il sert.</td></tr>
</tbody>
</table>
</div>
</section>
<section>
<div class="head">
<span class="num">05</span>
<div>
<h2>Dans quel ordre</h2>
<p class="sub">Les points d'un Map vivent côté serveur : l'éditeur de collection sait déjà leur parler depuis cette semaine.</p>
</div>
</div>
<ol class="steps">
<li><span class="n">1</span><span><strong>Fait.</strong> <code>MapCanvas</code> est sorti du dialogue : il reçoit une géométrie, la rend modifiable, rend la main à chaque changement, et affiche les autres tracés en fond. <code>MapGeometryPicker</code> n'est plus qu'une coquille autour de lui, pour ses deux appelants restants.</span></li>
<li><span class="n">2</span><span>Poser le gabarit C : liste, fiche, canevas, avec la liste en mode distant (les points sont chargés et écrits par l'API, comme les questions de Quiz).</span></li>
<li><span class="n">3</span><span>Migrer les champs du point depuis <code>showNewOrUpdateGeoPoint</code>, avec le repli des informations pratiques, puis supprimer la modale.</span></li>
<li><span class="n">4</span><span>Brancher les filtres qui existent déjà — recherche et catégories — sur la liste <em>et</em> sur le canevas, pour qu'ils cachent les mêmes points des deux côtés.</span></li>
<li><span class="n">5</span><span>Refaire le même geste sur Événement, avec la carte de base en fond, et traiter les deux autres appelants de <code>GeometryInputContainer</code>.</span></li>
</ol>
<div class="callout">
<span class="eyebrow">Ce que ça ne touche pas</span>
<p>Aucun DTO, aucun endpoint, aucune migration. <code>GeoPointDTO</code> garde ses neuf champs traduits, sa géométrie et sa couleur ; <code>MapDTO</code> garde ses catégories. Le risque est dans le comportement de la carte, pas dans les données — et il est réversible par <code>git</code>.</p>
</div>
</section>
<footer>
Proposition pour <strong>manager-app</strong> · même palette et mêmes tokens que la refonte Configuration &amp; Sections (<code>#264863</code>, <code>#f5f5f7</code>, <code>kRadiusCard</code>, <code>kLabelField</code>) · aucune dépendance nouvelle, la carte et le dessin sont déjà là.
</footer>
</div>

View File

@ -460,9 +460,9 @@
<section class="summary" aria-label="Chiffres clés">
<div class="stat"><span class="n n-critical">1</span><span class="k">Urgent</span></div>
<div class="stat"><span class="n n-info">1</span><span class="k">Migration v3</span></div>
<div class="stat"><span class="n n-warn">1</span><span class="k">Bugs ouverts</span></div>
<div class="stat"><span class="n n-warn">2</span><span class="k">Bugs ouverts</span></div>
<div class="stat"><span class="n">10</span><span class="k">À tester</span></div>
<div class="stat"><span class="n">32</span><span class="k">Planifié</span></div>
<div class="stat"><span class="n">36</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">60</span><span class="k">Fait récemment</span></div>
</section>
@ -520,7 +520,7 @@
<!-- BUGS -->
<section class="col" style="--stripe: var(--warn)">
<div class="col-head"><h2>Bugs ouverts</h2><span class="count">1</span></div>
<div class="col-head"><h2>Bugs ouverts</h2><span class="count">2</span></div>
<div class="stack">
<article class="card" data-area="manager">
@ -532,6 +532,16 @@
<span class="src">todo-features.md — Quota stockage</span>
</article>
<article class="card" data-area="visitapp" data-horizon="v1">
<div class="card-meta"><span class="tag">visitapp</span><span class="tag">offline</span><span class="tag">store</span><span class="flag f-critical">condition de publication sur le Play Store</span></div>
<h3>La taille du téléchargement n'est jamais affichée</h3>
<p><strong>Le visiteur ne sait jamais combien il télécharge.</strong> Le dialogue de <code>configurations_list.dart</code> annonce « il est nécessaire de télécharger du contenu supplémentaire » sans chiffre, et l'écran de progression n'affiche qu'un pourcentage. Or une visite du Fourneau, c'est un ordre de grandeur de plusieurs centaines de Mo sur la 4G du visiteur.</p>
<p><strong>Google Play exige que la taille des téléchargements complémentaires soit annoncée avant de les lancer</strong> — c'est une condition de publication, pas un confort.</p>
<p><strong>Rien à ajouter côté serveur</strong> : <code>ResourceDTO.sizeBytes</code> existe déjà et arrive dans <code>ExportConfigurationDTO.resources</code>. La somme se calcule côté client, et sur les seules ressources réellement à télécharger — <code>isResourceOutdated</code> filtre déjà celles qui sont à jour, donc une mise à jour de visite doit annoncer le <em>delta</em>, pas le total.</p>
<p><strong>Deux endroits</strong> : le dialogue de confirmation (avant), et l'écran de progression (« 128 / 412 Mo » plutôt qu'un pourcentage nu). ⚠️ La taille annoncée dans le dialogue suppose d'avoir déjà appelé <code>configurationExport</code> pour connaître la liste — aujourd'hui cet appel n'a lieu qu'<em>après</em> la confirmation.</p>
<span class="src">relevé le 2026-09-04 — configurations_list.dart, downloadConfiguration.dart</span>
</article>
</div>
</section>
@ -629,7 +639,7 @@
<!-- PLANIFIÉ -->
<section class="col" style="--stripe: var(--ink-3)">
<div class="col-head"><h2>Planifié</h2><span class="count">32</span></div>
<div class="col-head"><h2>Planifié</h2><span class="count">36</span></div>
<div class="stack">
<article class="card" data-area="visitapp" data-horizon="v1">
@ -678,6 +688,27 @@
<span class="src">v1-plan.md lot D — D4 · STATUS.md §K5</span>
</article>
<article class="card" data-area="visitapp" data-horizon="v2">
<div class="card-meta"><span class="tag">visitapp</span><span class="tag">carto</span><span class="tag">offline</span><span class="flag f-warn">prérequis pour vendre un parcours GPS sur un site sans réseau</span></div>
<h3>Carte hors ligne — tile packs Mapbox</h3>
<p><strong>Aujourd'hui aucune carte ne fonctionne hors ligne</strong>, et ce n'est pas une question de données : <code>MapDTO</code> porte déjà <code>points</code>, <code>centerLatitude</code>, <code>zoom</code> et <code>iconResourceId</code>, et les icônes sont téléchargées avec la visite. C'est le <strong>fond</strong> qui manque — les tuiles sont chargées en réseau, sans cache.</p>
<p><strong>Le GPS, lui, n'est pas le problème</strong> : <code>geolocator ^13.0.0</code> lit le GNSS, qui est autonome. Seule réserve à annoncer au client : sans A-GPS, le premier point peut demander 30 à 60 s sous couvert forestier.</p>
<p><strong>Le chemin court existe déjà dans le pubspec</strong> : <code>mapbox_maps_flutter ^2.0.0</code> expose <code>OfflineManager</code> (style packs) et <code>TileStore</code> (tile regions) — on télécharge une emprise bornée au moment du téléchargement de la visite. Pour un domaine comme le Fourneau Saint-Michel, quelques dizaines de Mo. <code>MapProvider.MapBox</code> existe déjà côté modèle.</p>
<p><strong>Google est une impasse, et pas seulement techniquement.</strong> Le SDK Maps pour Android n'expose aucune API de tuiles hors ligne — les « zones hors connexion » sont une fonction de l'app Google Maps, pas du SDK intégrable. Et le code n'utilise même pas ce SDK pour les sections carte : il passe par <code>flutter_map</code> sur <code>https://mt1.google.com/vt/lyrs=m&amp;x={x}&amp;y={y}&amp;z={z}</code>, un endpoint non documenté dont la mise en cache est explicitement interdite. <strong>Conclusion : Mapbox devient le fournisseur du mode hors ligne, Google reste en ligne seulement.</strong></p>
<p>⚠️ Tant que ce chantier n'est pas fait, <code>SectionType.Map</code> reste volontairement hors de <code>offlineCapableSectionTypes</code> (<code>downloadConfiguration.dart</code>) : l'ajouter donnerait une carte grise, ce qui est pire que ne pas la proposer. Une fois les tuiles packagées, c'est <strong>une ligne à ajouter dans cette constante</strong>.</p>
<span class="src">analyse du code le 2026-09-04 — MapProvider, flutter_map, mapbox_maps_flutter</span>
</article>
<article class="card" data-area="manager backend visitapp" data-horizon="v2">
<div class="card-meta"><span class="tag">carto</span><span class="tag">offline</span><span class="tag">manager-app</span><span class="flag f-good">hors ligne par construction, et c'est ce qu'un musée dessine déjà</span></div>
<h3>Plan illustré géoréférencé — un 3<sup>e</sup> fournisseur de carte</h3>
<p><strong>Un musée de plein air ne distribue pas un fond OpenStreetMap, il distribue son plan dessiné.</strong> Aujourd'hui <code>MapProvider</code> ne connaît que <code>Google</code> et <code>MapBox</code> : il n'existe aucun moyen de téléverser une image de plan et d'y placer les points.</p>
<p><strong>Ce qu'il faut</strong> : une troisième valeur de <code>MapProvider</code>, une ressource image portée par la section, et un calage — deux points d'ancrage suffisent (coin haut-gauche / bas-droit en coordonnées réelles) pour projeter un <code>GeoPoint</code> sur l'image et y afficher la position du visiteur.</p>
<p><strong>Pourquoi c'est le meilleur des trois</strong> : aucune tuile, donc <strong>hors ligne par construction</strong> — l'image part avec la visite comme n'importe quelle ressource, et le mécanisme de téléchargement n'a rien de nouveau à apprendre. Le rendu est aussi plus lisible qu'un fond routier sur un site de plusieurs dizaines de bâtiments.</p>
<p><strong>Périmètre réel</strong> : c'est le plus gros des trois chemins carto — backend (modèle + migration), manager-app (téléversement du plan et pose des ancres), <code>mymuseum-visitapp</code> et <code>visitapp-web</code> (rendu). À faire après <em>Carte hors ligne — tile packs Mapbox</em>, qui débloque le besoin immédiat avec un fond classique.</p>
<span class="src">analyse du code le 2026-09-04 — MapProvider n'a que Google et MapBox</span>
</article>
<article class="card" data-area="backend manager visitapp" data-horizon="v2">
<div class="card-meta"><span class="tag">3 repos</span><span class="flag f-warn">Contredit le code</span></div>
<h3>Les swagger.yaml embarqués sont périmés</h3>
@ -874,6 +905,57 @@
<span class="src">v2/studio-plan.md — lots 0 à 10</span>
</article>
<article class="card" data-area="manager" data-horizon="v1">
<div class="card-meta"><span class="tag">manager-app</span><span class="tag">UI</span><span class="tag">dialogues</span><span class="flag f-good">Maquette prête, code à faire</span></div>
<h3>Refonte visuelle des écrans Configuration &amp; Section, et de leurs dialogues</h3>
<p><strong>Lots 1 à 5 codés le 04/09</strong> (<code>flutter analyze</code> sans erreur, <code>flutter build web</code> passe ; <strong>rien de vérifié à l'écran</strong>). Pilote <code>select_resource_modal</code> : coquille standard, corps souple, plus de double défilement, <code>showValues</code> mort supprimé, largeur <code>0,85</code> conservée. Lot 1 : <code>text_field_container</code> sans <code>width: double.infinity</code> ni rayon 29, compteur discret dans <code>rounded_input_field</code>. Lot 2 : libellé au-dessus et plus d'auto-centrage dans <code>string_</code>/<code>multi_string_</code>/<code>resource_</code>/<code>check_input_container</code>. Lot 3 : grille responsive de sections, réordonnancement par <code>Draggable</code>/<code>DragTarget</code> (<code>ReorderableListView</code> ne fait pas de grille, et pas de dépendance nouvelle), tuile « Ajouter » à la place du bouton vert. Lot 4 : les deux écrans de détail en <code>EditorHeader</code> + <code>EditorColumns</code> + <code>Pane</code> (nouveau <code>Components/pane.dart</code>), <code>_buildFooter</code> supprimé, le QR devient le rail « Diffusion ». Lot 5 : grille de types dans <code>new_section_popup</code>, modal de traduction en rail de langues + <code>IndexedStack</code>, et <code>multi_input_modal</code> réduit à une redirection <code>isHTML: false</code>. 24 clés i18n ajoutées en FR/EN/NL.</p>
<p>🐛 <strong>Le bug des catégories est confirmé et corrigé</strong> : <code>showCreateOrUpdateCategories</code> recevait un <code>newValues</code> vide de l'appelant et le renvoyait tel quel — valider sans rien toucher <em>effaçait</em> les catégories. Le tampon part maintenant des catégories existantes, et les libellés en dur sont passés par <code>AppLocalizations</code>.</p>
<p><strong>Lot 6 entamé</strong> : <code>Components/collection_editor.dart</code> existe — liste ordonnée à gauche (réordonnancement par poignée), élément édité en place à droite, ligne « Ajouter » en fin de liste, suppression dans l'en-tête du détail. Trois types migrés : <strong>Slider</strong> et <strong>Article</strong> (via un <code>ContentFields</code> partagé, extrait de <code>showNewOrUpdateContentSlider</code>) et <strong>PDF</strong> (le bloc figé <code>70 × 440</code> disparaît). Les filtres d'envoi au backend sont conservés à l'identique (contenus sans ressource exclus, champs d'article recopiés).</p>
<p>⚠️ <strong>Trois fichiers sont devenus du code mort mais ne sont pas supprimés</strong> : <code>pdf_file_input_container.dart</code>, <code>pdf_list.dart</code>, <code>new_update_pdfFile.dart</code>. Ils portent des modifications non committées d'un autre lot — les effacer perdrait ce travail. À supprimer une fois l'arbre propre.</p>
<p><strong>Lot 6 terminé pour le gabarit B.</strong> Décision prise le 04/09 (artifact <code>4af75171</code>) : <code>CollectionEditor</code> reçoit un <strong>mode distant</strong> (<code>RemoteCollection</code> : <code>create</code> / <code>delete</code> / <code>reorder</code> asynchrones, voile de chargement, retour à l'état d'avant si l'appel échoue). <strong>Quiz</strong> l'utilise : la modale de question disparaît, les messages de score remontent en tête de carte, les réponses gardent <code>QuizzResponseList</code> tel quel. <strong>Menu</strong> garde la navigation vers <code>sub_section_edit_screen</code> — une sous-section est une section complète — et prend la grille de tuiles, extraite en <code>SectionGrid</code> et partagée avec les sections d'une configuration. <strong>Parcours</strong> : liste qui suit son contenu, les deux <code>SizedBox(height: 500)</code> de <code>event_config</code> et <code>section_parcours_config</code> retirés, éditeur à fenêtre unique inchangé.</p>
<p>🐛 <strong>Le rang en double est corrigé</strong> dans Quiz et Menu : ils n'écrivaient que l'<code>order</code> de l'élément déplacé, les autres gardaient le leur. La liste entière est renumérotée et envoyée en un <code>Future.wait</code>, comme le faisait déjà Parcours.</p>
<p><strong>Agenda n'a pas été migré, et c'est délibéré</strong> : ses événements sont groupés par langue (<code>_displayGroups</code>, pastilles de langue quand un groupe en compte plusieurs) et n'ont pas d'ordre — une liste ordonnée à édition en place perdrait le groupement. Il reçoit ce qui lui revient : hauteur pilotée par le contenu (le <code>height: 600</code> saute), lignes au système de tokens, modale conservée.</p>
<p><strong>Gabarit C — Jeu est fait, Map et Événement sont dégrossis.</strong> <code>game_config</code> devient champs à gauche + canevas à droite : le canevas montre le découpage réel de l'image en <code>rows × cols</code>, seule décision que le formulaire ne montrait pas. Au passage, son <code>TabBar</code> n'en était pas un — les deux onglets construisaient le même formulaire et ne servaient qu'à écrire <code>gameType</code> : c'est devenu un choix à deux valeurs, et le <code>SizedBox(height: 650)</code> a disparu. <code>geoloc_input_container</code> passe à la coquille standard et affiche les coordonnées retenues au lieu d'un bouton « Changer la localisation », la carte prenant la hauteur disponible. Côté <code>map_config</code> : en-tête en grille de tokens, <code>height: 700</code> et <code>height: 70</code> retirés. Côté <code>event_config</code> : <code>height: 600</code> retiré, la liste des blocs suit son contenu.</p>
<p><strong>Reste : le canevas de Map et d'Événement — et il est maintenant maquetté</strong> : <code>DOCS/claude design/refonte-gabarit-canevas.html</code> (diffusion : artifact <code>caf400b5</code>). Liste des points, fiche, carte côte à côte ; les points non sélectionnés restent visibles en gris ; <code>MapGeometryPicker</code> sort de son dialogue pour devenir le canevas (déplacement, pas écriture) ; les informations pratiques d'un point (5 champs sur 9) sont repliées derrière un compteur. Absorbe <code>showNewOrUpdateGeoPoint</code> (362 l.), <code>geometry_input_container</code> et <code>showNewOrUpdateMapAnnotation</code> (182 l.). <strong>Une question produit reste ouverte</strong> : que montre le canevas d'Événement quand <code>baseSectionMapId</code> est nul — carte vide, ou invitation à choisir un plan d'abord (ma proposition).</p>
<p><strong>Étape 1 du canevas faite le 04/09</strong> : <code>Components/map_canvas.dart</code> — le dessin est sorti du dialogue. Il reçoit une géométrie, la rend modifiable, rend la main <em>à chaque changement</em> (pas de bouton « Enregistrer » : c'est l'écran qui enregistre), et affiche les autres tracés en fond via <code>MapCanvasGhost</code>. Barre d'outils compacte sur la carte, pied qui donne les coordonnées et ce qu'on modifie, bouton « agrandir » optionnel. <code>MapGeometryPicker</code> n'est plus qu'une coquille autour de lui, donc <code>etape_fields</code> et <code>showNewOrUpdateEventAgenda</code> ne bougent pas. Au passage, ses libellés « Ligne » / « Polygone » en dur sont passés par <code>AppLocalizations</code>. <strong>Taille tranchée</strong> : 420 px par défaut + mode agrandi, pas une interface centrée carte — un point porte neuf champs traduits, la carte n'en porte qu'un.</p>
<p><strong>Étapes 2 à 4 faites : le gabarit C est en place dans Map.</strong> <code>Map/geo_point_editor.dart</code> — liste des points à gauche, fiche au centre, carte à droite ; les points non sélectionnés apparaissent en fond ; le mode agrandi replie la fiche et garde le rail. Les neuf champs traduits sont rangés par fréquence : titre, catégorie, description et image visibles, les cinq informations pratiques repliées derrière un compteur. <code>map_config</code> passe de 937 à ~510 lignes : la modale <code>showNewOrUpdateGeoPoint</code> n'est plus appelée, <code>getElement</code> et son <code>boxDecoration</code> sont supprimés, et recherche + catégories filtrent désormais la liste <em>et</em> la carte par un seul calcul. Les points sont créés, modifiés et supprimés directement par l'API (<code>sectionMapCreate</code> / <code>Update</code> / <code>Delete</code>), la suppression passant par la confirmation destructive.</p>
<p><strong>Étape 5 faite : le canevas d'Événement.</strong> Les annotations passent dans l'éditeur de collection en mode distant, avec le canevas dans le panneau d'édition. <strong>Plan par défaut</strong> retenu (décision Thomas) : le premier plan disponible est pris quand <code>baseSectionMapId</code> est nul, plutôt que de bloquer. 🐛 <strong>Trouvé en chemin</strong> : <code>MapAnnotationDTO</code> portait déjà un champ <code>geometry</code> que l'ancienne modale ne remplissait <em>jamais</em> — elle ne réglait que <code>geometryType</code>. Une annotation déclarait donc une forme sans jamais recevoir de coordonnées ; le canevas les lui donne. Enfin <code>showMultiStringInputAndResourceHTML</code> est passé à la coquille standard : plus aucun dialogue de ce chantier sur l'ancienne.</p>
<p><strong>Plan de test</strong> : <code>DOCS/test-plan-refonte-config-section.md</code>, 14 sections. Rien n'a été vérifié à l'écran — <code>flutter analyze</code> propre et <code>flutter build web</code> qui passe, c'est tout.</p>
<p><strong>Reste</strong> : supprimer le code mort une fois l'arbre git propre — <code>showNewOrUpdateGeoPoint</code>, <code>showNewOrUpdateMapAnnotation</code>, <code>pdf_file_input_container</code>, <code>pdf_list</code>, <code>new_update_pdfFile</code>, <code>listView_card_subSection</code>, <code>new_update_question_quizz</code> ; et <code>geometry_input_container</code> quand <code>etape_fields</code> et <code>showNewOrUpdateEventAgenda</code> auront leur canevas.</p> Restent aussi <code>showMultiStringInputAndResourceHTML</code> sur l'ancienne coquille, et le code mort à supprimer une fois l'arbre propre : <code>pdf_file_input_container</code>, <code>pdf_list</code>, <code>new_update_pdfFile</code>, <code>listView_card_subSection</code>, <code>new_update_question_quizz</code>.<br>⚠️ <strong>À vérifier manuellement en priorité</strong> : les deux chemins qui recréent les contrôleurs Quill (traduction IA, « appliquer à toutes les langues ») ; les hauteurs figées restées autour des champs devenus plus hauts (<code>height: 70</code>, <code>100</code>) dans les écrans non repris — <code>showNewOrUpdateGeoPoint</code>, <code>showNewOrUpdateEventAgenda</code>, <code>sub_section_edit_screen</code>, <code>new_configuration_popup</code>.</p>
<p><strong>Maquette prête et complète</strong> : <code>DOCS/claude design/refonte-configuration-sections.html</code> (diffusion : artifact <code>4d0c2ad6</code>). Elle contient le diagnostic, les deux écrans, les 3 gabarits qui couvrent les 13 types de section, la coquille de dialogue, et deux dialogues dessinés — traduction et choix du type. <strong>Rien à décider côté design : tout est tranché.</strong></p>
<p><strong>Le blanc ne vient pas des écrans mais de 4 composants partagés.</strong> <code>text_field_container.dart</code> impose <code>width: double.infinity</code> + rayon 29 ; <code>string_input_container.dart</code> met le label à gauche dans un <code>Row</code> ; <code>multi_string_input_container</code> et <code>resource_input_container</code> se centrent eux-mêmes ; <code>section_reorderList.dart</code> réserve <code>0,3 × hauteur</code> pour une liste horizontale de cartes 150×150. On les corrige une fois, les 13 types en héritent.</p>
<p>⚠️ <strong>Aucune direction visuelle à inventer</strong> : <code>constants.dart</code> porte déjà le système (<code>kSpace1..8</code>, <code>kRadiusInput</code> 5 / <code>Card</code> 8 / <code>Shell</code> 10 / <code>Pill</code>, <code>kTitleCard</code>, <code>kLabelField</code>, <code>kTextHint</code>) et <code>confirmation_dialog.dart</code> + <code>resource_picker.dart</code> l'appliquent déjà. Ces deux écrans sont simplement <em>antérieurs</em> au système.</p>
<p><strong>Premier pas conseillé : <code>select_resource_modal.dart</code>, pas les composants partagés.</strong> Un fichier, ~15 lignes, zéro ligne dans <code>ResourcesScreen</code>, et ça corrige un vrai bug — <code>AlertDialog</code> dispose de <code>hauteur 48</code> mais le contenu en demande <code>0,85 × hauteur</code> plus titre plus actions, d'où un défilement dans un défilement. Ça sert de pilote avant de toucher aux composants qui irriguent 30 écrans. ⛔ <strong>Ne pas toucher à la largeur <code>0,85</code></strong> : la règle du rail de facettes de <code>v1-mediatheque-plan.md</code> §3.3 est écrite sur cette hypothèse et déjà codée.</p>
<p>⚠️ <strong>Deux pièges qui ne sont pas du visuel.</strong> D'abord, <code>manager-app</code> n'a que 3 tests, aucun sur ces composants : vérification manuelle, et il reste des conteneurs à hauteur figée (<code>height: 70</code>, <code>100</code>, <code>SizedBox(height: 250)</code>) autour — ce qui rentrait juste débordera. Ensuite un chemin d'écriture est câblé dans les composants : <code>section_detail_screen.dart:270</code> déclenche un <code>save(true, …)</code> depuis <code>MultiStringInputContainer.onGetResult</code>, et la ligne 381 un <code>save(false, …)</code> depuis la case iBeacon. À ne pas perdre ni dédoubler.</p>
<p>⚠️ <strong>Le piège Quill</strong> (remonté de mémoire par Thomas, confirmé dans le code). Ça marche aujourd'hui <em>parce que</em> l'empilement vertical monte toutes les langues : <code>_controllers</code> est bâtie une fois dans <code>initState</code> (<code>translation_input_container.dart:52</code>) et libérée seulement dans <code>dispose</code>. Le rail de langues de la maquette casse cette propriété s'il reconstruit l'éditeur à chaque bascule. <strong>Règle : map construite une fois pour toutes les langues, bascule par <code>IndexedStack</code>/<code>Offstage</code>, jamais de <code>TabBarView</code></strong> (construction paresseuse, enfants lâchés hors écran sans <code>AutomaticKeepAliveClientMixin</code>). Trace d'une tentative abandonnée : <code>translation_tab.dart</code> existe en <code>TabBar</code>, son appel est en commentaire dans <code>multi_input_modal.dart:46-51</code>. À retester en priorité : la traduction IA (ligne 142) et « appliquer à toutes les langues » (ligne 183), les deux chemins qui libèrent et recréent les contrôleurs.</p>
<p><strong>Deux fusions et un ménage</strong> décidés dans la maquette : <code>multi_input_modal.dart</code> est le jumeau sans HTML du modal de traduction (même travail, deux implémentations) → une coquille, deux corps, Quill si <code>isHTML</code> et champ simple sinon ; les 8 <code>showNewOrUpdate*</code> et les 2 sélecteurs géo disparaissent dans les gabarits B et C ; <code>showValues</code> est mort en double (<code>select_resource_modal.dart:64</code> et <code>multi_input_modal.dart:186</code>, appel commenté ligne 53).</p>
<p>⚠️ <strong>Conflit résolu avec l'audit</strong><code>security/audit-manager-app.md</code> recommandait d'extraire un <code>FormDialogScaffold</code> pour les 15 occurrences des <code>showNewOrUpdate*</code>. Or la refonte les <strong>supprime</strong> : les extraire d'abord, ce serait refactoriser du code à jeter. <strong>Ordre imposé : absorber d'abord (gabarits B et C), extraire ensuite sur ce qui survit</strong> — traduction, nouvelle section, nouvelle configuration, sélecteur de couleur, fichiers PDF, catégories. L'audit et <code>STATUS.md</code> ont été annotés en ce sens le 2026-09-04.</p>
<p>🐛 <strong>Trouvé en passant, à vérifier</strong> : <code>pdf_file_input_container.dart</code> et <code>category_input_container.dart</code> sont un copier-coller — mêmes dimensions, même structure — mais le second a ses libellés en dur (« Annuler », « Valider ») et son <code>onGetResult(result)</code> <strong>commenté</strong>. Une catégorie de carte modifiée ne remonte peut-être pas à la volée. Lecture de code, pas testé.</p>
<span class="src">DOCS/claude design/refonte-configuration-sections.html</span>
</article>
<article class="card" data-area="backend" data-horizon="v2">
<div class="card-meta"><span class="tag">manager-service</span><span class="tag">Studio IA</span><span class="flag f-critical">Prérequis dur</span></div>
<h3>Socle de stockage serveur — le backend ne sait pas écrire dans le bucket</h3>
@ -908,6 +990,17 @@
<span class="src">v2/vr-quest-unity-plan.md §1, §2 (lots V-1 à V-3)</span>
</article>
<article class="card" data-area="produit" data-horizon="v2">
<div class="card-meta"><span class="tag">studio</span><span class="tag">video</span><span class="tag">IA</span><span class="flag f-good">quelques euros, une soirée — débloque une décision produit</span></div>
<h3>Test « faire vivre un lieu » — 8 s au Fourneau</h3>
<p><strong>Une photo d'un bâtiment du Fourneau → sa version d'époque → un personnage en boucle de 8 s.</strong> Le but n'est pas de produire un asset, c'est de répondre à la seule question dont dépend tout le reste : <strong>est-ce que ça a l'air crédible, ou est-ce que ça décroche ?</strong> En patrimoine la crédibilité <em>est</em> la valeur — si le rendu est inquiétant, la fonctionnalité n'existe pas, quels que soient le prix et la facilité.</p>
<p><strong>Le principe à valider</strong> : le text-to-video pur est écarté (il invente la géométrie du lieu). Le lieu réel reste la base, l'IA n'ajoute que le mouvement — c'est « Point de départ » du Studio transposé à la vidéo, donc <strong>aucun mécanisme nouveau en base</strong> : <code>Kind = Video</code> et <code>sourceFidelity</code> existent déjà, c'est un gabarit de plus.</p>
<p><strong>Respecter les contraintes pendant le test, sinon il ne prouve rien</strong> : 6-10 s en boucle, <strong>une seule personne</strong>, mouvements simples, caméra calme, plan large ou moyen, <strong>jamais de gros plan</strong> sur les mains ou le visage.</p>
<p>⚠️ <strong>Deux choses déjà tranchées, à ne pas rejouer.</strong> Le <strong>motion transfer part en prestation Unov</strong>, pas dans le produit : il demande un tournage et un consentement écrit, ce qui échoue par construction le critère du §0 (« un conservateur produit sans intervention »). Et le cadrage de vente est resserré à <strong>« là où il ne reste rien »</strong> — bâtiment disparu, métier éteint, site réduit à ses fondations : partout où des archives existent, une vraie photo d'époque bat une boucle générée.</p>
<p><strong>Ne bloque rien et n'est bloqué par rien</strong> — c'est justement l'intérêt de le faire tôt. Mais la fonctionnalité, elle, reste derrière le prérequis dur du Studio : <code>IResourceBlobService</code> n'expose toujours que <code>IsConfigured</code> et <code>DeleteAsync</code>, le backend ne sait pas écrire dans le bucket.</p>
<span class="src">v2/studio-plan.md §3.3 — décidé le 04/09, section conditionnelle</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é · go/no-go</span></div>
<h3>Meta Quest — app Unity + Meta XR SDK</h3>

View File

@ -0,0 +1,12 @@
---
title: La taille du téléchargement n'est jamais affichée
area: visitapp
horizon: v1
tags: visitapp, offline, store
flag: critical | condition de publication sur le Play Store
src: relevé le 2026-09-04 — configurations_list.dart, downloadConfiguration.dart
---
<p><strong>Le visiteur ne sait jamais combien il télécharge.</strong> Le dialogue de <code>configurations_list.dart</code> annonce « il est nécessaire de télécharger du contenu supplémentaire » sans chiffre, et l'écran de progression n'affiche qu'un pourcentage. Or une visite du Fourneau, c'est un ordre de grandeur de plusieurs centaines de Mo sur la 4G du visiteur.</p>
<p><strong>Google Play exige que la taille des téléchargements complémentaires soit annoncée avant de les lancer</strong> — c'est une condition de publication, pas un confort.</p>
<p><strong>Rien à ajouter côté serveur</strong> : <code>ResourceDTO.sizeBytes</code> existe déjà et arrive dans <code>ExportConfigurationDTO.resources</code>. La somme se calcule côté client, et sur les seules ressources réellement à télécharger — <code>isResourceOutdated</code> filtre déjà celles qui sont à jour, donc une mise à jour de visite doit annoncer le <em>delta</em>, pas le total.</p>
<p><strong>Deux endroits</strong> : le dialogue de confirmation (avant), et l'écran de progression (« 128 / 412 Mo » plutôt qu'un pourcentage nu). ⚠️ La taille annoncée dans le dialogue suppose d'avoir déjà appelé <code>configurationExport</code> pour connaître la liste — aujourd'hui cet appel n'a lieu qu'<em>après</em> la confirmation.</p>

View File

@ -0,0 +1,13 @@
---
title: Carte hors ligne — tile packs Mapbox
area: visitapp
horizon: v2
tags: visitapp, carto, offline
flag: warn | prérequis pour vendre un parcours GPS sur un site sans réseau
src: analyse du code le 2026-09-04 — MapProvider, flutter_map, mapbox_maps_flutter
---
<p><strong>Aujourd'hui aucune carte ne fonctionne hors ligne</strong>, et ce n'est pas une question de données : <code>MapDTO</code> porte déjà <code>points</code>, <code>centerLatitude</code>, <code>zoom</code> et <code>iconResourceId</code>, et les icônes sont téléchargées avec la visite. C'est le <strong>fond</strong> qui manque — les tuiles sont chargées en réseau, sans cache.</p>
<p><strong>Le GPS, lui, n'est pas le problème</strong> : <code>geolocator ^13.0.0</code> lit le GNSS, qui est autonome. Seule réserve à annoncer au client : sans A-GPS, le premier point peut demander 30 à 60 s sous couvert forestier.</p>
<p><strong>Le chemin court existe déjà dans le pubspec</strong> : <code>mapbox_maps_flutter ^2.0.0</code> expose <code>OfflineManager</code> (style packs) et <code>TileStore</code> (tile regions) — on télécharge une emprise bornée au moment du téléchargement de la visite. Pour un domaine comme le Fourneau Saint-Michel, quelques dizaines de Mo. <code>MapProvider.MapBox</code> existe déjà côté modèle.</p>
<p><strong>Google est une impasse, et pas seulement techniquement.</strong> Le SDK Maps pour Android n'expose aucune API de tuiles hors ligne — les « zones hors connexion » sont une fonction de l'app Google Maps, pas du SDK intégrable. Et le code n'utilise même pas ce SDK pour les sections carte : il passe par <code>flutter_map</code> sur <code>https://mt1.google.com/vt/lyrs=m&amp;x={x}&amp;y={y}&amp;z={z}</code>, un endpoint non documenté dont la mise en cache est explicitement interdite. <strong>Conclusion : Mapbox devient le fournisseur du mode hors ligne, Google reste en ligne seulement.</strong></p>
<p>⚠️ Tant que ce chantier n'est pas fait, <code>SectionType.Map</code> reste volontairement hors de <code>offlineCapableSectionTypes</code> (<code>downloadConfiguration.dart</code>) : l'ajouter donnerait une carte grise, ce qui est pire que ne pas la proposer. Une fois les tuiles packagées, c'est <strong>une ligne à ajouter dans cette constante</strong>.</p>

View File

@ -0,0 +1,12 @@
---
title: Plan illustré géoréférencé — un 3<sup>e</sup> fournisseur de carte
area: manager backend visitapp
horizon: v2
tags: carto, offline, manager-app
flag: good | hors ligne par construction, et c'est ce qu'un musée dessine déjà
src: analyse du code le 2026-09-04 — MapProvider n'a que Google et MapBox
---
<p><strong>Un musée de plein air ne distribue pas un fond OpenStreetMap, il distribue son plan dessiné.</strong> Aujourd'hui <code>MapProvider</code> ne connaît que <code>Google</code> et <code>MapBox</code> : il n'existe aucun moyen de téléverser une image de plan et d'y placer les points.</p>
<p><strong>Ce qu'il faut</strong> : une troisième valeur de <code>MapProvider</code>, une ressource image portée par la section, et un calage — deux points d'ancrage suffisent (coin haut-gauche / bas-droit en coordonnées réelles) pour projeter un <code>GeoPoint</code> sur l'image et y afficher la position du visiteur.</p>
<p><strong>Pourquoi c'est le meilleur des trois</strong> : aucune tuile, donc <strong>hors ligne par construction</strong> — l'image part avec la visite comme n'importe quelle ressource, et le mécanisme de téléchargement n'a rien de nouveau à apprendre. Le rendu est aussi plus lisible qu'un fond routier sur un site de plusieurs dizaines de bâtiments.</p>
<p><strong>Périmètre réel</strong> : c'est le plus gros des trois chemins carto — backend (modèle + migration), manager-app (téléversement du plan et pose des ancres), <code>mymuseum-visitapp</code> et <code>visitapp-web</code> (rendu). À faire après <em>Carte hors ligne — tile packs Mapbox</em>, qui débloque le besoin immédiat avec un fond classique.</p>

View File

@ -0,0 +1,53 @@
---
title: Refonte visuelle des écrans Configuration &amp; Section, et de leurs dialogues
area: manager
horizon: v1
tags: manager-app, UI, dialogues
flag: good | Maquette prête, code à faire
src: DOCS/claude design/refonte-configuration-sections.html
---
<p><strong>Lots 1 à 5 codés le 04/09</strong> (<code>flutter analyze</code> sans erreur, <code>flutter build web</code> passe ; <strong>rien de vérifié à l'écran</strong>). Pilote <code>select_resource_modal</code> : coquille standard, corps souple, plus de double défilement, <code>showValues</code> mort supprimé, largeur <code>0,85</code> conservée. Lot 1 : <code>text_field_container</code> sans <code>width: double.infinity</code> ni rayon 29, compteur discret dans <code>rounded_input_field</code>. Lot 2 : libellé au-dessus et plus d'auto-centrage dans <code>string_</code>/<code>multi_string_</code>/<code>resource_</code>/<code>check_input_container</code>. Lot 3 : grille responsive de sections, réordonnancement par <code>Draggable</code>/<code>DragTarget</code> (<code>ReorderableListView</code> ne fait pas de grille, et pas de dépendance nouvelle), tuile « Ajouter » à la place du bouton vert. Lot 4 : les deux écrans de détail en <code>EditorHeader</code> + <code>EditorColumns</code> + <code>Pane</code> (nouveau <code>Components/pane.dart</code>), <code>_buildFooter</code> supprimé, le QR devient le rail « Diffusion ». Lot 5 : grille de types dans <code>new_section_popup</code>, modal de traduction en rail de langues + <code>IndexedStack</code>, et <code>multi_input_modal</code> réduit à une redirection <code>isHTML: false</code>. 24 clés i18n ajoutées en FR/EN/NL.</p>
<p>🐛 <strong>Le bug des catégories est confirmé et corrigé</strong> : <code>showCreateOrUpdateCategories</code> recevait un <code>newValues</code> vide de l'appelant et le renvoyait tel quel — valider sans rien toucher <em>effaçait</em> les catégories. Le tampon part maintenant des catégories existantes, et les libellés en dur sont passés par <code>AppLocalizations</code>.</p>
<p><strong>Lot 6 entamé</strong> : <code>Components/collection_editor.dart</code> existe — liste ordonnée à gauche (réordonnancement par poignée), élément édité en place à droite, ligne « Ajouter » en fin de liste, suppression dans l'en-tête du détail. Trois types migrés : <strong>Slider</strong> et <strong>Article</strong> (via un <code>ContentFields</code> partagé, extrait de <code>showNewOrUpdateContentSlider</code>) et <strong>PDF</strong> (le bloc figé <code>70 × 440</code> disparaît). Les filtres d'envoi au backend sont conservés à l'identique (contenus sans ressource exclus, champs d'article recopiés).</p>
<p>⚠️ <strong>Trois fichiers sont devenus du code mort mais ne sont pas supprimés</strong> : <code>pdf_file_input_container.dart</code>, <code>pdf_list.dart</code>, <code>new_update_pdfFile.dart</code>. Ils portent des modifications non committées d'un autre lot — les effacer perdrait ce travail. À supprimer une fois l'arbre propre.</p>
<p><strong>Lot 6 terminé pour le gabarit B.</strong> Décision prise le 04/09 (artifact <code>4af75171</code>) : <code>CollectionEditor</code> reçoit un <strong>mode distant</strong> (<code>RemoteCollection</code> : <code>create</code> / <code>delete</code> / <code>reorder</code> asynchrones, voile de chargement, retour à l'état d'avant si l'appel échoue). <strong>Quiz</strong> l'utilise : la modale de question disparaît, les messages de score remontent en tête de carte, les réponses gardent <code>QuizzResponseList</code> tel quel. <strong>Menu</strong> garde la navigation vers <code>sub_section_edit_screen</code> — une sous-section est une section complète — et prend la grille de tuiles, extraite en <code>SectionGrid</code> et partagée avec les sections d'une configuration. <strong>Parcours</strong> : liste qui suit son contenu, les deux <code>SizedBox(height: 500)</code> de <code>event_config</code> et <code>section_parcours_config</code> retirés, éditeur à fenêtre unique inchangé.</p>
<p>🐛 <strong>Le rang en double est corrigé</strong> dans Quiz et Menu : ils n'écrivaient que l'<code>order</code> de l'élément déplacé, les autres gardaient le leur. La liste entière est renumérotée et envoyée en un <code>Future.wait</code>, comme le faisait déjà Parcours.</p>
<p><strong>Agenda n'a pas été migré, et c'est délibéré</strong> : ses événements sont groupés par langue (<code>_displayGroups</code>, pastilles de langue quand un groupe en compte plusieurs) et n'ont pas d'ordre — une liste ordonnée à édition en place perdrait le groupement. Il reçoit ce qui lui revient : hauteur pilotée par le contenu (le <code>height: 600</code> saute), lignes au système de tokens, modale conservée.</p>
<p><strong>Gabarit C — Jeu est fait, Map et Événement sont dégrossis.</strong> <code>game_config</code> devient champs à gauche + canevas à droite : le canevas montre le découpage réel de l'image en <code>rows × cols</code>, seule décision que le formulaire ne montrait pas. Au passage, son <code>TabBar</code> n'en était pas un — les deux onglets construisaient le même formulaire et ne servaient qu'à écrire <code>gameType</code> : c'est devenu un choix à deux valeurs, et le <code>SizedBox(height: 650)</code> a disparu. <code>geoloc_input_container</code> passe à la coquille standard et affiche les coordonnées retenues au lieu d'un bouton « Changer la localisation », la carte prenant la hauteur disponible. Côté <code>map_config</code> : en-tête en grille de tokens, <code>height: 700</code> et <code>height: 70</code> retirés. Côté <code>event_config</code> : <code>height: 600</code> retiré, la liste des blocs suit son contenu.</p>
<p><strong>Reste : le canevas de Map et d'Événement — et il est maintenant maquetté</strong> : <code>DOCS/claude design/refonte-gabarit-canevas.html</code> (diffusion : artifact <code>caf400b5</code>). Liste des points, fiche, carte côte à côte ; les points non sélectionnés restent visibles en gris ; <code>MapGeometryPicker</code> sort de son dialogue pour devenir le canevas (déplacement, pas écriture) ; les informations pratiques d'un point (5 champs sur 9) sont repliées derrière un compteur. Absorbe <code>showNewOrUpdateGeoPoint</code> (362 l.), <code>geometry_input_container</code> et <code>showNewOrUpdateMapAnnotation</code> (182 l.). <strong>Une question produit reste ouverte</strong> : que montre le canevas d'Événement quand <code>baseSectionMapId</code> est nul — carte vide, ou invitation à choisir un plan d'abord (ma proposition).</p>
<p><strong>Étape 1 du canevas faite le 04/09</strong> : <code>Components/map_canvas.dart</code> — le dessin est sorti du dialogue. Il reçoit une géométrie, la rend modifiable, rend la main <em>à chaque changement</em> (pas de bouton « Enregistrer » : c'est l'écran qui enregistre), et affiche les autres tracés en fond via <code>MapCanvasGhost</code>. Barre d'outils compacte sur la carte, pied qui donne les coordonnées et ce qu'on modifie, bouton « agrandir » optionnel. <code>MapGeometryPicker</code> n'est plus qu'une coquille autour de lui, donc <code>etape_fields</code> et <code>showNewOrUpdateEventAgenda</code> ne bougent pas. Au passage, ses libellés « Ligne » / « Polygone » en dur sont passés par <code>AppLocalizations</code>. <strong>Taille tranchée</strong> : 420 px par défaut + mode agrandi, pas une interface centrée carte — un point porte neuf champs traduits, la carte n'en porte qu'un.</p>
<p><strong>Étapes 2 à 4 faites : le gabarit C est en place dans Map.</strong> <code>Map/geo_point_editor.dart</code> — liste des points à gauche, fiche au centre, carte à droite ; les points non sélectionnés apparaissent en fond ; le mode agrandi replie la fiche et garde le rail. Les neuf champs traduits sont rangés par fréquence : titre, catégorie, description et image visibles, les cinq informations pratiques repliées derrière un compteur. <code>map_config</code> passe de 937 à ~510 lignes : la modale <code>showNewOrUpdateGeoPoint</code> n'est plus appelée, <code>getElement</code> et son <code>boxDecoration</code> sont supprimés, et recherche + catégories filtrent désormais la liste <em>et</em> la carte par un seul calcul. Les points sont créés, modifiés et supprimés directement par l'API (<code>sectionMapCreate</code> / <code>Update</code> / <code>Delete</code>), la suppression passant par la confirmation destructive.</p>
<p><strong>Étape 5 faite : le canevas d'Événement.</strong> Les annotations passent dans l'éditeur de collection en mode distant, avec le canevas dans le panneau d'édition. <strong>Plan par défaut</strong> retenu (décision Thomas) : le premier plan disponible est pris quand <code>baseSectionMapId</code> est nul, plutôt que de bloquer. 🐛 <strong>Trouvé en chemin</strong> : <code>MapAnnotationDTO</code> portait déjà un champ <code>geometry</code> que l'ancienne modale ne remplissait <em>jamais</em> — elle ne réglait que <code>geometryType</code>. Une annotation déclarait donc une forme sans jamais recevoir de coordonnées ; le canevas les lui donne. Enfin <code>showMultiStringInputAndResourceHTML</code> est passé à la coquille standard : plus aucun dialogue de ce chantier sur l'ancienne.</p>
<p><strong>Plan de test</strong> : <code>DOCS/test-plan-refonte-config-section.md</code>, 14 sections. Rien n'a été vérifié à l'écran — <code>flutter analyze</code> propre et <code>flutter build web</code> qui passe, c'est tout.</p>
<p><strong>Reste</strong> : supprimer le code mort une fois l'arbre git propre — <code>showNewOrUpdateGeoPoint</code>, <code>showNewOrUpdateMapAnnotation</code>, <code>pdf_file_input_container</code>, <code>pdf_list</code>, <code>new_update_pdfFile</code>, <code>listView_card_subSection</code>, <code>new_update_question_quizz</code> ; et <code>geometry_input_container</code> quand <code>etape_fields</code> et <code>showNewOrUpdateEventAgenda</code> auront leur canevas.</p> Restent aussi <code>showMultiStringInputAndResourceHTML</code> sur l'ancienne coquille, et le code mort à supprimer une fois l'arbre propre : <code>pdf_file_input_container</code>, <code>pdf_list</code>, <code>new_update_pdfFile</code>, <code>listView_card_subSection</code>, <code>new_update_question_quizz</code>.<br>⚠️ <strong>À vérifier manuellement en priorité</strong> : les deux chemins qui recréent les contrôleurs Quill (traduction IA, « appliquer à toutes les langues ») ; les hauteurs figées restées autour des champs devenus plus hauts (<code>height: 70</code>, <code>100</code>) dans les écrans non repris — <code>showNewOrUpdateGeoPoint</code>, <code>showNewOrUpdateEventAgenda</code>, <code>sub_section_edit_screen</code>, <code>new_configuration_popup</code>.</p>
<p><strong>Maquette prête et complète</strong> : <code>DOCS/claude design/refonte-configuration-sections.html</code> (diffusion : artifact <code>4d0c2ad6</code>). Elle contient le diagnostic, les deux écrans, les 3 gabarits qui couvrent les 13 types de section, la coquille de dialogue, et deux dialogues dessinés — traduction et choix du type. <strong>Rien à décider côté design : tout est tranché.</strong></p>
<p><strong>Le blanc ne vient pas des écrans mais de 4 composants partagés.</strong> <code>text_field_container.dart</code> impose <code>width: double.infinity</code> + rayon 29 ; <code>string_input_container.dart</code> met le label à gauche dans un <code>Row</code> ; <code>multi_string_input_container</code> et <code>resource_input_container</code> se centrent eux-mêmes ; <code>section_reorderList.dart</code> réserve <code>0,3 × hauteur</code> pour une liste horizontale de cartes 150×150. On les corrige une fois, les 13 types en héritent.</p>
<p>⚠️ <strong>Aucune direction visuelle à inventer</strong> : <code>constants.dart</code> porte déjà le système (<code>kSpace1..8</code>, <code>kRadiusInput</code> 5 / <code>Card</code> 8 / <code>Shell</code> 10 / <code>Pill</code>, <code>kTitleCard</code>, <code>kLabelField</code>, <code>kTextHint</code>) et <code>confirmation_dialog.dart</code> + <code>resource_picker.dart</code> l'appliquent déjà. Ces deux écrans sont simplement <em>antérieurs</em> au système.</p>
<p><strong>Premier pas conseillé : <code>select_resource_modal.dart</code>, pas les composants partagés.</strong> Un fichier, ~15 lignes, zéro ligne dans <code>ResourcesScreen</code>, et ça corrige un vrai bug — <code>AlertDialog</code> dispose de <code>hauteur 48</code> mais le contenu en demande <code>0,85 × hauteur</code> plus titre plus actions, d'où un défilement dans un défilement. Ça sert de pilote avant de toucher aux composants qui irriguent 30 écrans. ⛔ <strong>Ne pas toucher à la largeur <code>0,85</code></strong> : la règle du rail de facettes de <code>v1-mediatheque-plan.md</code> §3.3 est écrite sur cette hypothèse et déjà codée.</p>
<p>⚠️ <strong>Deux pièges qui ne sont pas du visuel.</strong> D'abord, <code>manager-app</code> n'a que 3 tests, aucun sur ces composants : vérification manuelle, et il reste des conteneurs à hauteur figée (<code>height: 70</code>, <code>100</code>, <code>SizedBox(height: 250)</code>) autour — ce qui rentrait juste débordera. Ensuite un chemin d'écriture est câblé dans les composants : <code>section_detail_screen.dart:270</code> déclenche un <code>save(true, …)</code> depuis <code>MultiStringInputContainer.onGetResult</code>, et la ligne 381 un <code>save(false, …)</code> depuis la case iBeacon. À ne pas perdre ni dédoubler.</p>
<p>⚠️ <strong>Le piège Quill</strong> (remonté de mémoire par Thomas, confirmé dans le code). Ça marche aujourd'hui <em>parce que</em> l'empilement vertical monte toutes les langues : <code>_controllers</code> est bâtie une fois dans <code>initState</code> (<code>translation_input_container.dart:52</code>) et libérée seulement dans <code>dispose</code>. Le rail de langues de la maquette casse cette propriété s'il reconstruit l'éditeur à chaque bascule. <strong>Règle : map construite une fois pour toutes les langues, bascule par <code>IndexedStack</code>/<code>Offstage</code>, jamais de <code>TabBarView</code></strong> (construction paresseuse, enfants lâchés hors écran sans <code>AutomaticKeepAliveClientMixin</code>). Trace d'une tentative abandonnée : <code>translation_tab.dart</code> existe en <code>TabBar</code>, son appel est en commentaire dans <code>multi_input_modal.dart:46-51</code>. À retester en priorité : la traduction IA (ligne 142) et « appliquer à toutes les langues » (ligne 183), les deux chemins qui libèrent et recréent les contrôleurs.</p>
<p><strong>Deux fusions et un ménage</strong> décidés dans la maquette : <code>multi_input_modal.dart</code> est le jumeau sans HTML du modal de traduction (même travail, deux implémentations) → une coquille, deux corps, Quill si <code>isHTML</code> et champ simple sinon ; les 8 <code>showNewOrUpdate*</code> et les 2 sélecteurs géo disparaissent dans les gabarits B et C ; <code>showValues</code> est mort en double (<code>select_resource_modal.dart:64</code> et <code>multi_input_modal.dart:186</code>, appel commenté ligne 53).</p>
<p>⚠️ <strong>Conflit résolu avec l'audit</strong><code>security/audit-manager-app.md</code> recommandait d'extraire un <code>FormDialogScaffold</code> pour les 15 occurrences des <code>showNewOrUpdate*</code>. Or la refonte les <strong>supprime</strong> : les extraire d'abord, ce serait refactoriser du code à jeter. <strong>Ordre imposé : absorber d'abord (gabarits B et C), extraire ensuite sur ce qui survit</strong> — traduction, nouvelle section, nouvelle configuration, sélecteur de couleur, fichiers PDF, catégories. L'audit et <code>STATUS.md</code> ont été annotés en ce sens le 2026-09-04.</p>
<p>🐛 <strong>Trouvé en passant, à vérifier</strong> : <code>pdf_file_input_container.dart</code> et <code>category_input_container.dart</code> sont un copier-coller — mêmes dimensions, même structure — mais le second a ses libellés en dur (« Annuler », « Valider ») et son <code>onGetResult(result)</code> <strong>commenté</strong>. Une catégorie de carte modifiée ne remonte peut-être pas à la volée. Lecture de code, pas testé.</p>

View File

@ -0,0 +1,13 @@
---
title: Test « faire vivre un lieu » — 8 s au Fourneau
area: produit
horizon: v2
tags: studio, video, IA
flag: good | quelques euros, une soirée — débloque une décision produit
src: v2/studio-plan.md §3.3 — décidé le 04/09, section conditionnelle
---
<p><strong>Une photo d'un bâtiment du Fourneau → sa version d'époque → un personnage en boucle de 8 s.</strong> Le but n'est pas de produire un asset, c'est de répondre à la seule question dont dépend tout le reste : <strong>est-ce que ça a l'air crédible, ou est-ce que ça décroche ?</strong> En patrimoine la crédibilité <em>est</em> la valeur — si le rendu est inquiétant, la fonctionnalité n'existe pas, quels que soient le prix et la facilité.</p>
<p><strong>Le principe à valider</strong> : le text-to-video pur est écarté (il invente la géométrie du lieu). Le lieu réel reste la base, l'IA n'ajoute que le mouvement — c'est « Point de départ » du Studio transposé à la vidéo, donc <strong>aucun mécanisme nouveau en base</strong> : <code>Kind = Video</code> et <code>sourceFidelity</code> existent déjà, c'est un gabarit de plus.</p>
<p><strong>Respecter les contraintes pendant le test, sinon il ne prouve rien</strong> : 6-10 s en boucle, <strong>une seule personne</strong>, mouvements simples, caméra calme, plan large ou moyen, <strong>jamais de gros plan</strong> sur les mains ou le visage.</p>
<p>⚠️ <strong>Deux choses déjà tranchées, à ne pas rejouer.</strong> Le <strong>motion transfer part en prestation Unov</strong>, pas dans le produit : il demande un tournage et un consentement écrit, ce qui échoue par construction le critère du §0 (« un conservateur produit sans intervention »). Et le cadrage de vente est resserré à <strong>« là où il ne reste rien »</strong> — bâtiment disparu, métier éteint, site réduit à ses fondations : partout où des archives existent, une vraie photo d'époque bat une boucle générée.</p>
<p><strong>Ne bloque rien et n'est bloqué par rien</strong> — c'est justement l'intérêt de le faire tôt. Mais la fonctionnalité, elle, reste derrière le prérequis dur du Studio : <code>IResourceBlobService</code> n'expose toujours que <code>IsConfigured</code> et <code>DeleteAsync</code>, le backend ne sait pas écrire dans le bucket.</p>

View File

@ -9,7 +9,7 @@ Audit du 2026-07-13. Trois axes analysés : architecture/state management, écra
## Haute
- **Duplication du shell de dialog** — tous les `showNewOrUpdate*.dart` (Parcours, Event, Map, Agenda, Quiz) répètent le même squelette : `Dialog` + `Container` + calcul `contentWidth/halfWidth/thirdWidth` recopié à l'identique + ligne boutons Annuler/Sauvegarder (15 occurrences). → extraire un `FormDialogScaffold({title, child, onSave, onCancel, widthFactor})`.
- **Duplication du shell de dialog** — tous les `showNewOrUpdate*.dart` (Parcours, Event, Map, Agenda, Quiz) répètent le même squelette : `Dialog` + `Container` + calcul `contentWidth/halfWidth/thirdWidth` recopié à l'identique + ligne boutons Annuler/Sauvegarder (15 occurrences). → extraire un `FormDialogScaffold({title, child, onSave, onCancel, widthFactor})`. ⚠️ **Mis à jour le 2026-09-04 — le périmètre de cette extraction a changé.** La refonte des écrans Configuration/Section (`DOCS/claude design/refonte-configuration-sections.html` §6) **supprime** les 8 `showNewOrUpdate*` : ils sont absorbés par le panneau d'édition en place de l'éditeur de collection. Extraire un scaffold pour eux serait refactoriser du code à jeter. Le scaffold reste utile, mais pour les dialogues qui **survivent** — traduction, nouvelle section, nouvelle configuration, sélecteur de couleur, fichiers PDF, catégories de carte. Et la coquille de référence existe déjà : `confirmation_dialog.dart`. **Ordre : absorber d'abord, extraire ensuite sur ce qui reste.**
- **Composants quasi-copiés**`translation_input_container.dart` (365 l.) vs `translation_input_and_resource_container.dart` (347 l.) : ~80% identiques, mêmes méthodes aux mêmes numéros de ligne, seule différence la gestion du `resourceId`. Même schéma pour `multi_string_input_container.dart` vs `..._and_resource_container.dart`. → fusionner avec un flag `withResource`.
- **Méthodes/`build()` monolithiques** :
- `section_detail_screen.dart:455``save(bool isTraduction, AppContext)` fait ~440 lignes (455→894), logique métier (appels API, traduction, notifications) mêlée à l'UI.

View File

@ -0,0 +1,157 @@
# Plan de test — refonte Configuration & Sections (2026-09-04)
Périmètre : la refonte visuelle des écrans Configuration et Section, de leurs dialogues,
et le gabarit C de Map / Événement / Jeu. Maquettes : `DOCS/claude design/refonte-configuration-sections.html`
et `DOCS/claude design/refonte-gabarit-canevas.html`.
**Rien n'a été vérifié à l'écran** — `flutter analyze` est propre et `flutter build web` passe,
c'est tout. `manager-app` n'a que 3 tests et aucun sur ces composants : la vérification est manuelle.
Mode d'emploi : coche si le comportement attendu est constaté, laisse vide sinon et note l'écart
en bas. `→` = résultat attendu.
---
## §0. Porte d'entrée
- [ ] `flutter build web` passe
- [ ] `flutter analyze` ne remonte que les 3 problèmes connus de `test/widget_test.dart`
- [ ] L'app se lance, connexion OK → le champ e-mail et le mot de passe sont lisibles et espacés
## §1. Le champ partagé — le plus gros risque de régression
Ces composants sont importés par une trentaine d'écrans. **Ce qui rentrait juste peut déborder** :
un débordement Flutter est visible (rayures jaunes), pas silencieux.
- [ ] Écran Configuration : les libellés sont **au-dessus** des champs, pas à gauche
- [ ] Le compteur de caractères est petit et discret sous le champ
- [ ] Aucune bande jaune de débordement sur : Ressources, Instances, Utilisateurs, Clés API, Devices
- [ ] Les cases à cocher (Hors ligne, iBeacon…) sont compactes et alignées à gauche de leur libellé
- [ ] Un champ de ressource vide affiche « Choisir » sur fond clair bordé, pas une pastille pleine
## §2. Écran Configuration
- [ ] Les actions (Supprimer, Annuler, Enregistrer, Export) sont **dans l'en-tête**, plus de barre en bas
- [ ] Le sous-titre indique date de création · nombre de sections · nombre de langues
- [ ] Le nombre de sections est juste **après le chargement** de la liste (et pas 0)
- [ ] Deux colonnes : Réglages + Sections à gauche, Habillage à droite ; empilées sous 900 px
- [ ] Enregistrer, puis rouvrir : les valeurs sont bien persistées
## §3. La grille de sections
- [ ] Les sections sont en grille responsive, plus de liste horizontale
- [ ] Tuiles 16/9 avec badge d'ordre, image si présente, icône de type sinon
- [ ] Une section inactive porte le badge « Masquée » et est estompée
- [ ] Glisser une tuile par sa poignée (coin haut droit) la réordonne → l'ordre est sauvé
- [ ] La tuile « Ajouter une section » en fin de grille ouvre le dialogue de création
- [ ] La recherche filtre bien la grille
## §4. Dialogue « Nouvelle section »
- [ ] Les 13 types sont visibles, groupés en 4 familles, avec une description par type
- [ ] Sur une **sous-section**, le type Menu n'est pas proposé
- [ ] L'avertissement « le type ne pourra plus être changé » est dans le pied
- [ ] Créer sans nom (< 3 caractères) message d'erreur, pas de création
## §5. Écran Section
- [ ] En-tête : icône du type, pastille du type, actions ; plus de barre en bas
- [ ] Le panneau « Diffusion » est déplié dans le rail droit (QR, identifiant, iBeacon)
- [ ] Cliquer le QR le copie → notification de succès
- [ ] Cocher iBeacon **enregistre immédiatement** (chemin `save(false, …)`)
- [ ] Modifier le titre affiché **enregistre immédiatement** (chemin `save(true, …)`)
## §6. Modal de traduction — ⚠️ le piège Quill
Le point le plus fragile de la refonte. Les éditeurs sont montés en `IndexedStack` ;
deux chemins recréent les contrôleurs.
- [ ] Le rail de gauche liste les langues avec une pastille pleine / vide
- [ ] Basculer de langue **conserve** ce qui a été tapé dans l'autre langue
- [ ] La langue source s'affiche en référence au-dessus de l'éditeur
- [ ] Le compteur de caractères suit la frappe
- [ ] **« Appliquer à toutes les langues »** : le texte de la langue affichée part dans les autres, et l'éditeur montre le résultat
- [ ] **« Traduire avec l'IA »** : les langues manquantes se remplissent, l'éditeur montre le résultat, le quota se met à jour
- [ ] Après une traduction IA, retaper dans une langue traduite fonctionne encore
- [ ] Un champ **sans HTML** (ex. libellé d'annotation) ouvre le même modal, sans barre de format
- [ ] Annuler ne conserve rien, Valider conserve
## §7. Sélecteur de ressource
- [ ] La modale n'a **plus de double défilement** : la grille défile, pas le dialogue
- [ ] Un clic sur une vignette sélectionne et referme
- [ ] Sa largeur est inchangée (≈ 85 % de l'écran)
## §8. Éditeur de collection — Slider, Article, PDF
- [ ] Slider : liste à gauche, contenu édité à droite, plus de modale
- [ ] Ajouter un contenu, choisir une ressource, un titre → Enregistrer → il est là au retour
- [ ] Un contenu **sans ressource** n'est pas envoyé au backend (comportement d'avant conservé)
- [ ] Réordonner par la poignée → l'ordre est conservé après Enregistrer
- [ ] Supprimer un contenu
- [ ] Article : contenu, audio et les deux cases fonctionnent, la liste d'images suit
- [ ] PDF : plus de bloc figé, ajouter / renommer / supprimer un fichier
## §9. Quiz, Menu, Agenda
- [ ] Quiz : ajouter une question → elle apparaît **et** existe au rechargement
- [ ] Quiz : modifier l'intitulé, l'image de fond, les réponses → persisté
- [ ] Quiz : **réordonner** → les rangs ne se marchent plus dessus (bug corrigé), l'ordre tient au rechargement
- [ ] Quiz : supprimer une question ; en cas d'erreur réseau, la liste revient à son état d'avant
- [ ] Quiz : les 4 messages de score s'ouvrent et s'enregistrent
- [ ] Menu : les sous-sections sont en grille, un clic ouvre l'écran de sous-section
- [ ] Menu : réordonner → ordre correct au rechargement
- [ ] Menu : créer une sous-section depuis la tuile « Ajouter »
- [ ] Agenda : plus de bloc de 600 px, la liste suit son contenu, recherche et filtres de date OK
## §10. Le canevas — Map
- [ ] Trois volets : liste, fiche, carte
- [ ] Les **autres points** apparaissent en gris sur la carte
- [ ] Cliquer la carte place le point ouvert dans la fiche ; le pied affiche ses coordonnées
- [ ] Les outils Point / Ligne / Zone et la couleur fonctionnent
- [ ] Le bouton « agrandir » élargit la carte et replie la fiche
- [ ] Recherche et catégories cachent les **mêmes** points dans la liste et sur la carte
- [ ] « Informations pratiques » se replie/déplie et le compteur est juste
- [ ] Créer un point, le placer, le renommer → rechargement : tout est là
- [ ] Supprimer un point passe par la confirmation rouge
- [ ] ⚠️ **À juger à l'usage** : chaque déplacement de point déclenche un appel API. Si c'est trop bavard, il faudra grouper en fin de geste.
## §11. Le canevas — Événement, et Jeu
- [ ] Événement : si aucun plan de base n'est choisi, **le premier plan disponible est pris par défaut**
- [ ] Événement : ajouter une annotation, lui donner un libellé, une icône, une position sur la carte
- [ ] Événement : la position est **conservée au rechargement** (elle ne l'était jamais avant : la modale ne remplissait pas `geometry`)
- [ ] Événement : supprimer une annotation
- [ ] Événement : les blocs de programme s'affichent sans hauteur figée, le parcours est toujours là
- [ ] Jeu : le choix Puzzle / Taquin est deux pastilles, plus d'onglets
- [ ] Jeu : le canevas montre le découpage lignes × colonnes sur l'image, et le compte de pièces suit
## §12. Sélecteur de géométrie encore en modale
Deux appelants n'ont pas été migrés — ils doivent continuer de marcher **à l'identique**.
- [ ] Parcours → étape → géométrie : le dialogue s'ouvre, dessine, enregistre
- [ ] Agenda → événement → géométrie : idem
## §13. Catégories de carte — 🐛 bug corrigé
- [ ] Ouvrir les catégories d'une Map, **ne rien toucher**, valider → les catégories sont **toujours là**
(avant : la liste était effacée)
- [ ] Les boutons du dialogue sont traduits (ils étaient en dur en français)
- [ ] Ajouter / renommer / supprimer une catégorie fonctionne
## §14. i18n
- [ ] Passer le manager en EN puis NL : aucun texte français résiduel sur les écrans repris
- [ ] Les nouveaux libellés sont traduits : familles de types, descriptions de type, « Masquée »,
« Informations pratiques », « Nouveau point », outils de géométrie
---
## Écarts constatés
| § | Ce qui a été observé | Gravité |
|---|---|---|
| | | |
| | | |

View File

@ -173,7 +173,7 @@ Le libellé change, pas l'identifiant.
| `Screens/Resources/resource_formatting.dart` | **Nouveau** — extension de téléchargement, poids, mois, dates |
| `Screens/Resources/resource_download.dart` | **Nouveau**`downloadResource()`, la correction du `.json` en dur |
| `Helpers/ImageCompressor.dart` | `CompressedImage` expose `width`/`height` : c'est le seul endroit qui décode l'image avant l'upload |
| `Screens/Resources/select_resource_modal.dart` | Inchangé si possible — voir §3.3 |
| `Screens/Resources/select_resource_modal.dart` | **Contenu** inchangé ; **cadre** repris par la refonte Configuration/Sections — voir §3.3 |
| `constants.dart` | Aucun token nouveau à créer : `kSurface*`, `kInk*`, `kLine*`, `kSpace*`, `kRadius*` couvrent tout |
### 3.3 ⚠️ Le piège central : un écran, deux vies
@ -199,6 +199,10 @@ Règles à respecter :
n'ouvre pas le panneau de détail.
- Le rail de facettes doit être **réductible ou masqué** dans la modale : elle fait déjà
`size.width * 0.85`, et un rail de 194 px y est acceptable — mais le vérifier à 1280 px de large.
- ⚠️ **Cette règle dépend de la largeur `size.width * 0.85`.** La refonte des écrans
Configuration/Sections reprend le *cadre* de cette modale (voir plus bas) et **conserve
délibérément cette formule de largeur** pour ne pas invalider la règle ci-dessus. Si quelqu'un
veut un jour rétrécir la modale, c'est cette ligne-là qu'il faut rouvrir d'abord.
- Le mode sélection multiple est **désactivé** quand `isSelect: true`.
- `resourceTypes` filtre déjà en amont : la facette Type doit se limiter aux types reçus, comme
aujourd'hui (`resource_body_grid.dart:42`).
@ -206,6 +210,30 @@ Règles à respecter :
**Test de non-régression obligatoire** : ouvrir un champ image d'une section, choisir une ressource,
enregistrer. C'est le chemin le plus emprunté de toute l'app.
#### Le cadre de la modale — repris par un autre lot
Le *contenu* de `showSelectResourceModal` appartient à ce lot et n'est pas retouché ailleurs. Son
*cadre*, en revanche, est repris par la refonte des écrans Configuration/Sections — maquette :
`DOCS/claude design/refonte-configuration-sections.html`, section 06.
Motif : ce n'est pas qu'une question de style. Un `AlertDialog` dispose de `hauteur 48` (marge
verticale de 24 px), le contenu en demande `0,85 × hauteur`, plus un titre et une ligne d'actions de
70 px — d'où le `SingleChildScrollView` qui l'entoure, et un **défilement dans un défilement** : on
fait glisser le dialogue au lieu de la grille.
Ce que l'autre lot change, et rien de plus :
- coquille standard (`Dialog` + en-tête + pied) à la place de `AlertDialog` et de
`title: Center(Text(...))`, `kRadiusShell` au lieu du rayon 20 ;
- hauteur figée → corps souple sous un `maxHeight`, ce qui supprime le double défilement ;
- le bouton « Annuler » de 180 × 70 px descend dans le pied de dialogue ;
- `showValues` (lignes 64-78) supprimé : c'est du code mort. La copie qu'en garde
`multi_input_modal.dart` ligne 186 l'est aussi — son unique appel, ligne 53, est en commentaire.
Les deux peuvent partir.
**La largeur `size.width * 0.85` n'est pas touchée**, exprès — voir l'avertissement des règles
ci-dessus. Aucune ligne de `ResourcesScreen` n'est modifiée.
### 3.4 Le rail de facettes
```

View File

@ -442,6 +442,65 @@ pareil (badge, et proposition de régénérer). `AiProvenance.modelKey` le perme
assumer côté scénario. Les échappatoires restent 2 vues par personnage, ou la composition en deux
passes — générer A dans la scène, puis ajouter B en modification de cette image.
#### « Faire vivre un lieu » — la vidéo à partir du réel (décidé le 2026-09-04)
Le text-to-video pur est écarté : il invente la géométrie du lieu, ce qui est disqualifiant en
patrimoine. **Le lieu réel reste la base, l'IA n'ajoute que le mouvement** — c'est exactement
« Point de départ » ci-dessus, transposé à la vidéo, avec `sourceFidelity: framing` : le modèle n'a
pas à inventer l'architecture, il l'a sous les yeux.
**Rien de neuf en base.** `GenerationTemplate.Kind = Video` existe déjà, `sourceFidelity` aussi. Un
gabarit de plus, pas un mécanisme de plus.
**Où ça vaut vraiment quelque chose, et où ça n'en vaut pas.** Pas « faire vivre un bâtiment » en
général : là où le lieu a des archives, **une vraie photo d'époque bat une boucle générée**, parce
qu'en patrimoine la crédibilité *est* la valeur. Le cas qui gagne est celui où **il ne reste rien** :
un bâtiment disparu, un métier éteint, un site réduit à ses fondations. Là, huit secondes plausibles
battent un paragraphe, et elles ne concurrencent aucune archive. Cadrer la vente là-dessus.
**Deux voies techniques, et elles ne vont pas au même endroit :**
| Voie | Ce que ça demande au client | Verdict |
|---|---|---|
| **Image-to-video** — photo du lieu (ou sa version d'époque déjà générée) + gabarit de mouvement | Trois champs | **Dans le produit** |
| **Motion transfer** — le médiateur mime le geste, le mouvement est transféré sur le personnage | Un tournage, un consentement écrit, son archivage | **En prestation Unov, pas dans le produit** |
Le motion transfer donne un mouvement plus juste, et c'est la voie préférée qualitativement. Mais il
échoue le critère de réussite du §0 par construction — « installer un conservateur devant l'écran et
le laisser produire sans intervention » ne survit pas à « d'abord, filmez quelqu'un ». À 39-179 €/mois,
aucun client n'a l'équipe pour ça. Il se vend, il ne se livre pas en self-service.
**Contraintes de production — la partie qui ne périmera pas** (au-delà, le résultat décroche) :
- 6 à 10 secondes, pensées pour la boucle, fondu aux extrémités
- **une seule personne** à l'écran
- mouvements simples, caméra calme
- plan large ou moyen, **jamais de gros plan** sur les mains ou le visage
Ces quatre lignes valent mieux qu'un nom de modèle : elles décrivent le domaine de validité de la
technique, pas d'un fournisseur. Le `ProviderModelId` vit en base (§3.6) et aura tourné plusieurs
fois d'ici la livraison.
**Gabarits de mouvement, pas de prompt libre** — même règle que partout ailleurs dans Studio : le
conservateur **choisit**, il ne décrit pas.
⚠️ **Consentement.** Le motion transfer part du corps d'une personne réelle : c'est un traitement de
ressemblance, au même titre que « Modifier une image » sur une personne (voir l'encadré ci-dessus).
Consentement écrit, tracé, et archivé avec l'asset — obligation qui pèse sur la prestation, ce qui
est une raison de plus de ne pas l'ouvrir en self-service.
⚠️ **Mention IA plus visible qu'ailleurs.** Une figure humaine en mouvement dans un lieu réel n'est
pas une illustration décorative : le badge du §3.11 suffit juridiquement, mais certaines institutions
refuseront le principe même. À valider auprès d'un client réel, pas seulement techniquement.
**Contenu 2D pour écrans.** Ne remet pas en cause la décision de rester sur des personnages stylisés
en VR ([v2/vr-quest-unity-plan.md](vr-quest-unity-plan.md)).
> **Non tranché : est-ce que ça a l'air crédible ?** Tout le reste en dépend, et ça ne se décide pas
> sur le papier. Test à faire avant d'écrire la moindre ligne d'UI — une photo d'un bâtiment du
> Fourneau, sa version d'époque, un personnage en boucle de 8 s. Quelques euros, une soirée.
> Si ça décroche, cette section saute.
### 3.4 Cycle de vie d'un asset
```