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

5.3 KiB
Raw Permalink Blame History

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.pycompute_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.