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
# 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<Type>('/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<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:
- **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 `<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.
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 `<html>`
- **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

View File

@@ -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__":

View File

@@ -269,7 +269,7 @@
<button type="submit" class="btn btn-primary" :disabled="authLoading" style="padding: 0.35rem 0.75rem;">OK</button>
</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' }">
<strong>Dev:</strong> QPF5L9ZK (60)
<strong>Dev:</strong> DEVTEST2 (RS 60) · DEVTEST3 (RP 120) · DEVTEST4 (PRO 350)
</p>
</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;">