Files
Johan LEROY bc75528616 fix(infra): lève les points de revue du reverse proxy
Compose interpole tout le fichier avant n'importe quelle sous-commande : la garde
`${PUBLIC_HOST:?}` de l'overlay cassait `stack-down` et `stack-logs` autant que le
démarrage. La valeur retombe sur `enervision.local`, et `stack-up` vérifie à la place
que le certificat présent couvre l'hôte demandé, ce qui est la condition réelle à tenir.

La CSP `script-src 'self'` bloquait le gestionnaire `onload` que l'inlining du CSS
critique d'Angular pose sur la feuille de styles : l'application se serait affichée sans
style derrière le proxy. `inlineCritical` passe à faux, le build de production ne produit
plus aucun script en ligne.

La zone de limitation resserrée ne couvre plus que les routes qui vérifient un secret.
Derrière le NAT de l'école, où une seule adresse porte toute la promotion, `/auth/me` et
`/auth/refresh` y auraient produit des 429 en usage normal.

Enfin `certbot/certbot` est épinglé en v5.8.0 pour que Dependabot puisse le suivre, le
proxy attend une API saine plutôt que démarrée, et la redirection vers `$host` est actée
comme risque accepté : figer un nom canonique couperait l'accès par adresse IP, seule
voie ouverte sur la machine cible.
2026-09-21 12:11:32 +02:00

170 lines
7.2 KiB
Makefile

