Files
sejeteralo/CLAUDE.md
Yvv 330726dcb3 Add CLAUDE.md project guide
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 22:54:30 +01:00

105 lines
5.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```bash
# 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 parameters
- `current_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
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.