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

364 lines
16 KiB
Markdown

# 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 <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` / `description``List<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** :
```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}`