Aller au contenu

Frontend (Vue 3 + Pinia)

Décomposition C4-C3 du container SPA. L'architecture post-refactor est entièrement spécifiée dans ADR-010 — ce document en donne la vue orientée building blocks.

Vue C4 niveau 3 frontend (exportée depuis Structurizr) :

C4 Components Frontend — organisms, composables, lib/, stores

5.F.1 Couches

src/
├── components/
│   ├── atoms/         # a- (ABadge, AButton, AIcon, ANumericValue, ASourcingBadge)
│   ├── molecules/     # m- (MSectionHeader, MStatCard, MStationCard, MStatusBadge)
│   ├── organisms/     # o- (OHeroSection, OHydroChart, OKeyMetricsSection, OMapSection,
│   │                  #     OResearchZonesSection, OSiteFooter, OStationDrawer,
│   │                  #     OStationMap, OWhyLindasSection
│   │                  #     + couche IA : OAlertsPanel (B), OChatPanel (C))
│   └── templates/     # t- (TDefaultLayout)
├── pages/             # p- (PHomePage)
├── composables/
│   ├── shared/        # useEscapeClose, useScrollLock, usePolling
│   ├── stations/      # useStationsList, useStationSelection,
│   │                  # useStationMeasurements, useStationDrawer
│   ├── alerts/        # useAlertsPanel (couche IA B)
│   ├── chat/          # useDataChat (couche IA C)
│   └── useI18nList.ts
├── lib/               # logique pure — aucune dépendance Vue/Pinia
│   ├── api-client.ts  # HTTP centralisé + ApiError discriminé
│   ├── charts/        # chart-model (D3 pure)
│   ├── map/           # station-map-mapping (Leaflet mapping pure)
│   ├── constants/     # chart.ts, map.ts, time.ts
│   ├── hydrodaten.ts  # stationToHydrodatenUrl + RESEARCH_OFEV_PREFIX
│   └── status.ts      # type BadgeStatus partagé
├── stores/            # Pinia singletons (stations, status)
├── locales/           # fr.json (vue-i18n FR uniquement)
├── router/            # 1 route (home), scrollBehavior configuré
├── assets/            # main.css (Tailwind layers)
└── main.ts

Préfixes ABEM strictement appliqués (ADR-002) — un composant .vue porte toujours un préfixe a-/m-/o-/t-/p- à son fichier et à sa classe racine.

5.F.2 Pattern façades feature-grouped

useStationsStore portait 3 responsabilités (liste, sélection UI, cache mesures). R3 du refactor a introduit 3 façades read-only sous composables/stations/ :

  • useStationsList() — expose stations, isLoading, error, hasLoadedOnce, loadAll(). Consommé par OMapSection et OKeyMetricsSection.
  • useStationSelection() — expose selectedStation, selectedStationId, selectStation(id), clearSelection(). Consommé par OStationMap (click → sélection) et indirectement par OStationDrawer.
  • useStationMeasurements(stationId: Ref<string | null>) — reçoit un stationId réactif, dérive series, isLoading, error via computed (pas storeToRefs car la clé varie), expose load() et reload(). Consommé par useStationDrawer.

Un barrel composables/stations/index.ts ré-exporte les 3 + le composable orchestrateur useStationDrawer.

Règle enforced — aucun fichier .vue de production n'importe useStationsStore. Vérifiable :

grep -rn "useStationsStore" --include="*.vue" src/
# → 0 résultat

Exceptions documentées dans les JSDoc de tête : tests (.test.ts) et decorators Storybook (seedStations) peuvent $patch le store directement pour le seed de scénarios contrôlés.

5.F.3 Orchestrateur useStationDrawer

OStationDrawer.vue est un organisme visuel — son <script setup> ne contient plus que 22 lignes après le refactor R2 (contre 102 avant). Toute la logique vit dans composables/stations/useStationDrawer.ts :

  • Souscription aux façades useStationSelection + useStationMeasurements(selectedStationId).
  • Snapshot de now à la sélection (fenêtre stable tant que le drawer est ouvert).
  • watch(selectedStationId) → déclenche load() + reset du snapshot.
  • Dérivations présentationnelles : dischargeSeries, coordsLabel, hydrodatenUrl.
  • Montage des primitives transverses : useEscapeClose(isOpen, close) + useScrollLock(isOpen).
  • Méthodes publiques : close(), retry().

