§8 — Concepts transverses¶
Les concepts qui s'appliquent de manière cohérente sur l'ensemble du code base — ni purement frontend ni purement backend. Chaque concept renvoie à son ADR ou à sa sous-page quand le détail l'exige.
8.1 Design system — Atomic Design + ABEM¶
Le design system est spécifié par ADR-002 (ABEM) et ADR-009 (Storybook scope). Il est catalogué et naviguable sur storybook.alpimonitor.fr — 15 composants présentationnels, 46 stories, 5 pages MDX design system.
Détail tokens, composants et règles éditoriales : design-system.md.
8.2 Conventions Git, code et commits¶
- Commits atomiques + conventional commits en anglais (règle d'engagement 6 de
CLAUDE.md). - ABEM strictement appliqué sur 100 % des composants Vue (ADR-002).
- Règle façade enforced côté web : aucun consumer prod n'importe
useStationsStoredirectement (ADR-010). - Gate
pnpm typecheckfonctionnel depuis le fix C1 (scriptvue-tsc --noEmit --project tsconfig.app.json, voir passe-c-findings §C1). - Push feature branches régulièrement pour déclencher la CI en avance, éviter la flakiness cross-env exposée au merge (mémoire projet
feedback_ci_feedback_loop).
Détail par domaine : conventions.md.
8.3 Observabilité¶
- Logs structurés Pino JSON stdout sur l'API, captés par Coolify. Pas de PII, pas d'IP en clair en base.
/api/v1/health— liveness probe minimaliste avec probe DB (SELECT 1). Consommé par Coolify/Traefik./api/v1/status— exposeIngestionRun.lastRun,lastSuccessAt,healthyThresholdMinutes, compteurs journée. Lu parMStatusBadgedans le hero UI (polling 60 s)./api/v1/ai/status— observabilité de la couche IA (ADR-012, D). Agrège lesLlmCallRundu jour (callsToday,errorRate,costUsdToday,avgLatencyMs). Un décorateurObservingLlmClientpersiste unLlmCallRunpar appel LLM, succès comme échec, étiqueté paroperation(narrationpour A,chatpour C) — la même observabilité couvre narration et chat, et alimente le plafond coût du chat (cf. §8.9).- Pas d'APM, pas de tracing distribué — hors scope v1, cf. §10 non-scope.
8.4 Sécurité¶
- 6 headers nginx côté SPA : HSTS (
max-age=31536000; includeSubDomains), CSP, X-Frame-OptionsDENY, X-Content-Type-Optionsnosniff, Referrer-Policystrict-origin-when-cross-origin, Cross-Origin-Opener-Policysame-origin. - CORS allowlist côté API — aucune étoile,
CORS_ORIGINSenv. Origines dev + prod whitelistées explicitement. - Container non-root (
USER app) + volume pre-créé + chowné dans le Dockerfile avantUSER(lesson post-mortem EACCES). - Zod systématique — validation runtime sur tous les endpoints. Payload malformé → 400
VALIDATION_ERROR, pas de crash. - Helmet + rate-limit API global reportés post-candidature (read-only public acceptable pour démo). Exception ciblée :
POST /ask(couche IA C, appels LLM payants) est protégé par un rate-guard maison (plafond coût $0,50/j + fenêtres par IP 5/min & 20/j) — refus 429 avant tout appel LLM, cf. §8.9. - Appels LLM server-side uniquement — la CSP actuelle (
connect-src 'self') interdit déjà tout appel LLM depuis le navigateur. Clés (MISTRAL_API_KEY) par env, jamais committées.
8.5 Internationalisation¶
vue-i18nFR uniquement en v1. Clésfr.jsonservies viauseI18n().t(key)etuseI18nList<T>(key)(composable dédié pour les arrays).- Multi-langue = backlog v2. Le public cible CREALP est francophone. Ajouter
en/deimpliquerait deux locales à maintenir sans gain immédiat. - Labels UI en français, code / commits / ADR en anglais pour le premier, français pour les autres (§2.2 contraintes).
8.6 Gestion d'erreurs¶
ApiErrorunion discriminée côté web —network | http | parse. Chaque consumer nomme sa branche via le compilateur TS.apiErrorMessage(error)centralise le rendu texte pour les logs / fallbacks (ADR-010 §1.3).- Ingestion LINDAS non-fatal — un échec d'archive disque logue
warnmais ne fait pas échouer l'upsert mesures. L'API reste up même si LINDAS est down (fallback seed + badge "données indisponibles"). entrypoint.shtolérant au seed échoué —prisma migrate deployfatal,prisma db seednon-fatal (warn + continue). Une seed cassée ne met pas l'API offline (post-mortem 2026-04-21).
8.7 Tests — pyramide¶
- Fonctions pures (
lib/charts/chart-model,lib/map/station-map-mapping,lib/hydrodaten) — tests unitaires Vitest, pas de mount, pas de Pinia. - Composables (
useStationDrawer,useStationsList,useStationSelection,useStationMeasurements,useEscapeClose,useScrollLock,usePolling) — tests via composant probe +effectScope. - Intégration composants (
OHydroChart,OStationMap,OStationDrawer,OKeyMetricsSection,MStationCard,ASourcingBadge) —@vue/test-utils+ mount + assertions DOM. - API — routes (
health,status,stations,station-measurements,station-status,cors) + ingestion (lindas-parser,lindas-ingestion) + couche IA : narration (features pures, cacheInsight), observabilité (computeCostUsd,ObservingLlmClient), anomalie (detectAnomalypur, scan), chat (chat-servicesurQueryPorten mémoire — sans DB,rate-guardà horloge injectée, dispatcher des 4 tools). - Total : 344 tests verts (208 API + 136 web) — dont la couche IA additive (A+D+B+C). Pas de E2E Playwright en v1 (reporté backlog).
8.8 Gestion de la documentation¶
- Source de vérité — ce dossier
docs/(arc42, ce site) + les ADR dansdocs/architecture/adr/(sources migrées ici §9). - Règle de dépendance — un changement de décision = mise à jour du doc concerné + ADR si structurant. Un ADR obsolète passe à
Statut: Superseded by ADR-XXX, jamais supprimé. - README racine court — pointe vers
docs.alpimonitor.fr(cette doc) etstorybook.alpimonitor.frpour les détails.
8.9 Couche IA — grounding strict, observabilité et gardes¶
Concepts transverses qui s'appliquent à toute la couche IA (ADR-012), additive et isolée du flux LINDAS → Prisma → API.
- Grounding STRICT — partout, le LLM ne fait que reformuler des faits déjà calculés / récupérés : narration (features pures pré-calculées, A), chat (faits récupérés via le
QueryPort, C). Il ne prédit rien, n'invente rien, n'extrapole rien. Hors périmètre des données récupérées → « je ne peux pas répondre » (chat). Toute valeur chiffrée d'une sortie IA provient d'une donnée groundée. - Sorties IA jamais présentées comme vérité hydrologique — narration étiquetée « résumé assisté par IA », chat avec disclaimer + trace des tools réellement appelés, anomalie étiquetée « statistique, non calibrée hydrologie experte ». Lignée de la transparence de sourcing (ADR-008).
- Provider derrière une interface isolée (
ai/llm-client.ts) — le code produit ne connaît jamais Mistral en direct. Client LiteLLM-ready (OpenAI-compatible viaLITELLM_BASE_URL) mais proxy LiteLLM non déployé — point d'extension assumé, pas une fonctionnalité livrée. - Observabilité unifiée — un
LlmCallRunpar appel (succès/échec), discriminé paroperation(narration|chat), agrégé sur/ai/status. La même source alimente le plafond coût du chat. - Garde unifié du seul endpoint coûteux (
POST /ask) — plafond coût quotidien ($0,50, disjoncteur global lu depuis l'observabilité) + rate-limit par IP (5/min, 20/j), dans un seul garde maison testable à horloge injectée. Refus 429 avant tout appel LLM. - Hexagonal localisé — la frontière ports/adapters (
QueryPort/PrismaQueryAdapter) n'existe qu'à la couche chat (C), où elle a une vraie valeur (testabilité du raisonnement sans DB). Le reste du backend reste volontairement plat (YAGNI, ADR-003) — on n'hexagonalise pas « par cohérence ». - Retrieval structuré, jamais vectoriel — function-calling sur 4 fonctions whitelistées, pas de text-to-SQL, pas d'embeddings, pas de vector store (sur-ingénierie écartée pour ~10 stations de données structurées, ADR-012 alternatives).