BACKEND := apps/backend
FRONTEND := apps/frontend
ML := ml
AIRFLOW := etl/airflow
COMPOSE_PROD := docker compose -f docker-compose.yml -f docker-compose.prod.yml
# Piège : sans `export`, une valeur passée en ligne de commande n'atteindrait pas docker compose.
# PUBLIC_HOST retombe sur le `.env`, que make ne lit pas, puis sur la valeur de `.env.example`.
PUBLIC_HOST ?= $(shell sed -n 's/^PUBLIC_HOST=//p' .env 2>/dev/null | tail -1)
PUBLIC_HOST := $(or $(strip $(PUBLIC_HOST)),enervision.local)
export PUBLIC_HOST
ifdef ACME_EMAIL
export ACME_EMAIL
endif
.DEFAULT_GOAL := help
.PHONY: help install install-backend install-frontend install-ml install-airflow \
dev dev-backend dev-frontend \
lint format typecheck test test-cov test-integration check \
openapi docker-build db-up db-down db-reset db-logs db-psql migrate bootstrap-admin \
ml-lint ml-typecheck ml-test ml-check ml-train ml-score recommendations \
airflow-lint airflow-test airflow-check airflow-up airflow-down airflow-logs \
tls-selfsigned tls-acme tls-renew stack-up stack-down stack-logs
help: ## Liste les cibles disponibles
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-16s\033[0m %s\n", $$1, $$2}'
install: install-backend install-frontend install-ml install-airflow ## Installe les dépendances backend, frontend, ML et Airflow
install-backend: ## Installe les dépendances du backend
cd $(BACKEND) && uv sync --all-groups
install-frontend: ## Installe les dépendances du frontend
cd $(FRONTEND) && npm ci
install-ml: ## Installe les dépendances du pipeline ML
cd $(ML) && uv sync --all-groups
install-airflow: ## Installe les dépendances de lint/test des DAGs Airflow
cd $(AIRFLOW) && uv sync --all-groups
dev: ## Lance toute la stack (backend + frontend) en rechargement à chaud
@trap 'kill 0' EXIT INT TERM; \
$(MAKE) --no-print-directory dev-backend & \
$(MAKE) --no-print-directory dev-frontend & \
wait
dev-backend: ## Lance l'API seule en rechargement à chaud
@echo "backend -> http://localhost:8000 (docs sur /docs)"
cd $(BACKEND) && uv run uvicorn app.main:create_app --factory --reload --host 0.0.0.0 --port 8000
dev-frontend: ## Lance le frontend seul en rechargement à chaud
@echo "frontend -> http://localhost:4200"
cd $(FRONTEND) && npm start
lint: ## Analyse statique du backend
cd $(BACKEND) && uv run ruff check .
format: ## Formate et corrige le backend
cd $(BACKEND) && uv run ruff format . && uv run ruff check --fix .
typecheck: ## Vérifie le typage du backend
cd $(BACKEND) && uv run mypy app
test: ## Exécute les tests backend ne demandant pas de base
cd $(BACKEND) && uv run pytest --cov-fail-under=85
test-cov: ## Rapports de couverture HTML et XML, plus les résultats au format JUnit
cd $(BACKEND) && uv run pytest --cov-fail-under=85 --cov-report=html \
--cov-report=xml --junitxml=test-results/junit.xml
test-integration: ## Exécute les tests exigeant une base joignable
cd $(BACKEND) && uv run pytest -m integration
check: lint typecheck test ## Chaîne de vérification complète
openapi: ## Régénère apps/backend/openapi.json depuis les routes déclarées
cd $(BACKEND) && uv run python -m app.cli export-openapi
ml-lint: ## Analyse statique du pipeline ML
cd $(ML) && uv run ruff check .
ml-typecheck: ## Vérifie le typage du pipeline ML
cd $(ML) && uv run mypy enervision_ml tests
ml-test: ## Exécute les tests du pipeline ML (donnees synthetiques, sans base ni serveur MLflow)
cd $(ML) && uv run pytest
ml-check: ml-lint ml-typecheck ml-test ## Chaîne de vérification complète du pipeline ML
ml-train: ## Entraine le modele LightGBM. CSV=chemin optionnel, sinon lit ML_DATABASE_URL
cd $(ML) && uv run python -m enervision_ml.train $(if $(CSV),--csv $(CSV),)
ml-score: ## Score le prochain pas horaire et l'ecrit dans `prediction`. CSV=chemin optionnel
cd $(ML) && uv run python -m enervision_ml.score $(if $(CSV),--csv $(CSV),)
recommendations: ## Genere les recommandations depuis les alertes en base. SITE=identifiant optionnel
cd $(BACKEND) && uv run python -m app.cli generate-recommendations $(if $(SITE),--site-id $(SITE),)
airflow-lint: ## Analyse statique des DAGs Airflow
cd $(AIRFLOW) && uv run ruff check .
airflow-test: ## Verifie que les DAGs s'importent sans erreur et ont la structure attendue
cd $(AIRFLOW) && uv run pytest
airflow-check: airflow-lint airflow-test ## Chaîne de vérification complète des DAGs Airflow
airflow-up: ## Démarre Airflow (webserver + scheduler, LocalExecutor). db-up requis avant.
docker compose up -d airflow-init airflow-webserver airflow-scheduler
@echo "airflow -> http://localhost:$${AIRFLOW_PORT:-8080}"
airflow-down: ## Arrête le webserver et le scheduler Airflow
docker compose stop airflow-webserver airflow-scheduler
airflow-logs: ## Suit les journaux du scheduler Airflow (où tournent les tâches, LocalExecutor)
docker compose logs -f airflow-scheduler
docker-build: ## Construit l'image du backend
docker build -t enervision-backend:local $(BACKEND)
tls-selfsigned: ## Génère le certificat de démonstration. PUBLIC_HOST=..., FORCE=1 pour écraser
./scripts/tls-selfsigned.sh $(if $(FORCE),--force,)
stack-up: ## Démarre la stack complète derrière le reverse proxy (80/443). PUBLIC_HOST=... au besoin
@test -f infra/proxy/tls/fullchain.pem \
|| { echo "Aucun certificat dans infra/proxy/tls. Lancer d'abord make tls-selfsigned"; exit 1; }
@openssl x509 -in infra/proxy/tls/fullchain.pem -noout -checkhost "$(PUBLIC_HOST)" >/dev/null \
|| { echo "Le certificat ne couvre pas $(PUBLIC_HOST). Relancer make tls-selfsigned PUBLIC_HOST=$(PUBLIC_HOST) FORCE=1"; exit 1; }
$(COMPOSE_PROD) up -d --build
stack-down: ## Arrête la stack complète en conservant les données
$(COMPOSE_PROD) stop
stack-logs: ## Suit les journaux du reverse proxy
$(COMPOSE_PROD) logs -f proxy
tls-acme: ## Demande un certificat Let's Encrypt. PUBLIC_HOST public et ACME_EMAIL requis
@test "$(PUBLIC_HOST)" != enervision.local \
|| { echo "PUBLIC_HOST doit être un domaine public résolvable, pas le nom de démonstration"; exit 1; }
$(COMPOSE_PROD) --profile acme run --rm certbot certonly --webroot -w /var/www/certbot \
-d $(PUBLIC_HOST) \
--email $${ACME_EMAIL:?ACME_EMAIL=... requis} \
--agree-tos --no-eff-email --deploy-hook /deploy-hook.sh
$(COMPOSE_PROD) exec proxy nginx -s reload
tls-renew: ## Renouvelle les certificats Let's Encrypt et recharge le proxy
$(COMPOSE_PROD) --profile acme run --rm certbot renew --deploy-hook /deploy-hook.sh
$(COMPOSE_PROD) exec proxy nginx -s reload
db-up: ## Démarre la base PostgreSQL TimescaleDB
docker compose up -d db
db-down: ## Arrête la base en conservant ses données
docker compose stop db
db-reset: ## Détruit la base et rejoue db/init
docker compose down -v && docker compose up -d db
db-logs: ## Suit les journaux de la base
docker compose logs -f db
db-psql: ## Ouvre une session psql sur la base applicative
docker compose exec db psql -U $${POSTGRES_USER:-enervision} -d $${POSTGRES_DB:-enervision}
migrate: ## Applique les migrations Alembic
cd $(BACKEND) && uv run alembic upgrade head
bootstrap-admin: ## Crée le premier administrateur, mot de passe saisi au clavier
cd $(BACKEND) && uv run python -m app.cli create-admin --email $${EMAIL:?EMAIL=... requis}