diff --git a/Makefile b/Makefile index 9d4beb4..157bff2 100644 --- a/Makefile +++ b/Makefile @@ -13,11 +13,28 @@ ifdef ACME_EMAIL export ACME_EMAIL endif +# Piege : make ne lit pas `.env`, que seul docker compose interpole. Les cibles hors conteneur +# (ml-*, demo-data, db-wait) joignent la base par le port publie et ont besoin de ces valeurs. +env-val = $(shell sed -n 's/^$(1)=//p' .env 2>/dev/null | tail -1) +PG_USER := $(or $(strip $(call env-val,POSTGRES_USER)),enervision) +PG_PASSWORD := $(or $(strip $(call env-val,POSTGRES_PASSWORD)),change_me) +PG_DB := $(or $(strip $(call env-val,POSTGRES_DB)),enervision) +PG_PORT := $(or $(strip $(call env-val,POSTGRES_PORT)),5433) +AIRFLOW_PORT := $(or $(strip $(call env-val,AIRFLOW_PORT)),8080) +MAILPIT_UI_PORT := $(or $(strip $(call env-val,MAILPIT_UI_PORT)),8025) +ML_DATABASE_URL ?= postgresql+psycopg://$(PG_USER):$(PG_PASSWORD)@localhost:$(PG_PORT)/$(PG_DB) +export ML_DATABASE_URL + +# Le jeu historique s'arrete au 31/12/2024 : score et detection ancres a l'horloge reelle ne +# verraient qu'un parc muet depuis des mois. Cf. `--now` de enervision_ml.score. +DEMO_NOW ?= 2024-12-31T00:00:00Z + .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 \ + openapi docker-build db-up db-down db-reset db-logs db-psql db-wait db-ensure-airflow \ + migrate bootstrap-admin services-up demo-data demo-data-force \ ml-lint ml-typecheck ml-test ml-check ml-train ml-score detect-alerts 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 @@ -39,12 +56,19 @@ install-ml: ## Installe les dépendances du pipeline ML 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 +dev: services-up migrate demo-data ## Lance toute la stack : base, Mailpit, Airflow, puis backend et frontend + @echo "airflow -> http://localhost:$(AIRFLOW_PORT) mailpit -> http://localhost:$(MAILPIT_UI_PORT)" @trap 'kill 0' EXIT INT TERM; \ $(MAKE) --no-print-directory dev-backend & \ $(MAKE) --no-print-directory dev-frontend & \ wait +services-up: ## Démarre les services conteneurisés dont `make dev` dépend (base, Mailpit, Airflow) + docker compose up -d db mailpit + @$(MAKE) --no-print-directory db-wait + @$(MAKE) --no-print-directory db-ensure-airflow + docker compose up -d airflow-init airflow-webserver airflow-scheduler + 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 @@ -91,8 +115,8 @@ ml-check: ml-lint ml-typecheck ml-test ## Chaîne de vérification complète du 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),) +ml-score: ## Score le prochain pas horaire et l'ecrit dans `prediction`. CSV= et NOW= optionnels + cd $(ML) && uv run python -m enervision_ml.score $(if $(CSV),--csv $(CSV),) $(if $(NOW),--now $(NOW),) detect-alerts: ## Détecte les alertes internes depuis les lectures en base. SITE= et NOW= optionnels cd $(BACKEND) && uv run python -m app.detection.internal_alerts $(if $(SITE),--site-id $(SITE),) $(if $(NOW),--now $(NOW),) @@ -108,7 +132,7 @@ airflow-test: ## Verifie que les DAGs s'importent sans erreur et ont la structur 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. +airflow-up: db-ensure-airflow ## 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}" @@ -165,8 +189,36 @@ db-logs: ## Suit les journaux de la base db-psql: ## Ouvre une session psql sur la base applicative docker compose exec db psql -U $${POSTGRES_USER:-enervision} -d $${POSTGRES_DB:-enervision} +db-wait: ## Attend que la base accepte les connexions + @for _ in $$(seq 1 60); do \ + docker compose exec -T db pg_isready -U $(PG_USER) -d $(PG_DB) >/dev/null 2>&1 && exit 0; \ + sleep 1; \ + done; \ + echo "La base n'accepte toujours pas de connexion apres 60s"; exit 1 + +# Piege : db/init ne rejoue qu'a la premiere initialisation du volume. Un `pgdata` cree avant +# db/init/120-airflow-database.sql n'a pas de base `airflow`, et airflow-init boucle dessus. +db-ensure-airflow: ## Crée la base de métadonnées Airflow si le volume pgdata est antérieur à db/init/120 + @docker compose exec -T db psql -U $(PG_USER) -d postgres -tAc \ + "SELECT 1 FROM pg_database WHERE datname = 'airflow'" | grep -q 1 \ + || docker compose exec -T db psql -U $(PG_USER) -d postgres -c "CREATE DATABASE airflow" + 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} + +demo-data: ## Renseigne prédictions, alertes et recommandations si elles manquent. NOW= optionnel + @nombre=$$(docker compose exec -T db psql -U $(PG_USER) -d $(PG_DB) -tAc 'SELECT count(*) FROM alert') \ + || { echo "demo-data : base injoignable ou migrations non appliquees"; exit 1; }; \ + if [ "$$nombre" = 0 ]; then \ + $(MAKE) --no-print-directory demo-data-force; \ + else \ + echo "demo-data : $$nombre alerte(s) deja en base (make demo-data-force pour rejouer)"; \ + fi + +demo-data-force: ## Rejoue le peuplement sans regarder l'existant. Les trois etapes sont idempotentes + $(MAKE) --no-print-directory ml-score NOW=$(DEMO_NOW) + $(MAKE) --no-print-directory detect-alerts NOW=$(DEMO_NOW) + $(MAKE) --no-print-directory recommendations diff --git a/README.md b/README.md index cc139dc..56093e9 100644 --- a/README.md +++ b/README.md @@ -75,25 +75,52 @@ Prerequis : uv, Docker, Node 24 LTS (npm fourni). Le poste doit disposer de Pyth cp .env.example .env # variables de docker-compose cp apps/backend/.env.example apps/backend/.env # variables du backend hors conteneur -make db-up # PostgreSQL + TimescaleDB, publie sur le port 5433 -make install # dependances du backend et du frontend -make migrate # applique les migrations Alembic -make dev # backend sur http://localhost:8000 (docs sur /docs), frontend sur http://localhost:4200 +make install # dependances du backend, du frontend, du ML et des DAGs +make dev # toute la stack, voir ci-dessous make check # lint + typage + tests ``` +`make dev` enchaine tout : demarrage des services conteneurises (base sur le port 5433, Mailpit, +Airflow), migrations Alembic, peuplement de demonstration si les alertes manquent, puis backend +et frontend en rechargement a chaud sur le poste. + +| Service | Adresse | +|---|---| +| Backend | (documentation sur `/docs`) | +| Frontend | | +| Airflow | (`AIRFLOW_ADMIN_USERNAME` / `AIRFLOW_ADMIN_PASSWORD` du `.env`) | +| Mailpit | | + +Le `.env` doit porter les cles Airflow avant le premier `make dev` : `AIRFLOW_FERNET_KEY`, +`AIRFLOW_WEBSERVER_SECRET_KEY`, `AIRFLOW_APP_SECRET_KEY` et `AIRFLOW_ADMIN_PASSWORD`. Sans elles +`airflow-init` refuse de demarrer, et `airflow-webserver` comme `airflow-scheduler` avec lui. + +Les cibles d'origine restent disponibles pour ne demarrer qu'une partie : `make db-up`, +`make airflow-up`, `make dev-backend`, `make dev-frontend`. + `make help` liste les cibles disponibles. Deux fichiers d'environnement, deux usages : `.env` a la racine alimente `docker-compose.yml`, `apps/backend/.env` alimente le backend lance sur le poste. Le port 5433 est publie plutot que 5432, souvent deja pris par une autre base. -La boucle de developpement est `make db-up` puis `make dev` : seule la base tourne en -conteneur, le backend et le frontend tournent tous les deux sur le poste, lances ensemble par -`make dev` (logs entrelaces dans le meme terminal, Ctrl+C arrete les deux). `make dev-backend` -et `make dev-frontend` restent disponibles pour lancer un seul des deux. Le service `backend` -du `docker-compose.yml` sert la stack complete et la recette, et n'embarque pas le source, donc -toute modification y demande un `docker compose up -d --build backend`. +Le backend et le frontend tournent sur le poste, lances ensemble par `make dev` (logs +entrelaces dans le meme terminal, Ctrl+C arrete les deux) ; la base, Mailpit et Airflow tournent +en conteneur. Le service `backend` du `docker-compose.yml` sert la stack complete et la recette, +et n'embarque pas le source, donc toute modification y demande un +`docker compose up -d --build backend`. + +### Donnees de demonstration + +Le jeu historique s'arrete au 31/12/2024. `make demo-data` renseigne les tables que les vues +alertes, recommandations et previsions lisent, en ancrant le scoring et la detection a cette +date (`DEMO_NOW`) plutot qu'a l'horloge reelle, qui ne verrait qu'un parc muet depuis des mois. +La cible ne fait rien si des alertes existent deja ; `make demo-data-force` rejoue les trois +etapes, toutes idempotentes en base. + +Un volume `pgdata` cree avant `db/init/120-airflow-database.sql` n'a pas de base `airflow` : +`db/init` ne rejoue qu'a la premiere initialisation. `make db-ensure-airflow`, appelee par +`make dev` et `make airflow-up`, la cree au besoin, sans detruire les donnees applicatives. Verifier que la base repond et que l'extension est chargee :