Seed : foyers fixture dev + codes stables dans dev hints
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

- 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 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-03-24 03:44:59 +01:00
parent 017806025c
commit 19ac64c856
3 changed files with 94 additions and 88 deletions

166
CLAUDE.md
View File

@@ -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 - **Frontend** : Nuxt 4 (Vue 3, TypeScript) + Pinia ; package manager : npm ; port dev : 3009
# Install - **Backend** : Python FastAPI + SQLAlchemy 2.0 async + SQLite (aiosqlite) ; port dev : 8000
cd backend && python3 -m venv venv && . venv/bin/activate && pip install -r requirements.txt - Déploiement : Docker multi-stage + Traefik (backend + frontend) ; CI Woodpecker
cd frontend && npm install - Pas d'UnoCSS — CSS vanilla avec variables CSS palettes dans `main.css`
# Dev servers (run separately) ## Structure
cd backend && . venv/bin/activate && uvicorn app.main:app --reload --port 8000
cd frontend && npm run dev # port 3009
# Seed demo data (Saoû commune, 363 households, admin accounts) ```
cd backend && . venv/bin/activate && python seed.py frontend/
app/
# Tests components/ # composants Vue (dont DisplaySettings.vue — 6 palettes)
cd backend && . venv/bin/activate && python -m pytest tests/ -v layouts/ # layouts Nuxt
pages/
# Build frontend commune/[slug]/index.vue # page principale citoyenne (~1900 lignes)
cd frontend && npm run build composables/
useApi.ts # wraps fetch avec Bearer token ; usage : api.get<Type>('/path')
# Docker middleware/ # route middleware (auth)
make docker-up # production (docker/docker-compose.yml) plugins/
make docker-dev # dev with hot-reload (+ docker-compose.dev.yml) 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. # Docker
- `pricing.py``compute_p0()`, `compute_tariff()`, `compute_impacts()` make docker-up # production
- `integrals.py` — Bézier curve integral computation (α₁, α₂, β₂ coefficients) make docker-dev # dev avec hot-reload
- `median.py` — Element-wise median of vote parameters ```
- `current_model.py` — Baseline linear tariff for comparison
### Frontend (Nuxt 4 + Vue 3 + Pinia) ## Conventions / pièges
**Config:** `nuxt.config.ts`, dev port 3009, API base via `NUXT_PUBLIC_API_BASE`. - **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é :
**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<Type>('/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:
``` ```
p0 = (Recettes Σabo Σβ₂) / Σ(α₁ + α₂) p0 = (Recettes Σabo Σβ₂) / Σ(α₁ + α₂)
``` ```
Implémenté deux fois : backend Python (`engine/pricing.py`) et frontend TS (`utils/bezier-math.ts`) — garder synchronisés
Tier 1 (0→vinf): population pricing. Tier 2 (vinf→vmax): exceptional consumption pricing. - **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
**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. - **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 `<html>`
- **Dev hints** : classe `.dev-hint` + `v-if="isDev"` ; les codes auth doivent exister dans la DB seedée
## Vote Flow - **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
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 `<html>` 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.

View File

@@ -72,12 +72,21 @@ async def seed():
commune_admin.communes.append(commune) commune_admin.communes.append(commune)
db.add(commune_admin) 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 # Import households from Eau2018.xls
book = xlrd.open_workbook(XLS_PATH) book = xlrd.open_workbook(XLS_PATH)
sheet = book.sheet_by_name("CALCULS") sheet = book.sheet_by_name("CALCULS")
nb_hab = 363 nb_hab = 363
existing_codes = set()
for r in range(1, nb_hab + 1): for r in range(1, nb_hab + 1):
name = sheet.cell_value(r, 0) name = sheet.cell_value(r, 0)
status = sheet.cell_value(r, 3) status = sheet.cell_value(r, 3)
@@ -174,10 +183,11 @@ async def seed():
vote_count += 1 vote_count += 1
await db.commit() 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" Published curve: vinf={ref_vinf}, p0={ref_p0:.3f}")
print(f" Super admin: superadmin@sejeteralo.fr / superadmin") print(f" Super admin: superadmin@sejeteralo.fr / superadmin")
print(f" Commune admin Saou: saou@sejeteralo.fr / saou2024") 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__": if __name__ == "__main__":

View File

@@ -269,7 +269,7 @@
<button type="submit" class="btn btn-primary" :disabled="authLoading" style="padding: 0.35rem 0.75rem;">OK</button> <button type="submit" class="btn btn-primary" :disabled="authLoading" style="padding: 0.35rem 0.75rem;">OK</button>
</form> </form>
<p v-if="isDev" :style="{ marginTop: '0.4rem', padding: '0.3rem 0.5rem', background: t.devBg, border: '1px solid ' + t.devBorder, borderRadius: '5px', fontSize: '0.7rem', color: isDark ? '#fcd34d' : 'inherit' }"> <p v-if="isDev" :style="{ marginTop: '0.4rem', padding: '0.3rem 0.5rem', background: t.devBg, border: '1px solid ' + t.devBorder, borderRadius: '5px', fontSize: '0.7rem', color: isDark ? '#fcd34d' : 'inherit' }">
<strong>Dev:</strong> QPF5L9ZK (60) <strong>Dev:</strong> DEVTEST2 (RS 60) · DEVTEST3 (RP 120) · DEVTEST4 (PRO 350)
</p> </p>
</div> </div>
<div class="sidebar-auth-block" style="opacity: 0.5; border-top: 1px solid var(--color-border); padding-top: 0.75rem; margin-top: 0.75rem;"> <div class="sidebar-auth-block" style="opacity: 0.5; border-top: 1px solid var(--color-border); padding-top: 0.75rem; margin-top: 0.75rem;">