+ tablet-app · lot K
+ K3 — SectionEvent
+ K7 — SectionArticle
+ 12 août 2026
+
+
Deux écrans à faire passer du téléphone debout à la borne couchée
+
+ Le code se recopie depuis mymuseum-visitapp — ce n'est pas là qu'est le travail.
+ Ces écrans sont dessinés pour un téléphone tenu à la verticale, à trente centimètres des yeux.
+ Une borne fait 1280 px de large, elle est fixe, et on la lit debout à un mètre.
+ Cette page arrête la mise en page des deux écrans ensemble, parce que les trancher
+ séparément donnerait deux grammaires différentes sur la même dalle.
+ Validée le 12 août — les quatre décisions de fin de page sont prises.
+
+
+
+
+
+
+
+
+
Ce qui change, pour les deux
+
+ Six décisions valables sur toute la borne. Elles ne sortent pas d'une préférence :
+ chacune corrige un idiome de téléphone qui devient faux sur une dalle large et fixe.
+
+
+
+
+
+
Deux colonnes, jamais une
+
+ La largeur est la ressource qu'on gagne, et une colonne unique la gaspille intégralement.
+ Chaque écran se découpe en un ancrage qui ne bouge pas et un
+ flux qui défile. Sur l'article, le média ancre et le texte défile ; sur
+ l'événement, la carte ancre et le programme défile.
+ mobile — une colonne, tout défile ensemble
+
+
+
+
+
Le texte reste étroit
+
+ Élargir la colonne de texte à 1280 px la rend illisible : l'œil perd la ligne au retour.
+ Le corps de l'article est plafonné autour de 65 caractères et le reste de
+ la largeur va au média, pas au texte. C'est la règle qui justifie à elle seule les deux colonnes.
+
+
+
+
+
Le héros est une bande, pas un écran
+
+ Sur mobile, l'événement ouvre sur une image qui occupe 52 % de la hauteur,
+ et le parallaxe récompense le défilement. Debout devant une borne, personne ne défile pour
+ découvrir qu'il y a un programme dessous. Le héros tombe à 26 % : il situe,
+ il n'accueille pas.
+ mobile — SliverAppBar, expandedHeight = 0.52 × hauteur
+
+
+
+
+
Pas de feuille modale
+
+ showModalBottomSheet est une réponse à l'étroitesse : sur un téléphone il n'y a
+ pas de place à côté, alors on empile. Sur une borne il y a la place à côté. Le détail d'un
+ bloc de programme s'ouvre dans la colonne de droite, sous la carte, sans
+ masquer ce qu'on regardait.
+ mobile — _showBlockDetail() en bottom sheet
+
+
+
+
+
Rien à agrandir
+
+ La carte de l'événement est un aperçu de 220 px qu'il faut toucher pour ouvrir une page
+ pleine. Sur une borne d'accueil, la carte est la raison pour laquelle on s'arrête :
+ elle est affichée à sa taille utile d'emblée, et EventMapFullPage ne se porte pas.
+ mobile — aperçu 220 px + bouton « Agrandir »
+
+
+
+
+
Les couleurs viennent de la configuration
+
+ mymuseum-visitapp code ses couleurs en dur par flavor client — rouge MDLF, bleu
+ Fort. tablet-app n'a pas de flavor : un seul APK sert tous les clients et la
+ charte arrive avec la configuration choisie au pincode. Tout accent dérive de
+ configuration.primaryColor, comme l'écran Game livré en K4.
+ Le bleu ardoise de cette page est le repli kTestSecondColor, pas une proposition
+ de charte.
+
+
+
+
+
+
+
+
+
+
Article — le média ancre, le texte défile
+
+ Un article porte un corps HTML, souvent un audio, et parfois une série d'images.
+ Sur mobile les trois s'empilent et l'audio finit en lecteur flottant par-dessus le texte.
+ En paysage, chacun a sa place et le lecteur audio n'a plus besoin de flotter.
+
+
+
+
+ Article — 1280 × 800
+ MDLF · « Ruche 7 »
+
+
+
+
+
+ A
+
image principale
+
+
2
+
3
+
4
+
5
+
+
+ ▶
+
+ 3:12
+
+
+
+
+
+ B
+
+
Rucher pédagogique
+
Ruche 7
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ A
+
+ Rail média, 38 %. L'image principale prend la hauteur disponible, les
+ autres ressources deviennent une bande de vignettes — c'est SliderImages à plat
+ plutôt qu'en carrousel, parce qu'une borne n'a pas de raison de cacher quatre images
+ derrière un geste. L'audio est docké en bas du rail : sur mobile,
+ audio_player_floating flotte au-dessus du texte faute de place. Ici il a une
+ place, donc il ne flotte plus.
+
+
+
+ B
+
+ Colonne de lecture, 62 %, texte plafonné. Le rendu HTML
+ (flutter_widget_from_html) se recopie tel quel ; ce qui change est le
+ maxWidth qui l'entoure. Le titre passe en display, avec l'étiquette de section
+ au-dessus — sur une borne, un visiteur arrive au milieu d'un parcours et doit savoir
+ où il est avant quoi il lit.
+
+
+
+ C
+
+ Barre de pied, hauteur de doigt. Un seul bouton, cible à
+ 56 px minimum : on tape debout, parfois avec un enfant au bout du bras.
+ Pas de sélecteur de langue — elle est choisie en amont, la réafficher ici
+ rouvrirait une décision déjà prise. Et rien d'autre : SectionPageDetail ne
+ reçoit qu'une section et un booléen isFromMenu, jamais la liste de ses
+ voisines. Un repère du type « 3 / 8 » n'est donc adossé à aucune donnée — il faudrait
+ descendre la liste jusqu'ici pour le calculer.
+
+
+
+
+
Quand il manque l'audio, les photos, ou les deux
+
+ Les trois cas sont possibles en configuration, et le rail média de 38 % ne survit pas tel quel
+ à la disparition de son contenu. La règle qui les couvre tous :
+ le rail n'existe que s'il a quelque chose à ancrer. Sinon la colonne de lecture
+ se centre — ce qui ne contredit pas la règle des deux colonnes, dont l'objet est d'empêcher une
+ ligne de texte de s'étirer sur 1280 px, pas d'imposer une colonne vide.
+
+
+
+
+
+
+
+
+
+
image principale
+
+
2
+
3
+
4
+
5
+
+
+
+
+
+
Rucher pédagogique
+
Ruche 7
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Sans audio
+
+ Le dock disparaît, l'image reprend la hauteur libérée. Aucune autre reprise —
+ c'est le cas le plus simple des trois.
+ audioIds vide pour la langue courante
+
+
+
+
+
+
+
+
+
+
Rucher pédagogique
+
Ruche 7
+
+ ▶
+
+ 3:12
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Sans photo, avec audio
+
+ Le rail tombe — une colonne de 38 % contenant un seul lecteur serait un trou, pas une mise
+ en page. L'audio passe en tête de la colonne de lecture, à la largeur du
+ texte, juste sous le titre : c'est là qu'on le cherche quand il est la pièce principale.
+ imageSource nul et contents vide
+
+
+
+
+
+
+
+
+
+
Rucher pédagogique
+
Ruche 7
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Texte seul
+
+ Une seule colonne, centrée, toujours plafonnée à 65 caractères. Le vide de part et d'autre
+ est voulu : c'est ce qui garde le texte lisible. La tentation d'étirer la
+ ligne pour « remplir » est précisément ce que la règle interdit.
+ ni image, ni contents, ni audio
+
+
+
+
+
+
+
Deux choses vérifiées dans ArticleDTO, pas supposées
+
+ audioIds est une List<TranslationDTO>, pas un id.
+ L'audio est donc par langue : un article peut avoir sa version française et pas sa
+ néerlandaise. « Sans audio » n'est pas un état de l'article, c'est un état de
+ l'article dans la langue courante — et la borne doit basculer sur la variante
+ du milieu pour ce visiteur-là seulement.
+
+
+ isContentTop existe déjà et dit si le texte passe avant le média.
+ En portrait, « avant » veut dire au-dessus. En paysage, il n'y a plus de dessus : le drapeau
+ devient l'ordre des deux colonnes — média à gauche, ou texte à gauche. À câbler, pas à ignorer,
+ sinon un réglage déjà offert dans manager-app devient sans effet sur la borne.
+
+
+
+
+
Les statistiques sont déjà là — vérifié dans le code
+
+ Rien à câbler. section_page_detail.dart émet sectionView à
+ l'ouverture et sectionLeave avec la durée au dispose, et il
+ enveloppe les treize types : l'article sera compté dès que son case
+ entre dans le switch. statisticsService est câblé à sept endroits de
+ l'app. L'événement articleRead de mymuseum, plus spécifique, n'existe pas ici —
+ c'est un ajout facultatif, pas un manque.
+
+
+ En revanche, le marquage « déjà lu » par visiteur
+ (articleRead en base locale, DatabaseHelper) ne se recopie pas : sur
+ un appareil partagé, le lecteur suivant n'est pas le précédent.
+
+
+
+
+
+
+
+
Événement — la carte ancre, le programme défile
+
+ C'est l'écran du cas E du test-plan : la borne d'accueil d'un carnaval ou d'un festival, qui
+ répond à deux questions et deux seulement — qu'est-ce qui se passe maintenant et
+ où est-ce que ça se passe. La mise en page les met côte à côte, sans geste entre elles.
+
+
+
+
+ Événement — 1280 × 800
+ Cas E · carnaval
+
+
+
+
+
+ D
+
Carnaval des Légendes
+ 14/03/2026 → 16/03/2026
+
+
+
+
+
+ E
+
Programme du jour
+
+ 10:00
+ Ouverture des portes
+
+
+ 14:00
+ Grand cortège — départ place du Marché
+ EN COURS
+
+
+ 16:30
+ Remise des prix des chars
+
+
+ 18:00
+ Brûlage du Bonhomme Hiver
+
+
+ 20:00
+ Feu d'artifice
+
+
+
+
+ F
+
+
+
+
+
+
+ 5 points · le point orange suit le bloc en cours
+
+
+
+
+
+
+
+
+
+ D
+
+ Bande héros, 26 %. Assez pour porter l'image, le titre et les dates ;
+ pas assez pour repousser le programme sous la ligne de flottaison. Le dégradé de repli
+ dérive de primaryColor — c'est exactement le calcul déjà écrit dans l'écran
+ Game de K4, à réutiliser plutôt qu'à refaire.
+
+
+
+ E
+
+ Programme, 44 %. La frise verticale à pastilles et trait de liaison de
+ mymuseum tombe : elle coûte 56 px de gouttière pour exprimer une chronologie que l'heure
+ en tête de ligne dit déjà. Le bloc actif garde son fond plein et son pastillage
+ « EN COURS » — c'est l'information la plus demandée de l'écran, elle doit
+ se voir à trois mètres.
+
+
+
+ F
+
+ Carte, 56 %, toujours vive. Les annotations globales sont dans l'accent,
+ celles du bloc en cours dans l'orange sémantique — la même paire de couleurs que le
+ programme, pour que l'œil relie une ligne à un point sans légende. Défilement et zoom
+ restent actifs ; InteractiveFlag.none était une contrainte d'aperçu.
+
+
+
+
+
+
Le piège à ne pas déplacer
+
+ Les coordonnées de MapAnnotationDTO.geometry.coordinates sont en
+ [lng, lat] — GeoJSON. Celles de GuidedStep sont en
+ [lat, lng]. Les deux ordres sont en production, et une confusion
+ ne lève aucune erreur : elle déplace les points sur une carte qui reste
+ plausible. La lecture de mymuseum (coords[1] = lat, coords[0] = lng)
+ se recopie telle quelle, sans passer par un helper venu d'un autre contexte. Le volet parcours
+ étant exclu de la borne, cet écran ne voit qu'une seule des deux conventions — c'est une raison
+ de plus de ne pas y introduire l'autre.
+
+
+
+
+
+
+
+
Tranché le 12 août
+
+
+
+
+
Fond clair
+
+ La nouvelle section ne doit pas dénoter dans l'app. tablet-app est clair —
+ kBackgroundColor vaut #FFFFFF et kBackgroundLight
+ #F3F3F3 — et les maquettes ci-dessus emploient ces valeurs-là,
+ pas une palette inventée. Le #111111 en dur de l'écran Événement de mymuseum ne
+ se recopie donc pas : il vient d'un téléphone consulté en extérieur.
+ mymuseum — Scaffold backgroundColor: Color(0xFF111111)
+
+
+
+
+
Hors dates, aucune prose
+
+ Le carnaval dure trois jours, la borne tourne les 362 autres. Le programme reste affiché,
+ sans pastille « EN COURS » et avec les blocs en gris — le surlignage est
+ calculé sur DateTime.now(), il s'éteint tout seul. La bande héros continue de
+ porter les dates, qui sont numériques donc sans traduction, et suffisent à
+ dire qu'on est avant ou après.
+ état passé = les mêmes blocs, désaturés — pas un écran différent
+
+
+
+
+
Et c'est l'i18n qui tranche
+
+ Ton réflexe sur les traductions était le bon, et il est plus contraignant que prévu :
+ tablet-app n'a aucun système d'i18n — pas de l10n/,
+ pas d'AppLocalizations, les libellés sont en français en dur
+ ("Ce type n'est pas supporté"). Un bandeau « Édition passée » serait donc
+ français pour un visiteur néerlandophone. Le vrai libellé viendrait soit
+ d'un champ backend traduit — qui n'existe pas — soit d'un i18n à créer, ce qui n'est pas un
+ chantier de K3. D'où la règle ci-dessus : on affiche des dates, pas des phrases.
+
+
+
+
+
Retour à l'accueil après 5 min
+
+ Adopté, et écrit une seule fois pour les treize types :
+ section_page_detail enveloppe déjà toutes les sections et porte déjà les crochets
+ initState / dispose des statistiques. Le minuteur d'inactivité va
+ là, pas dans les deux écrans de cette page. Bénéfice de bord : un retour automatique passe
+ par le même dispose, donc il émet un sectionLeave avec sa durée
+ réelle — la statistique reste juste au lieu de compter une consultation ouverte jusqu'au
+ prochain visiteur.
+ hors périmètre K3/K7 — à ouvrir comme chantier propre
+
Un visiteur part sans fermer l'écran ; le suivant trouve l'article du précédent. Délai proposé : 5 minutes. Sorti de la maquette K3/K7 parce que ce n'est ni l'un ni l'autre — c'est un comportement de borne qui vaut pour les treize types.
+
À écrire une seule fois, dans section_page_detail : il enveloppe déjà toutes les sections et porte déjà les crochets initState/dispose des statistiques. Bénéfice de bord : le retour passant par le même dispose, il émet un sectionLeave avec sa durée réelle au lieu de laisser une consultation ouverte jusqu'au prochain visiteur — la statistique devient juste, en plus de l'écran.
+ v1-plan.md lot K — K8 · maquette kiosk-paysage
+
+
diff --git a/v1-plan.md b/v1-plan.md
index 3f3e39a..be826aa 100644
--- a/v1-plan.md
+++ b/v1-plan.md
@@ -284,7 +284,8 @@ Ordre du §2 de STATUS.md, corrigé par **L12**.
| ~~**K4**~~ ✅ **2026-08-12** | **Écran Game repris de mymuseum.** Les 6 fichiers de `Screens/Sections/Game/` sont dans `tablet-app/lib/Screens/Game/`, contexte adapté ; `lib/Screens/Puzzle/` supprimé ; `main_view` bascule sur `SectionType.Game` / `GameDTO` / `GamePage`. **Gains** : le puzzle glissant (mélange par mouvements valides, donc toujours soluble), le bouton d'indice, et un dimensionnement qui respecte le ratio de l'image — 508 lignes contre 237 à l'ancien `puzzle_view`. ⚠️ **Une décision de rendu à valider à l'œil** : mymuseum code ses couleurs **en dur par flavor client** (`kMainColor0/1/2` — rouge MDLF, bleu Fort). `tablet-app` n'a pas de flavor : un seul APK sert tous les clients et la charte vient de la configuration choisie au pincode. Recopier ces constantes aurait affiché du bleu chez MDLF. Le dégradé et le bouton d'indice sont donc **dérivés de `configuration.primaryColor`** (même parsing que `audio_player`/`loading_common`, repli `kTestSecondColor`, 3 nuances par `Color.lerp`). Plus juste sur le fond, mais **le rendu diffère de la version mobile** : si le dégradé dessiné à la main est préféré, figer les trois couleurs |
| ~~K4 — énoncé d'origine~~ | **`SectionPuzzle` → `SectionGame` + le type glissant.** Le serveur a renommé et porte `GameTypes { Puzzle, SlidingPuzzle }` ; `tablet-app` a encore un écran `Puzzle/` qui ne connaît que l'ancien. **Reprendre le code de `mymuseum-visitapp/lib/Screens/Sections/Game/`** — `game_page.dart` et `sliding_puzzle_piece.dart` existent déjà là-bas, et cette version est **moins buguée** que celle de tablet-app | Récupération, pas réécriture |
| ~~**K5**~~ ✅ **2026-08-12** | **Les trois flavors de `mymuseum-visitapp` construisent** (`dev`, `mdlf`, `fortsaintheribert`), exit 0, APK à l'appui. **D0 est débloqué.** ⛔ **L'énoncé était faux : il n'y avait pas de « même rattrapage » à faire.** Sa config Android était **déjà à niveau** — Gradle 8.11.1, AGP 8.9.0, `-Xmx4096M`, `enableJetifier=false`, aucun forçage d'`androidx.lifecycle`. Les dix crans de K1 ont amené **`tablet-app` jusqu'à `mymuseum`** ; le lot supposait la symétrie du problème, elle n'existait pas. Le réflexe du diff annoncé en K1 a donné la réponse en deux `cat`. **Ce qui restait, et qui est fait** : les deux avertissements du build. NDK **27.0.12077973 → 28.2.13676358** (`speech_to_text` le réclame nommément) et Kotlin **2.1.0 → 2.3.10** (seuil 2.2.20 annoncé comme rupture ; c'est la version de `tablet-app`, donc un alignement). Six builds — trois avant, trois après. APK de **382 à 349 Mo**. ⚠️ **Un cran de plus existe, délibérément non pris** : Kotlin réglé, Flutter réclame Gradle 8.14.0 et AGP 8.11.1. C'est du **terrain neuf pour les deux repos** (`tablet-app` est à 8.11.1/8.9.1, mêmes avertissements latents) — le faire ici seul **désaligne** les deux apps. À faire d'un bloc sur les deux, ou pas du tout : c'est la dette d'outillage déjà parquée en V2 | Son code était déjà porté sur le nouveau contrat — **c'est ce qui aurait dû mettre la puce à l'oreille** : le repo qui a servi de modèle au portage K2 n'avait pas de raison d'être en retard d'outillage sur celui qui le copiait |
-| **K7** | **`SectionArticle` sur le kiosk — ajouté le 2026-08-12**, en recomptant les types couverts. Son `case` est commenté « TODO » dans `main_view.getContent` et `Screens/Article/article_view.dart` fait 1 Ko : un article rend « Ce type n'est pas supporté ». **Contrairement à `SectionEvent` et `SectionParcours`, ce type existait dans Mongo** — c'est donc un vrai retard, pas un type né avec Postgres v3, et il sortira de la migration avec du contenu réel. **Le code est une reprise de `mymuseum-visitapp/lib/Screens/Sections/Article/`**, pas une conception. **Le travail n'est pas là** : il est dans le **passage en paysage**. Les écrans de mymuseum sont pensés pour un téléphone tenu debout ; une borne est large, et un article rendu en colonne unique sur 1280 px de large est illisible. C'est le seul endroit du lot K où il y a une décision de mise en page à prendre, pas une copie à faire — d'où la maquette ci-dessous | **À traiter avec K3** : `SectionEvent` pose exactement la même question de paysage (héros, programme, carte), et la trancher deux fois donnerait deux mises en page différentes sur la même borne. Une seule passe de design pour les deux, même raisonnement d'économie que **L5** et **L15** |
+| **K7** | **`SectionArticle` sur le kiosk — ajouté le 2026-08-12**, en recomptant les types couverts. Son `case` est commenté « TODO » dans `main_view.getContent` et `Screens/Article/article_view.dart` fait 1 Ko : un article rend « Ce type n'est pas supporté ». **Contrairement à `SectionEvent` et `SectionParcours`, ce type existait dans Mongo** — c'est donc un vrai retard, pas un type né avec Postgres v3, et il sortira de la migration avec du contenu réel. **Le code est une reprise de `mymuseum-visitapp/lib/Screens/Sections/Article/`**, pas une conception. **Le travail n'est pas là** : il est dans le **passage en paysage**. Les écrans de mymuseum sont pensés pour un téléphone tenu debout ; une borne est large, et un article rendu en colonne unique sur 1280 px de large est illisible. C'est le seul endroit du lot K où il y a une décision de mise en page à prendre, pas une copie à faire. ✅ **Maquette faite et validée le 2026-08-12** — `DOCS/claude design/kiosk-paysage-article-event.html`, une seule passe pour K3 et K7. Six règles communes (deux colonnes ancrage/flux, texte à 65 caractères, héros à 26 % au lieu de 52 %, plus de `showModalBottomSheet`, carte sans « Agrandir », accents dérivés de `configuration.primaryColor` selon le précédent K4) et quatre décisions tranchées : **fond clair** aux valeurs réelles de l'app (`kBackgroundColor` / `kBackgroundLight`), **pas de sélecteur de langue** (choisie en amont), **hors dates : aucune prose**, et le **retour automatique** sorti du périmètre (ligne suivante). ⚠️ **Deux vérifications faites dans le code plutôt que supposées.** Les **statistiques sont déjà acquises** : `section_page_detail.dart:60/72` émet `sectionView` et `sectionLeave` avec durée, et il enveloppe les treize types — l'article sera compté dès que son `case` entre dans le `switch`, sans une ligne à écrire. Et **`tablet-app` n'a aucun i18n** : pas de `l10n/`, pas d'`AppLocalizations`, libellés français en dur. C'est ce qui interdit un bandeau « Édition passée » — il serait français pour un visiteur néerlandophone — et c'est pourquoi l'état hors dates s'exprime par les **dates**, numériques donc sans traduction | **À traiter avec K3** : `SectionEvent` pose exactement la même question de paysage (héros, programme, carte), et la trancher deux fois donnerait deux mises en page différentes sur la même borne. Une seule passe de design pour les deux, même raisonnement d'économie que **L5** et **L15**. **Trois états incomplets couverts par la maquette** : sans audio (le dock tombe, l'image reprend la hauteur), sans photo (le rail tombe, l'audio passe en tête de la colonne de lecture), texte seul (une colonne centrée, toujours plafonnée). ⚠️ **`audioIds` est une `List`** : « sans audio » est un état **de la langue courante**, pas de l'article. Et **`isContentTop` existe déjà** — en paysage il ne veut plus dire « au-dessus » mais « à gauche », à câbler sous peine de rendre sans effet un réglage déjà offert dans `manager-app` |
+| **K8** | **Retour automatique à l'accueil après inactivité — ajouté le 2026-08-12**, sorti de la maquette K3/K7. Un visiteur part sans fermer l'écran ; le suivant trouve l'article du précédent. Délai proposé : **5 minutes**. **À écrire une seule fois pour les treize types**, dans `section_page_detail` — il enveloppe déjà toutes les sections et porte déjà les crochets `initState`/`dispose`. Bénéfice de bord : le retour passant par le même `dispose`, il émet un `sectionLeave` **avec sa durée réelle** au lieu de laisser une consultation ouverte jusqu'au prochain visiteur. **La statistique devient juste, en plus de l'écran** | Ce n'est ni K3 ni K7 : c'est un comportement de borne qui vaut pour tous les types. À ne pas glisser dans le port des deux écrans, sinon il sera écrit deux fois puis une troisième |
| **K6** | **Publier les APK à jour**, puis vérifier le renouvellement du parc | **L19**. Les tablettes kiosk sont des appareils que tu gères — mise à jour maîtrisée. **Les téléphones des visiteurs, non** : aucune manœuvre technique ne force une mise à jour, d'où la stratégie de coexistence du lot I |
### Lot J — RGPD & conformité (2 à 3 jours) — juste avant le lot I