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

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

16 KiB

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 webapp.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
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.

// layout.tsx — injection dans <html style={...}>
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 / descriptionList<TranslationDTO> 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 :

{ "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 :

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}