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) :
5.F.1 Couches¶
src/
├── components/
│ ├── atoms/ # a- (ABadge, AButton, AIcon, ANumericValue, ASourcingBadge)
│ ├── molecules/ # m- (MSectionHeader, MStatCard, MStationCard, MStatusBadge
│ │ # + énergie : MCategorySelect, MEnergySource, MPageHeader, MVerdictGroup
│ │ # + accueil : MDomainCard)
│ ├── organisms/ # o- (OHeroSection, OHydroChart, OKeyMetricsSection, OMapSection,
│ │ # OResearchZonesSection, OSiteFooter, OStationDrawer,
│ │ # OStationMap, OWhyLindasSection
│ │ # + couche IA : OAlertsPanel (B), OChatPanel (C)
│ │ # + énergie : OEnergyPositioning, OEnergyTrajectory,
│ │ # OEnergyBreakdown, OPositioningChart, OTrajectoryChart,
│ │ # OBreakdownChart, OGroupingMethod
│ │ # + plateforme : OSiteHeader, OHomeMethod, OHomeDomains)
│ └── templates/ # t- (TDefaultLayout : en-tête, main, pied de page)
├── pages/ # p- (PHomePage, PHydrologyPage, PEnergyPage, PGroupingMethodPage)
├── composables/
│ ├── shared/ # useEscapeClose, useScrollLock, usePolling
│ ├── stations/ # useStationsList, useStationSelection,
│ │ # useStationMeasurements, useStationDrawer
│ ├── alerts/ # useAlertsPanel (couche IA B)
│ ├── chat/ # useDataChat (couche IA C)
│ ├── energy/ # useEnergyTariffs (extension énergie, ADR-013 ; loadPositioning pour l'accueil)
│ └── 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, energy-chart.ts
│ ├── energy/ # energy-presentation (formatage fr-CH, verdicts, encart H1),
│ │ # grouping-snapshot (chiffres datés de la note de méthode)
│ ├── home/ # domain-figures (chiffres des cartes de l'accueil)
│ ├── hydrodaten.ts # stationToHydrodatenUrl + RESEARCH_OFEV_PREFIX
│ ├── navigation.ts # isSectionActive (lien de domaine actif sur ses sous-pages)
│ ├── page-title.ts # titre d'onglet « Page — AlpiMonitor »
│ └── status.ts # type BadgeStatus partagé
├── stores/ # Pinia singletons (stations, status, alerts, aiStatus, energy)
├── locales/ # fr.json (vue-i18n FR uniquement)
├── router/ # 4 routes : / (accueil), /hydrologie, /energie, /energie/regroupement
│ # (domaines chargés à la demande) ; redirection des anciennes ancres
│ # /#map|#alerts|#chat ; titre d'onglet par route (legacy-anchors.ts)
├── 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()— exposestations,isLoading,error,hasLoadedOnce,loadAll(). Consommé parOMapSectionetOKeyMetricsSection.useStationSelection()— exposeselectedStation,selectedStationId,selectStation(id),clearSelection(). Consommé parOStationMap(click → sélection) et indirectement parOStationDrawer.useStationMeasurements(stationId: Ref<string | null>)— reçoit unstationIdréactif, dériveseries,isLoading,errorviacomputed(passtoreToRefscar la clé varie), exposeload()etreload(). Consommé paruseStationDrawer.
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 :
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éclencheload()+ 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.ts—api.getStations(),api.getStationMeasurements(id, params),api.getStatus(),api.getHealth(). Retourne unApiResponse<T> = { success: true; data: T } | { success: false; error: ApiError }oùApiErrorest une union discriminéenetwork | http | parse. Chaque consumer (stores + composables) nomme sa branche d'échec via le compilateur — impossible d'oublier un cas.charts/chart-model.ts—computeYDomain,findNearestPointByPx. Fonctions pures D3, testées avec des points synthétiques.map/station-map-mapping.ts—stationToMarkerOptions,findLatestDischarge. Mapping Leaflet pur (couleurs, radius, fill).hydrodaten.ts—stationToHydrodatenUrl(station)+ constanteRESEARCH_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— typeBadgeStatus = 'live' | 'stale' | 'offline' | 'loading'partagé entreMStatusBadge.vueetOHeroSection.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 destores/status.tset 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çonuseStationNarrativeet 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 /askest indépendant (pas d'historique serveur). Lemessagesref est un transcript purement client de la session courante pour le rendu du fil ; il n'est jamais rejoué au serveur. - Échecs honnêtes —
useDataChatmappe 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.askest ajouté àlib/api-client.ts(mêmeApiResponse<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.
5.F.7 Extension énergie — écrans ElCom (ADR-013)¶
Page PEnergyPage sur la route /energie, chargée à la demande (chunk séparé) : ni l'accueil ni la page hydrologie n'en portent le poids. Elle est atteinte par la barre de navigation (§5.F.8). Le pied de page y passe en variante energy (sources ElCom et OFS, sans les sources hydrologiques).
useEnergyStore+ façadeuseEnergyTariffs()— même frontière queuseAlertsPanel(ADR-010) : les organismes n'importent jamais le store. Les trois écrans sont chargés ensemble une fois par visite (tarifs annuels, pas de polling) et partagent une catégorie sélectionnée (H4 par défaut). Un 404ENERGY_DATA_NOT_IMPORTEDest un état distinct d'une erreur.- Trois organismes de section —
OEnergyPositioning(trois groupes de verdict, détail sur quatre bases avec tableau),OEnergyTrajectory,OEnergyBreakdown(avec l'encart H1 sur le comptage). Chacun affiche source et millésime (MEnergySource) ; une valeurnulls'affiche « donnée manquante », jamais 0. - Trois graphiques —
OPositioningChart,OTrajectoryChart,OBreakdownChart, sur le patron deOHydroChart: échelles D3 dans descomputed, rendu SVG par Vue, largeur suivie parResizeObserver. La médiane ElCom est un objet graphique distinct des boîtes ; une valeur manquante coupe une ligne au lieu d'être interpolée. lib/energy/energy-presentation.ts— logique pure : formatagefr-CH, regroupement par verdict, lignes de l'encart H1 déduites des taux publiés.- Note de méthode
PGroupingMethodPage(/energie/regroupement, liée depuis l'en-tête de/energie) — remplace la carte d'éligibilité du cadrage initial. Page statique :OGroupingMethodaffiche un instantané daté et sourcé (lib/energy/grouping-snapshot.ts, mesuré le 2026-09-27 sur les trois communes de SEDRE SA) en trois blocs : ce que les données ouvertes établissent, ce qu'elles approximent (avec la méthode), ce qui est hors de portée (la donnée interne du gestionnaire de réseau, champ par champ). Les critères juridiques des regroupements sont affichés comme non vérifiés et ne sont associés à aucune donnée ; un test le garantit. - En-tête partagé
MPageHeader— en-tête sombre des deux pages énergie (h1, sous-titre, introduction, emplacement pour les contrôles).
5.F.8 Plateforme — accueil et navigation (ADR-014)¶
La plateforme compte quatre pages, reliées par une barre de navigation commune (ADR-014).
TDefaultLayoutporte l'en-tête, le<main id="main-content">et le pied de page de toutes les pages ; deux props choisissent les variantes (headerVariantclair/sombre,footerVarianthydro/energy/platform).OSiteHeader— marque vers/, liens « Hydrologie » et « Énergie », lien d'évitement en premier élément focusable ; sticky, 56 px, sans menu burger. Le domaine actif est marqué par soulignement et graisse ; il reste actif sur ses sous-pages grâce àlib/navigation.ts(isSectionActive), car des routes déclarées à plat ne le permettent pas à vue-router.aria-currentvautpagesur la page elle-même,truesur une sous-page.PHomePage(/) — introduction,OHomeMethod(les quatre principes de la méthode, statique) etOHomeDomains(uneMDomainCardpar domaine). Seule page chargée d'emblée : l'hydrologie et l'énergie sont des chunks séparés.MDomainCard— molécule présentationnelle : question, un chiffre, sa date, la source et un lien ; quatre états (loading,error,missing,dated). Un chiffre n'est jamais affiché sans sa date.OHomeDomains— branche les deux chiffres sur des données déjà servies : le débit à Porte du Scex (code OFEV 2009, exutoire du Rhône valaisan) depuisuseStationsList, avec l'heure de mesure en heure suisse ; la répartition des verdicts de positionnement depuisuseEnergyTariffs().loadPositioning, qui charge l'écran de positionnement seul, avec son propre état, sans marquer la page énergie comme chargée. Logique pure danslib/home/domain-figures.ts.PHydrologyPage(/hydrologie) — l'ancien tableau de bord servi sur/, inchangé hormis la hauteur du hero (écran moins l'en-tête). Les anciennes ancres/#map,/#alerts,/#chatredirigent vers la même ancre sur/hydrologie(router/legacy-anchors.ts).- Titres d'onglet — chaque route porte la clé i18n de son nom (
meta.titleKey) ;router.afterEachfixe « Hydrologie — AlpiMonitor », et « AlpiMonitor » seul sur l'accueil.