5.3 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
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.
Development Commands
# Install
cd backend && python3 -m venv venv && . venv/bin/activate && pip install -r requirements.txt
cd frontend && npm install
# Dev servers (run separately)
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
# 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)
Architecture
Backend (FastAPI + SQLAlchemy async + SQLite)
API prefix: /api/v1/ — 6 routers: auth, communes, tariff, votes, households, content.
Auth: JWT tokens (python-jose). Admin tokens = 24h, citizen tokens = 4h. Dependencies: get_current_admin, get_current_citizen, require_super_admin.
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.
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 parameterscurrent_model.py— Baseline linear tariff for comparison
Frontend (Nuxt 4 + Vue 3 + Pinia)
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:
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
- Citizen authenticates with commune slug + 8-char auth_code → JWT citizen token
- Frontend loads tariff params, household stats, published/median curve
- Citizen drags Bézier control points → local p0 recomputation in real-time
- Submit: POST
/communes/{slug}/voteswith 6 params → backend computes p0, deactivates old votes - 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 fromisDark), never hardcoded colors .palette-darkclass on<html>for CSS overrides inmain.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 orvar(--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 byv-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.