2 Commits
main ... CI

Author SHA1 Message Date
syoul
45253ba705 Migrate CI to single-step DinD with Fabio/Consul
Replace 5-step pipeline (build/test/push/push/deploy) with single
docker:dind step that builds and deploys in-place via Docker socket.

- .woodpecker.yml: single-step DinD, 1 secret (SECRET_KEY)
- docker-compose.fabio.yml: overlay with SERVICE_* labels for Registrator
- docker-compose.yml: add ports without host bind (Fabio/Traefik routing)
- docker-compose.dev.yml: named volumes with bind driver
- Dockerfiles: install curl, HEALTHCHECK via curl /api/health
- Makefile: docker-fabio, consul-services, fabio-routes targets

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 18:15:12 +01:00
syoul
ff901064e9 add CI branch 2026-03-13 17:40:31 +01:00
15 changed files with 330 additions and 839 deletions

4
.gitignore vendored
View File

@@ -56,7 +56,9 @@ data/DEV-CREDENTIALS.md
# Data files (research, not part of the app) # Data files (research, not part of the app)
*.pdf *.pdf
*.xls *.xls
!Eau2018.xls
*.xlsx *.xlsx
*.ipynb *.ipynb
eau.py eau.py
# User Syl
/docs-syoul

View File

@@ -1,217 +1,22 @@
when: when:
- branch: main branch: main
event: push event: push
steps: steps:
- name: build
- name: security-check image: docker:dind
image: alpine:3.20
commands:
- |
if [ -f .env ]; then
echo "ERREUR: .env ne doit pas etre commite dans le depot"
exit 1
fi
- 'grep -q "^\.env$" .gitignore || (echo "ERREUR: .env manquant dans .gitignore" && exit 1)'
- echo "Security check OK"
- name: validate
image: docker:27-cli
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment: environment:
APP_DOMAIN: validate.example.com COMPOSE_PROJECT_NAME: ${CI_REPO_OWNER,,}-${CI_REPO_NAME,,}-${CI_COMMIT_BRANCH//\//-}
SECRET_KEY: placeholder DOMAIN: sejeteralo.org
commands: LETSENCRYPT_HOST: sejeteralo.org
- | SERVICE_8000_TAGS: urlprefix-sejeteralo.org:443/api
export COMPOSE_PROJECT_NAME=$(printf '%s-%s-%s' "$CI_REPO_OWNER" "$CI_REPO_NAME" "$CI_COMMIT_BRANCH" | tr 'A-Z/' 'a-z-') SERVICE_3000_TAGS: urlprefix-sejeteralo.org:443/*
docker compose -f docker/docker-compose.yml config --quiet
- echo "docker-compose.yml valide"
- name: test-backend
image: python:3.11-slim
commands:
- pip install -r backend/requirements.txt
- cd backend && python -m pytest tests/ -v --tb=short -k "not (test_saou_data_loaded or test_p0 or test_full_tariff or test_linear_p0)"
# NOTE: volumes + pas de from_secret : compatible
- name: build-backend
image: docker:27-cli
volumes:
- /var/run/docker.sock:/var/run/docker.sock
commands:
- docker build -t sejeteralo-backend:latest -f docker/backend.Dockerfile --target production .
- echo "Image backend construite"
# NOTE: volumes + pas de from_secret : compatible
- name: build-frontend
image: docker:27-cli
volumes:
- /var/run/docker.sock:/var/run/docker.sock
commands:
- docker build -t sejeteralo-frontend:latest -f docker/frontend.Dockerfile --target production .
- echo "Image frontend construite"
# NOTE: volumes + pas de from_secret : compatible
- name: sbom-generate
image: alpine:3.20
volumes:
- /var/run/docker.sock:/var/run/docker.sock
commands:
- apk add --no-cache curl
- curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin latest
- mkdir -p .reports
- syft sejeteralo-backend:latest -o cyclonedx-json --file .reports/sbom-backend.cyclonedx.json
- syft sejeteralo-frontend:latest -o cyclonedx-json --file .reports/sbom-frontend.cyclonedx.json
- echo "SBOM generes"
# NOTE: volumes + pas de from_secret : compatible
- name: sbom-scan
image: aquasec/trivy:0.70.0
volumes:
- /home/syoul/trivy-cache:/root/.cache/trivy
commands:
- trivy sbom --format json --output .reports/trivy-backend.json .reports/sbom-backend.cyclonedx.json
- trivy sbom --format json --output .reports/trivy-frontend.json .reports/sbom-frontend.cyclonedx.json
- echo "Scan CVE termine"
# NOTE: from_secret + pas de volumes : compatible
- name: sbom-publish
image: alpine/curl:latest
environment:
DTRACK_TOKEN:
from_secret: dependency_track_token
DTRACK_DOMAIN:
from_secret: dtrack_domain
commands:
- |
VERSION=$(date +%Y-%m-%d)-$(echo "$CI_COMMIT_SHA" | cut -c1-8)
for COMPONENT in backend frontend; do
HTTP=$(curl -s -o /tmp/dtrack-resp.txt -w "%{http_code}" -X POST "https://$DTRACK_DOMAIN/api/v1/bom" \
-H "X-Api-Key: $DTRACK_TOKEN" \
-F "autoCreate=true" \
-F "projectName=sejeteralo-$COMPONENT" \
-F "projectVersion=$VERSION" \
-F "bom=@.reports/sbom-$COMPONENT.cyclonedx.json")
echo "HTTP $HTTP sejeteralo-$COMPONENT : $(cat /tmp/dtrack-resp.txt)"
[ "$HTTP" -ge 200 ] && [ "$HTTP" -lt 300 ] || exit 1
done
# NOTE: from_secret + pas de volumes : compatible
- name: write-env
image: alpine:3.20
environment:
APP_DOMAIN:
from_secret: app_domain
SECRET_KEY: SECRET_KEY:
from_secret: secret_key from_secret: SECRET_KEY
commands: commands:
- env | grep -E "^(APP_DOMAIN|SECRET_KEY)=" > .env.deploy - docker compose -f docker/docker-compose.yml -f docker/docker-compose.fabio.yml up --build -d
- OWNER=$(echo "$CI_REPO_OWNER" | tr 'A-Z' 'a-z') && REPO=$(echo "$CI_REPO_NAME" | tr 'A-Z' 'a-z') && BRANCH=$(echo "$CI_COMMIT_BRANCH" | tr 'A-Z/' 'a-z-') && echo "COMPOSE_PROJECT_NAME=$OWNER-$REPO-$BRANCH" >> .env.deploy
- echo ".env.deploy cree ($(wc -c < .env.deploy) octets)"
- name: test-env
image: alpine:3.20
commands:
- |
[ -f .env.deploy ] || { echo "FAIL: .env.deploy introuvable"; exit 1; }
echo "PASS: .env.deploy present"
- |
VAL=$(grep '^COMPOSE_PROJECT_NAME=' .env.deploy | cut -d= -f2)
[ -z "$VAL" ] && echo "FAIL: COMPOSE_PROJECT_NAME vide" && exit 1
echo "PASS: COMPOSE_PROJECT_NAME = $VAL"
- |
VAL=$(grep '^APP_DOMAIN=' .env.deploy | cut -d= -f2)
[ -z "$VAL" ] && echo "FAIL: APP_DOMAIN vide" && exit 1
echo "PASS: APP_DOMAIN = $VAL"
# NOTE: volumes + pas de from_secret : compatible
# Routing Fabio gere automatiquement par Registrator via labels SERVICE_* du compose
- name: deploy
image: docker:27-cli
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - ${DOCKER_SOCKET_LOCATION:-/var/run/docker.sock}:/var/run/docker.sock
- /opt/sejeteralo:/opt/sejeteralo
commands:
- cp .env.deploy /opt/sejeteralo/.env
- chmod 600 /opt/sejeteralo/.env
- cp docker/docker-compose.yml /opt/sejeteralo/docker-compose.yml
# Arreter avant le challenge ACME : libere le webroot pour sonic-acme-1
- cd /opt/sejeteralo && docker compose stop
- |
DOMAIN=$(grep '^APP_DOMAIN=' /opt/sejeteralo/.env | cut -d= -f2)
ACME_EXIT=0
docker exec sonic-acme-1 /app/acme.sh \
--home /etc/acme.sh \
--issue -d "$DOMAIN" \
--webroot /usr/share/nginx/html \
--server letsencrypt \
--accountemail support+acme@asycn.io || ACME_EXIT=$?
if [ "$ACME_EXIT" -ne 0 ] && [ "$ACME_EXIT" -ne 2 ]; then
echo "ERREUR: acme.sh a echoue (exit $ACME_EXIT)"
exit 1
fi
docker exec sonic-acme-1 cp /etc/acme.sh/$DOMAIN/fullchain.cer /host/certs/$DOMAIN-cert.pem
docker exec sonic-acme-1 cp /etc/acme.sh/$DOMAIN/$DOMAIN.key /host/certs/$DOMAIN-key.pem
echo "TLS OK (acme exit $ACME_EXIT)"
# Images construites localement dans la pipeline : pas de docker compose pull
- cd /opt/sejeteralo && docker compose up -d --remove-orphans
- cd /opt/sejeteralo && docker compose ps
- name: seed
image: docker:27-cli
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /opt/sejeteralo:/opt/sejeteralo
commands:
- |
PROJECT=$(grep '^COMPOSE_PROJECT_NAME=' /opt/sejeteralo/.env | cut -d= -f2)
BACKEND="$PROJECT-backend"
echo "Seed sur $BACKEND..."
docker exec "$BACKEND" python seed.py
# NOTE: volumes + pas de from_secret : compatible
- name: test-deploy
image: docker:27-cli
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /opt/sejeteralo:/opt/sejeteralo
commands:
- |
PROJECT=$(grep '^COMPOSE_PROJECT_NAME=' /opt/sejeteralo/.env | cut -d= -f2)
for SVC in backend frontend; do
STATUS=$(docker inspect --format '{{.State.Status}}' "$PROJECT-$SVC" 2>/dev/null || echo "absent")
echo "$PROJECT-$SVC : $STATUS"
[ "$STATUS" = "running" ] || { echo "FAIL: $PROJECT-$SVC non running"; exit 1; }
done
echo "PASS: tous les containers running"
- name: healthcheck
image: alpine:3.20
commands:
- apk add --no-cache --quiet curl
- |
SITE=$(grep '^APP_DOMAIN=' .env.deploy | cut -d= -f2)
TARGET="https://$SITE"
echo "Healthcheck $TARGET..."
MAX=60
i=0
until [ $i -ge $MAX ]; do
CODE=$(curl -sSo /dev/null -w "%{http_code}" "$TARGET" 2>/dev/null)
echo "Tentative $((i+1))/$MAX - HTTP $CODE"
if [ "$CODE" = "200" ] || [ "$CODE" = "301" ] || [ "$CODE" = "302" ]; then
echo "PASS: app repond sur $TARGET"
exit 0
fi
i=$((i+1))
sleep 10
done
echo "FAIL: app ne repond pas apres 10 minutes"
exit 1
- name: notify-failure
image: alpine:3.20
commands:
- 'echo "ECHEC pipeline #$CI_BUILD_NUMBER sur $CI_COMMIT_BRANCH ($CI_COMMIT_SHA)"'
when: when:
- status: failure - branch: main
event: push

174
CLAUDE.md
View File

@@ -1,100 +1,104 @@
# SejeteralO # CLAUDE.md
Plateforme de tarification participative de l'eau pour communes françaises. This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
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.
## Protocole de début de session ## Project Overview
1. `git pull --rebase origin main` 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.
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
## Stack ## Development Commands
- **Frontend** : Nuxt 4 (Vue 3, TypeScript) + Pinia ; package manager : npm ; port dev : 3009
- **Backend** : Python FastAPI + SQLAlchemy 2.0 async + SQLite (aiosqlite) ; port dev : 8000
- Déploiement : Docker multi-stage + Traefik (backend + frontend) ; CI Woodpecker
- Pas d'UnoCSS — CSS vanilla avec variables CSS palettes dans `main.css`
## Structure
```
frontend/
app/
components/ # composants Vue (dont DisplaySettings.vue — 6 palettes)
layouts/ # layouts Nuxt
pages/
commune/[slug]/index.vue # page principale citoyenne (~1900 lignes)
composables/
useApi.ts # wraps fetch avec Bearer token ; usage : api.get<Type>('/path')
middleware/ # route middleware (auth)
plugins/
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
```
## Données runtime (CRITIQUE)
- `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`
## Commandes
```bash ```bash
# Backend # Install
cd backend && . venv/bin/activate cd backend && python3 -m venv venv && . venv/bin/activate && pip install -r requirements.txt
uvicorn app.main:app --reload --port 8000 --host 0.0.0.0 cd frontend && npm install
python -m pytest tests/ -v
python seed.py # Saoû, 363 foyers, admin accounts
# Frontend # Dev servers (run separately)
cd frontend && npm run dev # :3009 cd backend && . venv/bin/activate && uvicorn app.main:app --reload --port 8000
npm run build 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 # Docker
make docker-up # production make docker-up # production (docker/docker-compose.yml)
make docker-dev # dev avec hot-reload make docker-dev # dev with hot-reload (+ docker-compose.dev.yml)
``` ```
## Conventions / pièges ## Architecture
- **UI français, code anglais** — "foyer" = household (facturation), "électeur" = voter (vote) ### Backend (FastAPI + SQLAlchemy async + SQLite)
- **Modèle Bézier deux niveaux** — 6 paramètres citoyens (vinf, a, b, c, d, e) + p0 auto-calculé :
**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 Σβ₂) / Σ(α₁ + α₂) p0 = (Recettes Σabo Σβ₂) / Σ(α₁ + α₂)
``` ```
Implémenté deux fois : backend Python (`engine/pricing.py`) et frontend TS (`utils/bezier-math.ts`) — garder synchronisés
- **Agrégation votes** : médiane élément par élément des votes actifs (pas moyenne) Tier 1 (0→vinf): population pricing. Tier 2 (vinf→vmax): exceptional consumption pricing.
- **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
- **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>` **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.
- **Dev hints** : classe `.dev-hint` + `v-if="isDev"` ; les codes auth doivent exister dans la DB seedée
- **Port backend** : 8000 en local (nuxt.config + uvicorn) — la table globale CLAUDE.md indique 8009 par erreur ; les fichiers de config font foi ## Vote Flow
- **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.

Binary file not shown.

View File

@@ -1,4 +1,4 @@
.PHONY: install dev dev-backend dev-frontend test seed docker-up docker-down docker-dev .PHONY: install dev dev-backend dev-frontend test seed docker-up docker-down docker-dev docker-fabio docker-fabio-down consul-services fabio-routes
# ── Development (local) ── # ── Development (local) ──
@@ -32,3 +32,17 @@ docker-down:
docker-dev: docker-dev:
docker compose -f docker/docker-compose.yml -f docker/docker-compose.dev.yml up --build docker compose -f docker/docker-compose.yml -f docker/docker-compose.dev.yml up --build
# ── Docker (Fabio/Consul) ──
docker-fabio:
docker compose -f docker/docker-compose.yml -f docker/docker-compose.fabio.yml up --build -d
docker-fabio-down:
docker compose -f docker/docker-compose.yml -f docker/docker-compose.fabio.yml down
consul-services:
@curl -s http://localhost:8500/v1/catalog/services | python3 -m json.tool
fabio-routes:
@curl -s http://localhost:9998/routes

View File

@@ -19,7 +19,6 @@ app = FastAPI(
description="Outil de démocratie participative pour la tarification de l'eau", description="Outil de démocratie participative pour la tarification de l'eau",
version="0.1.0", version="0.1.0",
lifespan=lifespan, lifespan=lifespan,
redirect_slashes=False,
) )
app.add_middleware( app.add_middleware(

View File

@@ -17,31 +17,20 @@ from app.services.auth_service import hash_password
from app.services.import_service import generate_auth_code from app.services.import_service import generate_auth_code
from app.engine.pricing import HouseholdData, compute_p0 from app.engine.pricing import HouseholdData, compute_p0
_XLS_CANDIDATES = [ XLS_PATH = os.path.join(os.path.dirname(__file__), "..", "Eau2018.xls")
"/opt/Eau2018.xls", # image Docker (hors volume monté sur /app)
os.path.join(os.path.dirname(__file__), "Eau2018.xls"),
os.path.join(os.path.dirname(__file__), "..", "Eau2018.xls"), # dev local depuis backend/
]
XLS_PATH = next((p for p in _XLS_CANDIDATES if os.path.exists(p)), _XLS_CANDIDATES[-1])
# Codes fixes — identiques dans le dev hint frontend
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"},
]
async def seed(): async def seed():
await init_db() await init_db()
async with async_session() as db: async with async_session() as db:
# ── Commune Saoû (idempotent) ────────────────────────────────────────── # Check if already seeded
result = await db.execute(select(Commune).where(Commune.slug == "saou")) result = await db.execute(select(Commune).where(Commune.slug == "saou"))
commune = result.scalar_one_or_none() if result.scalar_one_or_none():
print("Saoû already seeded.")
return
if commune is None: # Create commune
commune = Commune( commune = Commune(
name="Saoû", name="Saoû",
slug="saou", slug="saou",
@@ -88,7 +77,7 @@ async def seed():
sheet = book.sheet_by_name("CALCULS") sheet = book.sheet_by_name("CALCULS")
nb_hab = 363 nb_hab = 363
existing_codes = {f["auth_code"] for f in DEV_FIXTURES} 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)
@@ -100,18 +89,20 @@ async def seed():
code = generate_auth_code() code = generate_auth_code()
existing_codes.add(code) existing_codes.add(code)
db.add(Household( household = Household(
commune_id=commune.id, commune_id=commune.id,
identifier=str(name).strip(), identifier=str(name).strip(),
status=str(status).strip().upper(), status=str(status).strip().upper(),
volume_m3=float(volume), volume_m3=float(volume),
price_paid_eur=float(price) if price else 0.0, price_paid_eur=float(price) if price else 0.0,
auth_code=code, auth_code=code,
)) )
db.add(household)
await db.flush() await db.flush()
# ── Publish a reference curve ── # ── Publish a reference curve ──
# Reference: vinf=400, all params=0.5
ref_vinf, ref_a, ref_b, ref_c, ref_d, ref_e = 400, 0.5, 0.5, 0.5, 0.5, 0.5 ref_vinf, ref_a, ref_b, ref_c, ref_d, ref_e = 400, 0.5, 0.5, 0.5, 0.5, 0.5
hh_result = await db.execute( hh_result = await db.execute(
@@ -159,6 +150,7 @@ async def seed():
used_households = set() used_households = set()
vote_count = 0 vote_count = 0
for prof in vote_profiles: for prof in vote_profiles:
# Pick a unique household
hh_pick = random.choice(all_households) hh_pick = random.choice(all_households)
while hh_pick.id in used_households: while hh_pick.id in used_households:
hh_pick = random.choice(all_households) hh_pick = random.choice(all_households)
@@ -170,13 +162,14 @@ async def seed():
vinf=prof["vinf"], vmax=params.vmax, pmax=params.pmax, vinf=prof["vinf"], vmax=params.vmax, pmax=params.pmax,
a=prof["a"], b=prof["b"], c=prof["c"], d=prof["d"], e=prof["e"], a=prof["a"], b=prof["b"], c=prof["c"], d=prof["d"], e=prof["e"],
) )
db.add(Vote( vote = Vote(
commune_id=commune.id, commune_id=commune.id,
household_id=hh_pick.id, household_id=hh_pick.id,
vinf=prof["vinf"], vinf=prof["vinf"],
a=prof["a"], b=prof["b"], c=prof["c"], d=prof["d"], e=prof["e"], a=prof["a"], b=prof["b"], c=prof["c"], d=prof["d"], e=prof["e"],
computed_p0=vp0, computed_p0=vp0,
)) )
db.add(vote)
hh_pick.has_voted = True hh_pick.has_voted = True
vote_count += 1 vote_count += 1
@@ -185,21 +178,6 @@ async def seed():
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")
else:
print("Saoû already seeded.")
# ── Dev fixtures (idempotent — insérés à chaque run si absents) ────────
fixture_added = 0
for fixture in DEV_FIXTURES:
res = await db.execute(
select(Household).where(Household.auth_code == fixture["auth_code"])
)
if res.scalar_one_or_none() is None:
db.add(Household(commune_id=commune.id, **fixture))
fixture_added += 1
if fixture_added:
await db.commit()
print(f" Dev fixtures: {fixture_added} ajoutés — DEVTEST2 (RS 60m³) · DEVTEST3 (RP 120m³) · DEVTEST4 (PRO 350m³)")
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -11,21 +11,22 @@ COPY backend/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
COPY backend/ . COPY backend/ .
COPY Eau2018.xls /opt/Eau2018.xls
# Production # Production
FROM base AS production FROM base AS production
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
RUN apt-get update && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
COPY --from=build /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages COPY --from=build /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=build /usr/local/bin/uvicorn /usr/local/bin/uvicorn COPY --from=build /usr/local/bin/uvicorn /usr/local/bin/uvicorn
COPY --from=build /usr/local/bin/alembic /usr/local/bin/alembic COPY --from=build /usr/local/bin/alembic /usr/local/bin/alembic
COPY --from=build /app /app COPY --from=build /app /app
COPY --from=build /opt/Eau2018.xls /opt/Eau2018.xls
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/docs')" || exit 1 CMD curl -f http://localhost:8000/api/health || exit 1
EXPOSE 8000 EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

View File

@@ -10,8 +10,7 @@ services:
ports: !override ports: !override
- "8000:8000" - "8000:8000"
volumes: volumes:
- ../backend:/app - backend-sources:/app
labels: []
frontend: frontend:
build: build:
@@ -22,5 +21,18 @@ services:
- "3000:3000" - "3000:3000"
- "24678:24678" - "24678:24678"
volumes: volumes:
- ../frontend:/app - frontend-sources:/app
labels: []
volumes:
backend-sources:
driver: local
driver_opts:
type: none
o: bind
device: ../backend
frontend-sources:
driver: local
driver_opts:
type: none
o: bind
device: ../frontend

View File

@@ -0,0 +1,14 @@
# Overlay Fabio — combine avec : docker compose -f docker-compose.yml -f docker-compose.fabio.yml
services:
backend:
labels:
- SERVICE_8000_CHECK_HTTP=${SERVICE_8000_CHECK_HTTP:-/api/health}
- SERVICE_8000_NAME=${SERVICE_8000_NAME:-${COMPOSE_PROJECT_NAME:-sejeteralo}-backend-8000}
- SERVICE_8000_TAGS=${SERVICE_8000_TAGS:-urlprefix-sejeteralo.org:443/api}
frontend:
labels:
- SERVICE_3000_CHECK_HTTP=${SERVICE_3000_CHECK_HTTP:-/}
- SERVICE_3000_NAME=${SERVICE_3000_NAME:-${COMPOSE_PROJECT_NAME:-sejeteralo}-frontend-3000}
- SERVICE_3000_TAGS=${SERVICE_3000_TAGS:-urlprefix-sejeteralo.org:443/*}

View File

@@ -1,51 +1,47 @@
name: ${COMPOSE_PROJECT_NAME:-syoul-sejeteralo-main} name: sejeteralo
services: services:
backend: backend:
build: build:
context: .. context: ../
dockerfile: docker/backend.Dockerfile dockerfile: docker/backend.Dockerfile
target: production target: production
image: sejeteralo-backend:latest
container_name: ${COMPOSE_PROJECT_NAME:-syoul-sejeteralo-main}-backend
restart: always
environment: environment:
DATABASE_URL: sqlite+aiosqlite:///./sejeteralo.db DATABASE_URL: sqlite+aiosqlite:///./sejeteralo.db
SECRET_KEY: ${SECRET_KEY} SECRET_KEY: ${SECRET_KEY}
DEBUG: "false" DEBUG: "false"
CORS_ORIGINS: '["https://${APP_DOMAIN:-sejeteralo.fr}"]' CORS_ORIGINS: '["https://${DOMAIN:-sejeteralo.org}"]'
ports:
- 8000
volumes: volumes:
- backend-data:/app - backend-data:/app
restart: always
labels: labels:
- SERVICE_8000_NAME=${COMPOSE_PROJECT_NAME:-syoul-sejeteralo-main}-backend-8000 - "traefik.enable=true"
- SERVICE_8000_TAGS=urlprefix-${APP_DOMAIN:-sejeteralo.fr}/api/* - "traefik.http.routers.sejeteralo-api.rule=Host(`${DOMAIN:-sejeteralo.org}`) && PathPrefix(`/api`)"
- SERVICE_8000_CHECK_TCP=true - "traefik.http.routers.sejeteralo-api.entrypoints=websecure"
networks: - "traefik.http.routers.sejeteralo-api.tls.certresolver=letsencrypt"
- sonic - "traefik.http.services.sejeteralo-api.loadbalancer.server.port=8000"
frontend: frontend:
build: build:
context: .. context: ../
dockerfile: docker/frontend.Dockerfile dockerfile: docker/frontend.Dockerfile
target: production target: production
image: sejeteralo-frontend:latest
container_name: ${COMPOSE_PROJECT_NAME:-syoul-sejeteralo-main}-frontend
restart: always
environment: environment:
NODE_ENV: production NODE_ENV: production
NUXT_PUBLIC_API_BASE: https://${APP_DOMAIN:-sejeteralo.fr}/api/v1 NUXT_PUBLIC_API_BASE: http://backend:8000/api/v1
ports:
- 3000
depends_on: depends_on:
- backend - backend
restart: always
labels: labels:
- SERVICE_3000_NAME=${COMPOSE_PROJECT_NAME:-syoul-sejeteralo-main}-frontend-3000 - "traefik.enable=true"
- SERVICE_3000_TAGS=urlprefix-${APP_DOMAIN:-sejeteralo.fr}/* - "traefik.http.routers.sejeteralo.rule=Host(`${DOMAIN:-sejeteralo.org}`)"
- SERVICE_3000_CHECK_TCP=true - "traefik.http.routers.sejeteralo.entrypoints=websecure"
networks: - "traefik.http.routers.sejeteralo.tls.certresolver=letsencrypt"
- sonic - "traefik.http.services.sejeteralo.loadbalancer.server.port=3000"
volumes: volumes:
backend-data: backend-data:
networks:
sonic:
external: true

View File

@@ -10,7 +10,7 @@ WORKDIR /src
FROM base AS build FROM base AS build
COPY frontend/package.json frontend/package-lock.json ./ COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci --legacy-peer-deps RUN npm ci
COPY frontend/ . COPY frontend/ .
RUN npm run build RUN npm run build
@@ -21,6 +21,9 @@ FROM base AS production
ENV PORT=3000 ENV PORT=3000
ENV NODE_ENV=production ENV NODE_ENV=production
RUN apt-get update && apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
COPY --from=build /src/.output /src/.output COPY --from=build /src/.output /src/.output
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \

View File

@@ -1,195 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>SejeteralO — Modèle mathématique</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/contrib/auto-render.min.js"
onload="renderMathInElement(document.body, {delimiters:[
{left:'$$',right:'$$',display:true},
{left:'$',right:'$',display:false}
]})"></script>
<style>
body { font-family: 'Georgia', serif; max-width: 860px; margin: 3rem auto; padding: 0 2rem; color: #1a1a2e; background: #fafaf8; line-height: 1.7; }
h1 { font-size: 1.6rem; border-bottom: 2px solid #3a5a8a; padding-bottom: .5rem; color: #1a2e5a; }
h2 { font-size: 1.2rem; color: #2a4a7a; margin-top: 2.5rem; border-left: 4px solid #7aacdc; padding-left: .8rem; }
h3 { font-size: 1rem; color: #3a5a8a; margin-top: 1.8rem; }
.block { background: #f0f4fb; border-radius: 10px; padding: 1.2rem 1.5rem; margin: 1rem 0; }
.cp-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .3rem 2rem; font-size: .92rem; }
table { border-collapse: collapse; width: 100%; font-size: .92rem; margin: 1rem 0; }
th { background: #3a5a8a; color: #fff; padding: .4rem .8rem; text-align: left; }
td { padding: .35rem .8rem; border-bottom: 1px solid #dde; }
tr:nth-child(even) td { background: #f4f7fc; }
.note { font-size: .88rem; color: #556; font-style: italic; margin-top: .5rem; }
code { background: #e8ecf5; padding: .1rem .35rem; border-radius: 4px; font-size: .88rem; font-family: monospace; }
.sep { border: none; border-top: 1px dashed #ccd; margin: 2.5rem 0; }
</style>
</head>
<body>
<h1>SejeteralO — Modèle tarifaire Bézier</h1>
<p>Tarification participative de l'eau : les citoyens façonnent une courbe prix/volume via des points de contrôle Bézier ; le prix d'inflexion $p_0$ est calculé automatiquement pour équilibrer les recettes de la commune.</p>
<h2>1. Paramètres</h2>
<h3>Paramètres structurels (fixés par la commune)</h3>
<table>
<tr><th>Paramètre</th><th>Signification</th></tr>
<tr><td>$v_\text{inf}$</td><td>Volume d'inflexion — frontière entre les deux paliers (m³/an)</td></tr>
<tr><td>$v_\text{max}$</td><td>Volume maximum de la courbe (m³/an)</td></tr>
<tr><td>$p_\text{max}$</td><td>Prix au m³ en $v_\text{max}$ (€)</td></tr>
<tr><td>$R$</td><td>Recettes cibles totales (€/an)</td></tr>
<tr><td>$\text{abo}_P, \text{abo}_S$</td><td>Abonnements résidence principale / secondaire (€/an)</td></tr>
</table>
<h3>Paramètres de forme (votés par les citoyens, 6 valeurs dans $[0,1]$)</h3>
<table>
<tr><th>Param.</th><th>Rôle géométrique</th></tr>
<tr><td>$a$</td><td>Hauteur du 2ᵉ point de contrôle du palier 1 (courbure initiale)</td></tr>
<tr><td>$b$</td><td>Position horizontale du 3ᵉ point de contrôle du palier 1</td></tr>
<tr><td>$c$</td><td>Position horizontale du 1ᵉʳ point de contrôle du palier 2</td></tr>
<tr><td>$d$</td><td>Position horizontale du 2ᵉ point de contrôle du palier 2</td></tr>
<tr><td>$e$</td><td>Hauteur du 2ᵉ point de contrôle du palier 2</td></tr>
</table>
<h3>Variable calculée</h3>
<p>$p_0$ — prix au m³ à l'inflexion — est toujours déduit de l'équilibre de recettes (§4).</p>
<hr class="sep">
<h2>2. Courbe Bézier cubique par paliers</h2>
<p>La courbe prix/volume est définie de façon paramétrique : pour $t \in [0,1]$, on obtient un point $(v(t),\, p(t))$.</p>
<h3>Palier 1 — $v \in [0,\, v_\text{inf}]$</h3>
<div class="block">
$$v_1(t) = v_\text{inf}\,\bigl[(1-3b)\,t^3 + 3b\,t^2\bigr]$$
$$p_1(t) = p_0\,\bigl[(3a-2)\,t^3 + (3-6a)\,t^2 + 3a\,t\bigr]$$
</div>
<p>Points de contrôle :</p>
<div class="cp-grid">
<span>$P_1 = (0,\; 0)$</span>
<span>$P_2 = (0,\; a\,p_0)$</span>
<span>$P_3 = (b\,v_\text{inf},\; p_0)$</span>
<span>$P_4 = (v_\text{inf},\; p_0)$</span>
</div>
<h3>Palier 2 — $v \in [v_\text{inf},\, v_\text{max}]$</h3>
<p>Posons $w_\text{max} = v_\text{max} - v_\text{inf}$.</p>
<div class="block">
$$v_2(t) = v_\text{inf} + w_\text{max}\,\Bigl[\bigl(3(c+d-cd)-2\bigr)\,t^3 + 3(1-2c-d+cd)\,t^2 + 3c\,t\Bigr]$$
$$p_2(t) = p_0 + (p_\text{max}-p_0)\,\bigl[(1-3e)\,t^3 + 3e\,t^2\bigr]$$
</div>
<p>Points de contrôle (partagent $P_4$ avec le palier 1) :</p>
<div class="cp-grid">
<span>$P_4 = (v_\text{inf},\; p_0)$</span>
<span>$P_5 = (v_\text{inf}+c\,w_\text{max},\; p_0)$</span>
<span>$P_6 = \bigl(v_\text{inf}+w_\text{max}(1-d+cd),\; p_0+e(p_\text{max}-p_0)\bigr)$</span>
<span>$P_7 = (v_\text{max},\; p_\text{max})$</span>
</div>
<hr class="sep">
<h2>3. Facture d'un foyer</h2>
<p>La facture annuelle d'un foyer de consommation $v$ est :</p>
<div class="block">
$$\text{Bill}(v) = \text{abo} + \int_0^v p(u)\,\mathrm{d}u$$
</div>
<p>L'intégrale est calculée analytiquement par décomposition :</p>
<div class="block">
$$\int_0^v p(u)\,\mathrm{d}u = (\alpha_1 + \alpha_2)\,p_0 + \beta_2$$
</div>
<p>où les trois coefficients dépendent uniquement de la forme de la courbe (paramètres $a$$e$, $v_\text{inf}$, $v_\text{max}$, $p_\text{max}$) et de la consommation $v$, mais <em>pas</em> de $p_0$. Cela rend le calcul de $p_0$ linéaire.</p>
<h3>Résolution cubique — inversion de la courbe</h3>
<p>Pour évaluer les coefficients en un volume $v$ donné, on cherche $T$ tel que $v_i(T) = v$.</p>
<p>Palier 1 ($v \leq v_\text{inf}$) — résoudre en $T_1 \in [0,1]$ :</p>
<div class="block">
$$(1-3b)\,T_1^3 + 3b\,T_1^2 - \frac{v}{v_\text{inf}} = 0$$
</div>
<p>Palier 2 ($v > v_\text{inf}$) — posons $w = v - v_\text{inf}$, résoudre en $T_2 \in [0,1]$ :</p>
<div class="block">
$$\bigl(3(c+d-cd)-2\bigr)\,T_2^3 + 3(1-2c-d+cd)\,T_2^2 + 3c\,T_2 - \frac{w}{w_\text{max}} = 0$$
</div>
<p class="note">Ces cubiques sont résolues par la formule de Cardano avec affinement Newton-Raphson.</p>
<h3>Coefficient $\alpha_1$ — intégrale du palier 1</h3>
<div class="block">
$$\alpha_1(T_1) = 3\,v_\text{inf}\left[
\frac{-9ab+3a+6b-2}{6}\,T_1^6
+\frac{24ab-6a-13b+3}{5}\,T_1^5
+\frac{3(-7ab+a+2b)}{4}\,T_1^4
+2ab\,T_1^3
\right]$$
</div>
<p class="note">Si $v \leq v_\text{inf}$ : on utilise le $T_1$ résolu ci-dessus. Si $v > v_\text{inf}$ : $\alpha_1 = \alpha_1(1)$ (palier 1 entier).</p>
<h3>Coefficients $\alpha_2$ et $\beta_2$ — intégrale du palier 2</h3>
<p>Posons l'auxiliaire :</p>
<div class="block">
$$u(T_2) =
\frac{-3cd+9ecd+3c-9ec+3d-9ed+6e-2}{6}\,T_2^6 \\[4pt]
+\frac{2cd-15ecd-4c+21ec-2d+15ed-12e+2}{5}\,T_2^5 \\[4pt]
+\frac{6ecd+c-15ec-6ed+6e}{4}\,T_2^4
+ec\,T_2^3$$
</div>
<p>Alors :</p>
<div class="block">
$$\alpha_2 = (v - v_\text{inf}) - 3\,u(T_2)\,w_\text{max}$$
$$\beta_2 = 3\,p_\text{max}\,w_\text{max}\,u(T_2)$$
</div>
<p class="note">Si $v \leq v_\text{inf}$ : $\alpha_2 = \beta_2 = 0$.</p>
<hr class="sep">
<h2>4. Équilibre de recettes — calcul de $p_0$</h2>
<p>En substituant la décomposition $(\alpha_1+\alpha_2)\,p_0+\beta_2$ dans la somme des factures :</p>
<div class="block">
$$R = \sum_{i} \text{abo}_i + \sum_{i}\bigl[(\alpha_{1,i}+\alpha_{2,i})\,p_0 + \beta_{2,i}\bigr]$$
</div>
<p>Comme les $\alpha$ et $\beta$ ne dépendent pas de $p_0$, on obtient directement :</p>
<div class="block">
$$\boxed{p_0 = \frac{R - \displaystyle\sum_i \text{abo}_i - \displaystyle\sum_i \beta_{2,i}}{\displaystyle\sum_i (\alpha_{1,i} + \alpha_{2,i})}}$$
</div>
<p class="note">$p_0$ est calculé une fois par évaluation de la courbe. Si $\sum\alpha = 0$ (tous les foyers consomment 0), on pose $p_0 = 0$.</p>
<hr class="sep">
<h2>5. Résumé du pipeline de calcul</h2>
<ol>
<li>Les citoyens placent des points de contrôle → paramètres $(a,b,c,d,e,v_\text{inf})$.</li>
<li>Pour chaque foyer $i$, calculer $\alpha_{1,i}$, $\alpha_{2,i}$, $\beta_{2,i}$ par inversion cubique.</li>
<li>Calculer $p_0$ par la formule d'équilibre.</li>
<li>Facture de chaque foyer : $\text{Bill}_i = \text{abo}_i + (\alpha_{1,i}+\alpha_{2,i})\,p_0 + \beta_{2,i}$.</li>
<li>Vérification : $\sum_i \text{Bill}_i = R$ (à précision numérique).</li>
</ol>
<p class="note">Sources : <code>backend/app/engine/pricing.py</code> · <code>backend/app/engine/integrals.py</code> · <code>frontend/app/utils/bezier-math.ts</code></p>
</body>
</html>

View File

@@ -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> DEVTEST2 (RS 60) · DEVTEST3 (RP 120) · DEVTEST4 (PRO 350) <strong>Dev:</strong> QPF5L9ZK (60)
</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;">

View File

@@ -1,21 +1,17 @@
{ {
"name": "sejeteralo-frontend", "name": "frontend",
"version": "0.1.0", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "sejeteralo-frontend", "name": "frontend",
"version": "0.1.0", "version": "1.0.0",
"license": "ISC",
"dependencies": { "dependencies": {
"nuxt": "^4.3.1", "nuxt": "^4.3.1",
"vue": "^3.5.28", "vue": "^3.5.28",
"vue-router": "^5.0.3" "vue-router": "^5.0.3"
},
"devDependencies": {
"@pinia/nuxt": "^0.9.0",
"pinia": "^3.0.2",
"typescript": "^5.8.2"
} }
}, },
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
@@ -2809,55 +2805,6 @@
"url": "https://opencollective.com/parcel" "url": "https://opencollective.com/parcel"
} }
}, },
"node_modules/@pinia/nuxt": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@pinia/nuxt/-/nuxt-0.9.0.tgz",
"integrity": "sha512-2yeRo7LeyCF68AbNeL3xu2h6uw0617RkcsYxmA8DJM0R0PMdz5wQHnc44KeENQxR/Mrq8T910XVT6buosqsjBQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nuxt/kit": "^3.9.0"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"pinia": "^2.3.0"
}
},
"node_modules/@pinia/nuxt/node_modules/@nuxt/kit": {
"version": "3.21.2",
"resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-3.21.2.tgz",
"integrity": "sha512-Bd6m6mrDrqpBEbX+g0rc66/ALd1sxlgdx5nfK9MAYO0yKLTOSK7McSYz1KcOYn3LQFCXOWfvXwaqih/b+REI1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"c12": "^3.3.3",
"consola": "^3.4.2",
"defu": "^6.1.4",
"destr": "^2.0.5",
"errx": "^0.1.0",
"exsolve": "^1.0.8",
"ignore": "^7.0.5",
"jiti": "^2.6.1",
"klona": "^2.0.6",
"knitwork": "^1.3.0",
"mlly": "^1.8.1",
"ohash": "^2.0.11",
"pathe": "^2.0.3",
"pkg-types": "^2.3.0",
"rc9": "^3.0.0",
"scule": "^1.3.0",
"semver": "^7.7.4",
"tinyglobby": "^0.2.15",
"ufo": "^1.6.3",
"unctx": "^2.5.0",
"untyped": "^2.0.0"
},
"engines": {
"node": ">=18.12.0"
}
},
"node_modules/@pkgjs/parseargs": { "node_modules/@pkgjs/parseargs": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -6515,15 +6462,15 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/mlly": { "node_modules/mlly": {
"version": "1.8.2", "version": "1.8.0",
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz",
"integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"acorn": "^8.16.0", "acorn": "^8.15.0",
"pathe": "^2.0.3", "pathe": "^2.0.3",
"pkg-types": "^1.3.1", "pkg-types": "^1.3.1",
"ufo": "^1.6.3" "ufo": "^1.6.1"
} }
}, },
"node_modules/mlly/node_modules/confbox": { "node_modules/mlly/node_modules/confbox": {
@@ -7341,81 +7288,6 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/pinia": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^7.7.7"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"typescript": ">=4.5.0",
"vue": "^3.5.11"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/pinia/node_modules/@vue/devtools-api": {
"version": "7.7.9",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz",
"integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vue/devtools-kit": "^7.7.9"
}
},
"node_modules/pinia/node_modules/@vue/devtools-kit": {
"version": "7.7.9",
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz",
"integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vue/devtools-shared": "^7.7.9",
"birpc": "^2.3.0",
"hookable": "^5.5.3",
"mitt": "^3.0.1",
"perfect-debounce": "^1.0.0",
"speakingurl": "^14.0.1",
"superjson": "^2.2.2"
}
},
"node_modules/pinia/node_modules/@vue/devtools-shared": {
"version": "7.7.9",
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz",
"integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==",
"dev": true,
"license": "MIT",
"dependencies": {
"rfdc": "^1.4.1"
}
},
"node_modules/pinia/node_modules/birpc": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz",
"integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/pinia/node_modules/perfect-debounce": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
"dev": true,
"license": "MIT"
},
"node_modules/pkg-types": { "node_modules/pkg-types": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
@@ -8963,20 +8835,6 @@
"integrity": "sha512-wTk4DH3cxwk196uGLK/E9pE45aLfeKJacKmcEgEOA/q5dnPGNxXt0cfYdFxb57L+sEpf1oJH4Dnx/pnRcku9jg==", "integrity": "sha512-wTk4DH3cxwk196uGLK/E9pE45aLfeKJacKmcEgEOA/q5dnPGNxXt0cfYdFxb57L+sEpf1oJH4Dnx/pnRcku9jg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/ufo": { "node_modules/ufo": {
"version": "1.6.3", "version": "1.6.3",
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz",