Seed : foyers fixture dev + codes stables dans dev hints
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
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:
166
CLAUDE.md
166
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
|
- **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.
|
|
||||||
|
|||||||
@@ -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__":
|
||||||
|
|||||||
@@ -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 (60m³)
|
<strong>Dev:</strong> DEVTEST2 (RS 60m³) · DEVTEST3 (RP 120m³) · DEVTEST4 (PRO 350m³)
|
||||||
</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;">
|
||||||
|
|||||||
Reference in New Issue
Block a user