105 lines
5.3 KiB
Markdown
105 lines
5.3 KiB
Markdown
# 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.
|