# Architecture web SaaS MyInfoMate Décision d'architecture pour les deux canaux web de la solution MyInfoMate : le kiosk web et l'app visiteur web. > ⚠️ **Statut révisé le 2026-08-07 — les deux canaux ne sont plus au même stade.** > > - **Visiteur web → V1.** `visitapp-web` est écrit, `npm run build` passe, les **13 types de section** sont rendus et l'assistant IA y a été ajouté le 2026-08-06. La section « État d'avancement global » en bas de ce fichier date d'avant et sous-estime ce qui est fait (elle annonce Map/Quiz/Game/GuidedPath en stub et l'assistant hors scope — c'est faux). **Il ne reste que le Dockerfile et l'entrée `app.myinfomate.be` dans le compose.** C'est bloquant : le plan Essentiel vendu en self-service est web-only. > - **Kiosk web → 📦 V2.** Pas commencé, aucun client demandeur, et `tablet-app` dont il dérive ne compile plus. Le reste de ce document reste la référence quand le chantier sera repris. --- ## Contexte Aujourd'hui MyInfoMate dispose de : - `tablet-app` (Flutter) — kiosk tablette fixe - `mymuseum-visitapp` (Flutter) — app native visiteur installée sur device perso - `manager-app` (Flutter) — back-office, génère le client API consommé par les apps - `manager-service` (C# / ASP.NET Core) — backend API REST Deux canaux web supplémentaires sont à ajouter : 1. **Kiosk web** — version web de tablet-app pour clients ne voulant pas d'install native 2. **Visiteur web** — `app.myinfomate.be/[nom-client]` ouvert via QR code depuis le smartphone du visiteur --- ## Décision ### Kiosk web → Flutter Web (extension de tablet-app) URL cible : `kiosk.myinfomate.be/[deviceId]` - Extension du code existant `tablet-app` (build web déjà testé sur `demo.myinfomate.be`) - Routing : si `deviceId` dans l'URL → on charge la conf du device. Si pas de `deviceId` → écran de saisie pincode → création d'un device côté backend → tag `web` (petite icône dans la liste devices côté manager-app) - Composants UI partagés via un package Dart commun (voir plus bas) **Pourquoi Flutter Web ici :** kiosk = écran fixe, connexion stable, plein écran, on charge une fois et on reste. Le bundle Flutter Web (~4-5 MB) n'est pas un problème dans ce contexte. Bénéfice énorme : 100 % de réutilisation du code tablet-app existant. #### Analyse de compatibilité web (tablet-app) Le dossier `web/` est déjà présent et configuré. Le code contient 52 occurrences de `kIsWeb` — la multiplateforme était anticipée dès le départ. **Blocants à traiter :** | Problème | Priorité | Solution | |----------|----------|----------| | `sqflite` pas supporté en web (persistance deviceId/config) | Haute | Remplacer par `hive` (support web natif) | | `flutter_pdfview` mobile-only | Moyenne | Fallback HTML ou PDF.js en web | | Mapbox pas supporté en web | Basse | `flutter_map` déjà dans les deps, ou Google Maps uniquement | | MQTT désactivé en web | Non prioritaire | Rechargement de page suffit en contexte web | **Effort estimé : 2-3 jours** pour une version kiosk web fonctionnelle. ### Visiteur web → Next.js (nouvelle stack séparée) URL cible : `app.myinfomate.be/[slug]` - App Next.js dédiée, **pas** de Flutter Web - Consomme le même `manager-service` que les autres apps - Routing dynamique sur le slug client (génération automatique à la création du compte SaaS) - Référence visuelle : `home_3.0.dart` + `ConfigurationPage` de `mymuseum-visitapp` **Pourquoi pas Flutter Web ici :** ce canal est ouvert via QR code depuis le smartphone perso du visiteur, parfois sur 3G/4G dégradée dans un musée. Flutter Web doit charger 4-5 MB de canvas JS avant d'afficher quoi que ce soit — c'est exactement le scénario où ça plombe l'expérience. Pour un produit SaaS vitrine, le time-to-interactive sur mobile est non négociable. **Pourquoi Next.js plutôt qu'Angular / SvelteKit :** - **Angular** : faisable mais bundle plus lourd, SSR via Angular Universal moins mature que Next.js. Philosophie calibrée pour back-offices riches (DI, modules, RxJS), pas pour une web app visiteur ultra-légère. On rame contre le framework. - **SvelteKit** : plus léger encore mais nouvelle stack complète à apprendre, écosystème plus petit. - **Next.js** : meilleur compromis. React + TypeScript, déjà utilisé pour les landings (`myinfomate-landing`, `unov-landing`), écosystème énorme, SSR/SSG natif, parfait pour le routing `app.myinfomate.be/[slug]`. #### Theming multi-client (équivalent des flavors Flutter) En Flutter, les couleurs/logos sont injectés au **build-time** via `--dart-define=FLAVOR=client`. En Next.js, c'est **runtime** — chaque client a son slug et sa config dans le backend. Avantage SaaS : zéro rebuild par client. Un nouveau client créé dans manager-app dispose instantanément de son URL avec ses couleurs et son logo. ##### Hiérarchie des couleurs Les couleurs existent à deux niveaux dans le modèle de données : - `ApplicationInstanceDTO.primaryColor / secondaryColor` → couleurs de l'instance - `ConfigurationDTO.primaryColor / secondaryColor` → couleurs d'une configuration spécifique **Règle d'application :** - Home (liste des configurations) → couleurs de l'**instance** - À l'intérieur d'une configuration → couleurs de la **configuration** si définies, sinon fallback sur l'instance ```ts const primaryColor = configuration.primaryColor ?? instance.primaryColor const secondaryColor = configuration.secondaryColor ?? instance.secondaryColor ``` Les deux niveaux sont configurables dans manager-app via `ColorPickerInputContainer` (écran Applications pour l'instance, écran Configuration pour chaque config). ##### Design system CSS Deux couleurs configurables, le reste dérivé automatiquement : ``` Configurables (viennent du backend) : --color-primary → boutons, header, gradient, états actifs --color-secondary → accent, fin du gradient Dérivées en JS au chargement : --color-primary-light → primary à 12% opacité (fonds hover, badges) --color-on-primary → blanc ou noir selon contraste WCAG automatique Fixes (identiques pour tous les clients) : --color-background → #FFFFFF --color-surface → #F8F8F8 (cards, sections) --color-text → #1A1A1A --color-text-muted → #6B7280 --color-border → #E5E7EB ``` `--color-on-primary` est calculé automatiquement (lib `color2k` ou `chroma-js`) pour garantir la lisibilité si un client choisit une couleur primaire claire. ```ts // layout.tsx — injection dans const luminance = getLuminance(primaryColor) const onPrimary = luminance > 0.4 ? '#1A1A1A' : '#FFFFFF' ``` #### Features — état d'avancement | Feature | Statut | Notes | |---------|--------|-------| | Grille de configurations (home) | ✅ Fait | `ConfigurationGrid.tsx` | | Page configuration (liste sections) | ✅ Fait | `SectionList.tsx` | | AppBar gradient + sélecteur de langue | ✅ Fait | `AppBar.tsx`, langue persistée en localStorage | | Theming CSS variables (couleurs instance/config) | ✅ Fait | `theme.ts`, fallback instance → config | | VisitorContext (langue + langues dispo) | ✅ Fait | `VisitorContext.tsx` | | Section Article (HTML + carousel + audio flottant) | ✅ Fait | `ArticleSection.tsx` | | Section Agenda (filtre par mois + popup détail) | ✅ Fait | `AgendaSection.tsx` | | Section Menu (grille + recherche) | ✅ Fait | `MenuSection.tsx` | | Section Slider (carousel + dots indicator) | ✅ Fait | `SliderSection.tsx` | | Section Video (YouTube / Vimeo / direct) | ✅ Fait | `VideoSection.tsx` | | Section PDF (iframe natif + sélecteur) | ✅ Fait | `PdfSection.tsx` | | Section Weather (OpenWeatherMap, onglets par jour) | ✅ Fait | `WeatherSection.tsx` | | Section Web (iframe) | ✅ Fait | `WebSection.tsx` | | Section Map (Leaflet ou Google Maps) | 🔲 À faire | Nécessite Opus — complexe | | Section Quiz | 🔲 À faire | | | Section Game (puzzle) | 🔲 À faire | | | Section GuidedPath | 🔲 À faire | | | QR Scanner | 🔲 À faire | `html5-qrcode` ou `jsQR` | | Chat assistant | ⏸ Hors scope | Feature premium — mobile uniquement pour l'instant | | Dockerfile + docker-compose `app.myinfomate.be` | 🔲 À faire | Base : Dockerfile de `myinfomate-landing` | #### Structure des données (depuis manager-app) Tout le contenu est configuré dans manager-app et exposé via `manager-service`. Champs clés à respecter côté rendu web : **ConfigurationDTO** (niveau racine) : - `primaryColor` / `secondaryColor` → format `#HEX`, injectés en CSS variables - `imageSource` → image de couverture - `loaderImageUrl` → image de chargement - `languages` → liste des langues actives (ex: `["FR", "EN", "NL"]`) - `sections` → ordonnées par `order`, filtrées par `isActive` **SectionDTO** (commun à tous les types) : - `title` / `description` → `List` multilingue - `imageSource` → icône/image de la section - `isActive` → masquer si false - `order` → ordre dans le menu de navigation - `type` → dispatcher vers le bon composant **TranslationDTO** : ```json { "language": "FR", "value": "Mon titre" } ``` Tous les champs texte sont multilingues. La langue sélectionnée par le visiteur détermine quelle valeur afficher. **Lien web ↔ instance** : `webSlug` et `publicApiKey` ajoutés sur `Instance` côté backend. Générés automatiquement à la création (slug depuis le nom, clé `ap_xxx`). Endpoint `POST /api/instance/{id}/generate-web-keys` pour les instances existantes. #### Multi-langue & persistance - Langue sélectionnée stockée dans un **React context** au niveau du `layout.tsx` de `[slug]` - Persistée en `localStorage` pour survivre aux reloads - Toutes les fonctions de traduction lisent depuis ce context - Le sélecteur de langue (drapeaux) est accessible depuis toutes les pages #### QR Code - Bouton flottant sur la page de configuration (même comportement que dans `mymuseum-visitapp`) - Scan via `html5-qrcode` ou `jsQR` + accès caméra `getUserMedia` - Fonctionne sur mobile web, Safari iOS inclus (depuis iOS 14) - Comportement : scan → valide l'ID contre la liste des sections → redirige vers la section #### Notes d'implémentation - **Audio player flottant** : utilisé uniquement dans `ArticleDetail`, pas besoin de persister au niveau du layout — composant local à la page article. - **Theming CSS variables** : plus puissant qu'en Flutter (gradients, shadows, hover, transitions natifs CSS). Le visuel sera dans la même veine que `mymuseum-visitapp`. #### Features explicitement hors scope (web) - Beacons BLE — WebBluetooth non supporté sur iOS Safari - Mode offline — Service Worker trop complexe pour le gain - Push notifications — Firebase Web, pas prioritaire --- ## Récapitulatif de la stack par canal | Canal | URL | Tech | Rationale | | ------------------ | -------------------------------- | ------------ | ---------------------------------- | | Kiosk natif | tablet-app installée | Flutter | Existant | | Kiosk web | `kiosk.myinfomate.be/[deviceId]` | Flutter Web | Réutilise tablet-app, contexte OK | | Visiteur natif | mymuseum-visitapp installée | Flutter | Existant | | Visiteur web SaaS | `app.myinfomate.be/[client]` | Next.js | Performance mobile critique | | Back-office | manager-app | Flutter | Existant | | Landings | myinfomate-landing, unov-landing | Next.js | Existant | --- ## Conséquence sur l'organisation du code Flutter Pour que le kiosk web fonctionne proprement et que tablet-app + mymuseum-visitapp restent cohérents, créer un package Dart partagé : ``` manager-app/ (génère client API) └── manager_api_new/ myinfomate_shared/ ← nouveau package Dart ├── widgets/ (composants UI réutilisables) ├── models/ (DTOs métier hors API) ├── theme/ └── utils/ tablet-app/ → consomme manager_api_new + myinfomate_shared mymuseum-visitapp/ → consomme manager_api_new + myinfomate_shared ``` Le canal **visiteur web Next.js** ne consomme pas ce package (techno différente) mais consomme le même backend `manager-service`. La duplication est limitée à la couche de présentation visiteur. --- ## Déploiement Docker ### Existant L'infra est déjà en place dans `manager-service/docker-compose-myinfomate.yml` : - **Traefik v2.8** — reverse proxy + SSL Let's Encrypt automatique - **manager-service** — API .NET 8, image versionnée (`2.0.0`) - **manager-app** — Flutter Web, image versionnée (`2.0.0`) - **MongoDB** — base de données - **Mosquitto MQTT** — ports 1883/9001 - **demo.myinfomate.be** — build Flutter Web de tablet-app, déjà présent Chaque projet a son propre `Dockerfile` multi-stage (Flutter → Nginx ou Node → Alpine). Pas de CI/CD déclaratif — déploiement manuel via `docker compose`. ### Ce qui manque pour la nouvelle archi | À ajouter | Effort | Base | |-----------|--------|------| | Entrée `kiosk.myinfomate.be` dans le docker-compose | Trivial | Copier/adapter l'entrée `demo.myinfomate.be` | | `Dockerfile` pour `visitapp-web` (Next.js) | Faible | Copier le Dockerfile de `myinfomate-landing` | | Entrée `app.myinfomate.be` dans le docker-compose | Trivial | Même pattern Traefik que les autres | ### Développement local Trois fichiers `.env` gèrent les environnements : ``` .env.local → dev local → backend local (gitignore) .env.production.local → build local → backend prod (gitignore) .env.production → valeurs prod par défaut (commité) ``` | Commande | Fichier lu | Usage | |----------|------------|-------| | `npm run dev` | `.env.local` | Dev quotidien | | `npm run build && npm run start` | `.env.production.local` si présent | Tester un build prod en local | ### Déploiement Build et déploiement manuel sur le serveur, cohérent avec l'existant : ```bash docker compose -f docker-compose-myinfomate.yml build visitapp-web docker compose -f docker-compose-myinfomate.yml up -d visitapp-web ``` CI/CD à envisager plus tard si le flow manuel devient trop lourd. ### Point de vigilance Les versions d'image sont fixées à `2.0.0` dans le docker-compose. À synchroniser avec les versions réelles des projets avant tout déploiement. --- ## État d'avancement global ### manager-service ✅ - `WebSlug` + `PublicApiKey` sur `Instance` (migration appliquée) - Endpoint `GET /api/instance/slug/{slug}` (AllowAnonymous) - Endpoint `POST /api/instance/{id}/generate-web-keys` - `ApiKeyAuthenticationHandler` supporte la `PublicApiKey` - CORS prod : `https://app.myinfomate.be` ajouté ### manager-app ✅ - `InstanceDTO` mis à jour avec `webSlug` et `publicApiKey` - Onglet Web dans le menu (existait déjà, branché sur `WebAppScreen`) - `WebAppScreen` : affiche URL visiteurs + clé publique (lecture seule, bouton copier) ### visitapp-web ✅ (repo `GITEA/visitapp-web`) - Bootstrap Next.js 16, App Router, TypeScript, Tailwind - Routing `[slug]` → `[configId]` → `sections/[sectionId]` - Theming runtime CSS variables (couleurs instance + override config) - VisitorContext : langue persistée en localStorage - AppBar gradient + sélecteur de langue - 9 types de sections implémentés : Article, Agenda, Menu, Slider, Video, PDF, Weather, Web - Sections stub (affiche "à venir") : Map, Quiz, Game, GuidedPath ### À faire | Tâche | Priorité | |-------|----------| | Section Map (Leaflet) | Haute — prévoir Opus | | Section Quiz | Moyenne | | Section Game (puzzle) | Basse | | Section GuidedPath | Basse | | QR Scanner sur page configuration | Moyenne | | Dockerfile + entrée docker-compose `app.myinfomate.be` | Haute avant déploiement | | Tester en local + ajuster le visuel | Immédiat | ### Pour tester en local 1. `POST /api/instance/{id}/generate-web-keys` → récupérer `webSlug` et `publicApiKey` 2. Configurer `visitapp-web/.env.local` avec `NEXT_PUBLIC_API_URL=http://localhost:5000` 3. `cd visitapp-web && npm run dev` 4. Ouvrir `http://localhost:3000/{webSlug}`