From 19ac64c856420bdfc880073155d493ba1f5e6d76 Mon Sep 17 00:00:00 2001 From: Yvv Date: Tue, 24 Mar 2026 03:44:59 +0100 Subject: [PATCH] Seed : foyers fixture dev + codes stables dans dev hints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - seed.py : 3 foyers avec codes fixes (DEVTEST2/3/4, RS/RP/PRO) insérés avant les 363 réels ; existing_codes pré-chargé → zéro collision - page citizen : dev hint mis à jour avec les 3 mêmes codes + profils - CLAUDE.md : reformaté en guide de session Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 166 ++++++++++---------- backend/seed.py | 14 +- frontend/app/pages/commune/[slug]/index.vue | 2 +- 3 files changed, 94 insertions(+), 88 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index ad7bb5e..893286c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,104 +1,100 @@ -# CLAUDE.md +# SejeteralO -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +Plateforme de tarification participative de l'eau pour communes françaises. +Les citoyens votent sur la forme de la courbe tarifaire via des éditeurs Bézier interactifs ; +le système calcule un tarif à équilibre de recettes en temps réel. -## Project Overview +## Protocole de début de session -SejeteralO is a participatory water tarification platform for French communes. Citizens vote on tariff curve shapes via interactive Bézier editors; the system computes revenue-balanced pricing in real-time. Built as a **FastAPI + Nuxt 4** monorepo. +1. `git pull --rebase origin main` +2. Vérifier que `data/` existe à la racine — si absent, signaler avant toute opération +3. Si l'objectif de la session n'est pas précisé, le demander -## Development Commands +## Stack -```bash -# Install -cd backend && python3 -m venv venv && . venv/bin/activate && pip install -r requirements.txt -cd frontend && npm install +- **Frontend** : Nuxt 4 (Vue 3, TypeScript) + Pinia ; package manager : npm ; port dev : 3009 +- **Backend** : Python FastAPI + SQLAlchemy 2.0 async + SQLite (aiosqlite) ; port dev : 8000 +- Déploiement : Docker multi-stage + Traefik (backend + frontend) ; CI Woodpecker +- Pas d'UnoCSS — CSS vanilla avec variables CSS palettes dans `main.css` -# Dev servers (run separately) -cd backend && . venv/bin/activate && uvicorn app.main:app --reload --port 8000 -cd frontend && npm run dev # port 3009 +## Structure -# Seed demo data (Saoû commune, 363 households, admin accounts) -cd backend && . venv/bin/activate && python seed.py - -# Tests -cd backend && . venv/bin/activate && python -m pytest tests/ -v - -# Build frontend -cd frontend && npm run build - -# Docker -make docker-up # production (docker/docker-compose.yml) -make docker-dev # dev with hot-reload (+ docker-compose.dev.yml) +``` +frontend/ + app/ + components/ # composants Vue (dont DisplaySettings.vue — 6 palettes) + layouts/ # layouts Nuxt + pages/ + commune/[slug]/index.vue # page principale citoyenne (~1900 lignes) + composables/ + useApi.ts # wraps fetch avec Bearer token ; usage : api.get('/path') + middleware/ # route middleware (auth) + plugins/ + auth-restore.client.ts # restaure token depuis localStorage au démarrage + stores/ + auth.ts # token, role, communeSlug + commune.ts + utils/ + bezier-math.ts # formule Bézier (Cardano + Newton-Raphson) — miroir du backend + nuxt.config.ts # port 3009, apiBase via NUXT_PUBLIC_API_BASE (défaut :8000) +backend/ + app/ + routers/ # 6 routers : auth, communes, tariff, votes, households, content + engine/ + pricing.py # compute_p0(), compute_tariff(), compute_impacts() + integrals.py # coefficients α₁, α₂, β₂ (intégrales Bézier) + median.py # médiane élément par élément des votes + current_model.py # tarif linéaire de référence + models/ # Commune, TariffParams, Household, Vote, AdminUser, CommuneContent + alembic/versions/ # migrations + tests/ + seed.py # Commune Saoû, 363 foyers, codes auth 8 chars, comptes admin +data/ # runtime — JAMAIS dans git (voir Données runtime) +docker/ + docker-compose.yml # backend + frontend (réseau traefik externe) + backend.Dockerfile + frontend.Dockerfile + docker-compose.dev.yml +Makefile # docker-up, docker-dev ``` -## Architecture +## Données runtime (CRITIQUE) -### Backend (FastAPI + SQLAlchemy async + SQLite) +- `data/` à la racine : contenu non géré par git, **jamais écrasé par les commits** +- Volume Docker `backend-data` monté sur `/app` — contient `sejeteralo.db` (SQLite) +- **Avant toute migration de chemin ou écriture sur data/ ou le volume : demander confirmation** +- En dev local : la DB SQLite est dans `backend/` (chemin relatif `./sejeteralo.db`) +- Seed requis après chaque reset DB : `cd backend && python seed.py` -**API prefix:** `/api/v1/` — 6 routers: auth, communes, tariff, votes, households, content. +## Commandes -**Auth:** JWT tokens (python-jose). Admin tokens = 24h, citizen tokens = 4h. Dependencies: `get_current_admin`, `get_current_citizen`, `require_super_admin`. +```bash +# Backend +cd backend && . venv/bin/activate +uvicorn app.main:app --reload --port 8000 --host 0.0.0.0 +python -m pytest tests/ -v +python seed.py # Saoû, 363 foyers, admin accounts -**Database:** SQLAlchemy 2.0 async with aiosqlite. Migrations via Alembic (`backend/alembic/versions/`). Key models: Commune, TariffParams, Household (with 8-char auth_code), Vote, AdminUser, CommuneContent. +# Frontend +cd frontend && npm run dev # :3009 +npm run build -**Engine** (`backend/app/engine/`): The core tariff math. -- `pricing.py` — `compute_p0()`, `compute_tariff()`, `compute_impacts()` -- `integrals.py` — Bézier curve integral computation (α₁, α₂, β₂ coefficients) -- `median.py` — Element-wise median of vote parameters -- `current_model.py` — Baseline linear tariff for comparison +# Docker +make docker-up # production +make docker-dev # dev avec hot-reload +``` -### Frontend (Nuxt 4 + Vue 3 + Pinia) +## Conventions / pièges -**Config:** `nuxt.config.ts`, dev port 3009, API base via `NUXT_PUBLIC_API_BASE`. - -**State:** Pinia stores in `app/stores/` — `auth.ts` (token, role, communeSlug) and `commune.ts`. Auth restored from localStorage on page load via `plugins/auth-restore.client.ts`. - -**API calls:** `app/composables/useApi.ts` — wraps fetch with Bearer token injection. Usage: `const api = useApi(); const data = await api.get('/path')`. - -**Key page:** `app/pages/commune/[slug]/index.vue` (~1900 lines) — the public citizen-facing page with interactive Bézier chart, sidebar auth/vote, histogram, baseline charts, marginal price chart, key metrics banner. All SVG charts use reactive theme bindings. - -## The Bézier Tariff Model - -Two-tier cubic Bézier with **6 citizen-adjustable parameters** + computed p0: - -- **vinf** — inflection volume (tier 1 ↔ tier 2 boundary) -- **a, b, c, d, e** — shape parameters, each ∈ [0, 1] -- **p0** — price at inflection, **auto-computed** to balance revenue: +- **UI français, code anglais** — "foyer" = household (facturation), "électeur" = voter (vote) +- **Modèle Bézier deux niveaux** — 6 paramètres citoyens (vinf, a, b, c, d, e) + p0 auto-calculé : ``` p0 = (Recettes − Σabo − Σβ₂) / Σ(α₁ + α₂) ``` - -Tier 1 (0→vinf): population pricing. Tier 2 (vinf→vmax): exceptional consumption pricing. - -**Implemented twice** — backend (`engine/pricing.py` with numpy) and frontend (`utils/bezier-math.ts` with Cardano's formula + Newton-Raphson). Frontend computes locally for instant UI feedback; server is authoritative. - -## Vote Flow - -1. Citizen authenticates with commune slug + 8-char auth_code → JWT citizen token -2. Frontend loads tariff params, household stats, published/median curve -3. Citizen drags Bézier control points → local p0 recomputation in real-time -4. Submit: POST `/communes/{slug}/votes` with 6 params → backend computes p0, deactivates old votes -5. Aggregation: element-wise median of all active votes produces the collective curve - -## Theme / Dark Mode - -- `DisplaySettings.vue`: 6 palettes (4 light: eau, terre, foret, ardoise; 2 dark: nuit, ocean) -- Global state via `useState('theme-dark')` — shared across components -- CSS variables: `--color-primary`, `--color-bg`, `--color-surface`, `--color-text`, `--color-border` -- SVG-specific: `--svg-plot-bg`, `--svg-grid`, `--svg-text`, `--svg-text-light` -- SVG elements use reactive `t.*` bindings (computed from `isDark`), never hardcoded colors -- `.palette-dark` class on `` for CSS overrides in `main.css` -- Settings persisted in localStorage: `sej-palette`, `sej-fontSize`, `sej-density` - -## Conventions - -- **Language:** All UI labels and messages are in French (no i18n layer) -- **Terminology:** "foyer" = household (bills/consumption context), "électeur" = voter (vote context) -- **SVG charts:** Inverted X axis (high volumes left, 0 right) on main/baseline charts. All colors must be theme-reactive — use `t.*` bindings or `var(--svg-*)` CSS variables, never hardcoded hex in SVG -- **CSS:** Mobile-first with `clamp()` for fluid sizing. Breakpoints: 480 / 768 / 1024px -- **Dev hint boxes:** Use class `.dev-hint` (has dark mode override in main.css), guarded by `v-if="isDev"` -- **Auth codes in dev hints:** Must actually exist in the seeded database - -## CI/CD - -Woodpecker CI (`.woodpecker.yml`): build frontend → build backend → docker push (2 images) → SSH deploy. Docker files in `docker/` with multi-stage builds (base → build → production/development). Production uses Traefik labels for routing. + Implémenté deux fois : backend Python (`engine/pricing.py`) et frontend TS (`utils/bezier-math.ts`) — garder synchronisés +- **Agrégation votes** : médiane élément par élément des votes actifs (pas moyenne) +- **Graphiques SVG** : axe X inversé (volumes élevés à gauche) — utiliser bindings réactifs `t.*` ou `var(--svg-*)`, jamais de couleurs hex codées en dur dans les SVG +- **Thème** : 6 palettes via `useState('theme-dark')` ; CSS vars : `--color-primary`, `--color-bg`, `--color-surface`, `--color-text`, `--color-border`, `--svg-plot-bg`, `--svg-grid`, `--svg-text`, `--svg-text-light` ; `.palette-dark` sur `` +- **Dev hints** : classe `.dev-hint` + `v-if="isDev"` ; les codes auth doivent exister dans la DB seedée +- **Port backend** : 8000 en local (nuxt.config + uvicorn) — la table globale CLAUDE.md indique 8009 par erreur ; les fichiers de config font foi +- **Pas de `ssr: true`** — CSR uniquement diff --git a/backend/seed.py b/backend/seed.py index d67cd82..077bf2e 100644 --- a/backend/seed.py +++ b/backend/seed.py @@ -72,12 +72,21 @@ async def seed(): commune_admin.communes.append(commune) db.add(commune_admin) + # ── Dev fixture households (codes fixes, affichés dans les dev hints) ── + DEV_FIXTURES = [ + {"identifier": "[DEV] Foyer RS 60m³", "status": "RS", "volume_m3": 60.0, "price_paid_eur": 140.0, "auth_code": "DEVTEST2"}, + {"identifier": "[DEV] Foyer RP 120m³", "status": "RP", "volume_m3": 120.0, "price_paid_eur": 265.0, "auth_code": "DEVTEST3"}, + {"identifier": "[DEV] Foyer PRO 350m³","status": "PRO", "volume_m3": 350.0, "price_paid_eur": 680.0, "auth_code": "DEVTEST4"}, + ] + existing_codes = {f["auth_code"] for f in DEV_FIXTURES} + for fixture in DEV_FIXTURES: + db.add(Household(commune_id=commune.id, **fixture)) + # Import households from Eau2018.xls book = xlrd.open_workbook(XLS_PATH) sheet = book.sheet_by_name("CALCULS") nb_hab = 363 - existing_codes = set() for r in range(1, nb_hab + 1): name = sheet.cell_value(r, 0) status = sheet.cell_value(r, 3) @@ -174,10 +183,11 @@ async def seed(): vote_count += 1 await db.commit() - print(f"Seeded: commune 'saou', {nb_hab} households, {vote_count} votes") + print(f"Seeded: commune 'saou', {nb_hab} + 3 fixture households, {vote_count} votes") print(f" Published curve: vinf={ref_vinf}, p0={ref_p0:.3f}") print(f" Super admin: superadmin@sejeteralo.fr / superadmin") print(f" Commune admin Saou: saou@sejeteralo.fr / saou2024") + print(f" Dev fixtures: DEVTEST2 (RS 60m³) · DEVTEST3 (RP 120m³) · DEVTEST4 (PRO 350m³)") if __name__ == "__main__": diff --git a/frontend/app/pages/commune/[slug]/index.vue b/frontend/app/pages/commune/[slug]/index.vue index cb8ccc0..cd39c26 100644 --- a/frontend/app/pages/commune/[slug]/index.vue +++ b/frontend/app/pages/commune/[slug]/index.vue @@ -269,7 +269,7 @@

- Dev: QPF5L9ZK (60m³) + Dev: DEVTEST2 (RS 60m³) · DEVTEST3 (RP 120m³) · DEVTEST4 (PRO 350m³)