Testé en isolation (useStationDrawer.test.ts) via un composant probe — pas besoin de mount le .vue.

5.F.4 Couche lib/ — logique pure

lib/ ne contient aucune dépendance Vue reactivity ni Pinia (hors Ref en signature pour useStationMeasurements). Testable sans monter de composant :

  • api-client.tsapi.getStations(), api.getStationMeasurements(id, params), api.getStatus(), api.getHealth(). Retourne un ApiResponse<T> = { success: true; data: T } | { success: false; error: ApiError }ApiError est une union discriminée network | http | parse. Chaque consumer (stores + composables) nomme sa branche d'échec via le compilateur — impossible d'oublier un cas.
  • charts/chart-model.tscomputeYDomain, findNearestPointByPx. Fonctions pures D3, testées avec des points synthétiques.
  • map/station-map-mapping.tsstationToMarkerOptions, findLatestDischarge. Mapping Leaflet pur (couleurs, radius, fill).
  • hydrodaten.tsstationToHydrodatenUrl(station) + constante RESEARCH_OFEV_PREFIX = 'TBD'. Lie la règle de génération d'URL Hydrodaten à un préfixe seed explicite.
  • constants/{chart,map,time}.ts — magic numbers dédupliqués (MARGIN, MAP_CENTER, MAP_ZOOM, ONE_DAY_MS, NARROW_BREAKPOINT, …).
  • status.ts — type BadgeStatus = 'live' | 'stale' | 'offline' | 'loading' partagé entre MStatusBadge.vue et OHeroSection.vue (auparavant inliné dans chaque fichier).

5.F.5 Stores Pinia

Deux singletons :

  • useStationsStore — état liste + sélection + cache per-station des mesures. Accessible uniquement via les 3 façades (règle §5.F.2).
  • useStatusStore — snapshot ingestion (lastSuccessAt, minutesSinceLastSuccess, isHealthy, today.runsCount, …). Pas de façade — 2 consommateurs prod (OHeroSection, OKeyMetricsSection), rule-of-three non atteinte. Décision explicitée dans le JSDoc de tête de stores/status.ts et tracée dans ADR-010 §2.2.

Chaque fetch utilise api.* de lib/api-client.ts — aucun fetch() direct dans les stores ni les composables.

5.F.6 Couche IA — chat sur données structurées (ADR-012, extension C)

Le chat est livré en C6 comme une section dédiée scrollable (organisme OChatPanel), pas un widget flottant (décision D7) — cohérent avec la page single-scroll.

  • OChatPanel.vue — organisme présentationnel : champ de question, fil question/réponse, trace discrète des tools appelés (used[], transparence du grounding), disclaimer « réponses assistées par IA » (ADR-008), questions d'exemple cliquables pour la découvrabilité (polish chat-ux).
  • useDataChat() — façade on-demand, façon useStationNarrative et non adossée à un store Pinia : un fil de chat est un état local au panneau, pas une donnée partagée cross-composant. Elle possède son état réactif (messages, isLoading, error, hasMessages) et délègue l'appel HTTP à api.ask(question, language?).
  • Backend stateless / single-turn — chaque POST /ask est indépendant (pas d'historique serveur). Le messages ref est un transcript purement client de la session courante pour le rendu du fil ; il n'est jamais rejoué au serveur.
  • Échecs honnêtesuseDataChat mappe les refus de garde en raisons UI explicites (429 → trop de requêtes, 503 → IA indisponible, 400 → requête invalide) plutôt que de laisser fuiter des formes HTTP brutes. api.ask est ajouté à lib/api-client.ts (même ApiResponse<T> discriminé que les autres appels).

Testé en isolation (useDataChat.test.ts, OChatPanel.test.ts) — la façade n'a pas de store à monter ; les appels api.ask sont mockés.