Fusionne dev dans test/integration-api-db-ml
Quatre conflits, tous additifs, nés du DAG `mock_api_import` (#146) arrivé sur `dev` pendant que cette branche ajoutait `derive` : la liste des DAGs du README, celle de la vue d'ensemble et du tableau d'infrastructure, et `DAG_IDS`/`TACHES` dans les tests d'intégrité. Les six DAGs sont conservés de part et d'autre. Collision que git ne voyait pas : `dev` a reçu un ADR 0011 et un 0012 (procédure de déploiement, état de la VM ENI) pendant que cette branche en ajoutait un autre sous le même numéro. L'ADR de la surveillance de dérive devient 0013, avec ses onze références, et la table de `docs/README.md` reprend les trois. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
# EnerVision · procédure de déploiement (22/09/2026)
|
||||
|
||||
Terraform provisionne la machine, GitHub Actions déploie (ADR 0010). Deux environnements Compose
|
||||
sur la VM ENI `10.101.200.37` : `rec` sur la branche `dev`, `prod` sur `main` (ADR 0009).
|
||||
|
||||
| | recette | production |
|
||||
|---|---|---|
|
||||
| Branche, environnement GitHub | `dev`, `rec` | `main`, `prod` |
|
||||
| Dossier, projet Compose | `/srv/enervision/rec`, `enervision-rec` | `/srv/enervision/prod`, `enervision-prod` |
|
||||
| URL | `https://rec.enervision.local:8443` | `https://enervision.local` |
|
||||
| Proxy HTTP / HTTPS | `127.0.0.1:8081` / `8443` | `80` / `443` |
|
||||
| Postgres / Mailpit / Airflow (locaux) | `5434` / `8026` / `8082` | `5433` / `8025` / `8080` |
|
||||
|
||||
## 0. Avant toute commande
|
||||
|
||||
1. **Clé SSH déposée** sur la VM : `ssh-copy-id -i ~/.ssh/id_ed25519.pub root@10.101.200.37`.
|
||||
Terraform ne gère **pas** l'authentification par mot de passe (elle finirait dans le state).
|
||||
2. **L'utilisateur propriétaire existe déjà** sur la VM (ex. `enervision`) : il possède
|
||||
`/srv/enervision` et fait tourner le runner. Terraform échoue tôt s'il manque, il ne le crée pas.
|
||||
3. **Jeton d'enregistrement du runner** : Settings > Actions > Runners > New self-hosted runner.
|
||||
Valable 1 h, une seule inscription, créé par un administrateur du dépôt (ineszang).
|
||||
4. **`main` est en retard de 64 commits** et ne porte ni `deploy.yml`, ni `provision-host.sh`, ni
|
||||
le Terraform, ni l'overlay paramétré (ports et `PUBLIC_ORIGIN` en dur). Tant que `dev` n'est pas
|
||||
remonté dans `main`, seule la recette est déployable : le clone `prod` sera préparé mais son
|
||||
`make stack-up` publierait 80/443 sans les variables, et aucun push sur `main` ne déclencherait
|
||||
de déploiement (le workflow n'y existe pas). **Remonter `dev` → `main` avant de toucher à prod.**
|
||||
|
||||
## 1. Provisionner la machine (depuis le poste)
|
||||
|
||||
```bash
|
||||
cd infra/terraform/environments/vm-eni
|
||||
cp terraform.tfvars.example terraform.tfvars
|
||||
terraform init
|
||||
terraform apply
|
||||
```
|
||||
|
||||
`terraform.tfvars`, ignoré par git, trois valeurs à renseigner :
|
||||
|
||||
```hcl
|
||||
proprietaire = "enervision" # doit exister sur la VM
|
||||
runner_version = "2.330.0" # épingler depuis github.com/actions/runner/releases
|
||||
runner_token = "..." # jeton d'1 h, à retirer du fichier après l'apply
|
||||
```
|
||||
|
||||
Défauts utiles : `ssh_host = "10.101.200.37"`, `ssh_user = "root"`,
|
||||
`ssh_private_key_path = "~/.ssh/id_ed25519"`, `racine = "/srv/enervision"`,
|
||||
`runner_labels = "eni-g3"` (ciblé par `deploy.yml`), `runner_dossier = "/opt/actions-runner"`.
|
||||
|
||||
L'apply fait trois choses, dans cet ordre : Docker + plugin Compose et `usermod -aG docker`,
|
||||
puis `scripts/provision-host.sh`, puis l'installation et l'enregistrement du runner en service.
|
||||
Il ne construit aucune image et ne démarre aucun conteneur : un apply n'interrompt pas la stack.
|
||||
|
||||
Rejouable : un clone existant est réaligné, un `.env` présent n'est **jamais** réécrit, un
|
||||
certificat présent n'est jamais régénéré. Un nouvel apply de la ressource runner redemande un
|
||||
jeton frais (il expire en 1 h).
|
||||
|
||||
## 2. Variables d'environnement
|
||||
|
||||
Un `.env` par dossier, en `600`, généré sur la machine depuis `.env.example`. **Aucun secret ne
|
||||
passe par git ni par GitHub** : le runner n'en reçoit aucun (seul `SONAR_TOKEN` existe côté CI).
|
||||
|
||||
**Générés automatiquement** : `POSTGRES_PASSWORD`, `APP_SECRET_KEY`, `AIRFLOW_FERNET_KEY`,
|
||||
`AIRFLOW_API_SECRET_KEY`, `AIRFLOW_JWT_SECRET`, `AIRFLOW_ADMIN_PASSWORD`, `AIRFLOW_APP_SECRET_KEY`.
|
||||
|
||||
**Fixés par environnement** : `COMPOSE_PROJECT_NAME`, `PUBLIC_HOST`, `PUBLIC_ORIGIN`,
|
||||
`PROXY_HTTP_PORT`, `PROXY_HTTPS_PORT`, `POSTGRES_PORT`, `MAILPIT_UI_PORT`, `AIRFLOW_PORT`.
|
||||
|
||||
**À renseigner à la main**, dans chaque `.env`, avant le premier démarrage :
|
||||
|
||||
```
|
||||
APP_MOCK_API_USERNAME=...
|
||||
APP_MOCK_API_PASSWORD=...
|
||||
```
|
||||
|
||||
Garde-fou : le script refuse d'écrire un `.env` s'il reste un `change_me` hors `APP_MOCK_API_*`
|
||||
(cas vécu d'une clé renommée en amont, `AIRFLOW_WEBSERVER_SECRET_KEY` sous Airflow 3).
|
||||
|
||||
`APP_ENV=prod` et `APP_DEBUG=false` sont en dur dans l'overlay, pas dans le `.env` : la valeur
|
||||
`local` du poste reprendrait le dessus et rouvrirait `/docs` sans cookie `__Secure-`.
|
||||
|
||||
`TS_TUNE_MEMORY=2GB` et `TS_TUNE_NUM_CPUS=2` sont obligatoires : deux TimescaleDB sur 8 Go se
|
||||
réserveraient 25 % de la RAM chacune. La montée à 32 Go est à demander.
|
||||
|
||||
Certificats auto-signés générés par le script (`infra/proxy/tls/`), couvrant le nom d'hôte,
|
||||
`localhost` et l'IP. Let's Encrypt (`make tls-acme`, `ACME_EMAIL`) reste hors d'atteinte sans
|
||||
domaine public résolvable.
|
||||
|
||||
## 3. Premier démarrage (manuel, une seule fois, sur la VM)
|
||||
|
||||
```bash
|
||||
cd /srv/enervision/rec && make stack-up # build + up + alembic upgrade head
|
||||
cd /srv/enervision/prod && make stack-up # seulement après la remontée dev → main
|
||||
```
|
||||
|
||||
`stack-up` refuse de démarrer si le certificat manque ou ne couvre pas `PUBLIC_HOST`, et applique
|
||||
les migrations : sans elles la stack démarrerait verte sur une base sans schéma.
|
||||
|
||||
Premier administrateur, stack démarrée, dans chaque dossier :
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml -f docker-compose.prod.yml exec backend \
|
||||
python -m app.cli create-admin --email <adresse>
|
||||
```
|
||||
|
||||
Données historiques : `data/raw` n'est pas dans git. Déposer les fichiers dans chaque dossier
|
||||
avant de déclencher le DAG `historical_import`.
|
||||
|
||||
## 4. Réglages GitHub (administrateur du dépôt)
|
||||
|
||||
- Environnement `prod` : branche `main` seule autorisée, **approbation d'un relecteur** requise.
|
||||
- Environnement `rec` : branche `dev` seule autorisée, sans approbation.
|
||||
- Settings > Actions : **« Require approval for all outside collaborators »**. Un runner
|
||||
auto-hébergé sur un dépôt public exécute ce qu'on lui envoie ; `deploy.yml` ne se déclenche
|
||||
jamais sur `pull_request`, et le runner ne tourne jamais en root.
|
||||
|
||||
## 5. Déploiement continu, ensuite
|
||||
|
||||
Un push sur `dev` déploie la recette, un push sur `main` la production après approbation.
|
||||
Le job (runner `eni-g3`) aligne le clone (`fetch`, `checkout`, `reset --hard`), lance
|
||||
`make stack-up`, puis sonde `/api/v1/health/ready` derrière le proxy pendant 3 minutes ; en cas
|
||||
d'échec il publie `ps` et les 50 dernières lignes de `backend` et `proxy`. Pas de `checkout` dans
|
||||
l'espace du runner : `.env`, certificats et volumes doivent survivre d'un déploiement à l'autre.
|
||||
Concurrence par branche, sans annulation.
|
||||
|
||||
Déclenchement manuel possible : `workflow_dispatch`.
|
||||
|
||||
## 6. Vérifier
|
||||
|
||||
```bash
|
||||
curl -k https://localhost:8443/api/v1/health/ready # recette, sur la VM
|
||||
curl -k https://localhost/api/v1/health/ready # production, sur la VM
|
||||
```
|
||||
|
||||
Depuis un poste, ajouter à `/etc/hosts` :
|
||||
|
||||
```
|
||||
10.101.200.37 enervision.local rec.enervision.local
|
||||
```
|
||||
|
||||
Les deux noms sont obligatoires : le cookie `__Secure-ev_refresh` est posé par hôte et non par
|
||||
port ; un seul nom déconnecterait la production à chaque connexion en recette.
|
||||
|
||||
## Pièges à connaître
|
||||
|
||||
- Compose **2.24.4 minimum** : l'overlay emploie `!override` et `!reset`, sans quoi l'API resterait
|
||||
joignable en clair à côté du proxy. Le script le vérifie.
|
||||
- Le runner doit tourner sous le propriétaire de `/srv/enervision` : sinon git refuse les clones
|
||||
(propriété douteuse) et le `.env` en `600` lui échappe. Correctif :
|
||||
`PROPRIETAIRE=<utilisateur> bash scripts/provision-host.sh`.
|
||||
- Chaque environnement reconstruit ses images à partir du même commit : la production n'exécute
|
||||
pas l'artefact validé en recette, mais un second build. Le passage à GHCR lèvera cette limite.
|
||||
- Un `.env` perdu se régénère, mais invalide les sessions et les connexions chiffrées par Airflow :
|
||||
ils ne sont sauvegardés nulle part ailleurs.
|
||||
- Retirer le runner se fait à la main, depuis les paramètres du dépôt : `terraform destroy` ne le
|
||||
désinscrit pas.
|
||||
|
||||
## Références dans le dépôt
|
||||
|
||||
`docs/adr/0009-deux-environnements-compose-sur-la-vm-eni.md`,
|
||||
`docs/adr/0010-terraform-provisionne-github-actions-deploie.md`,
|
||||
`docs/architecture/50-cicd.md`, `docs/architecture/10-infra.md`, `infra/README.md`,
|
||||
`scripts/provision-host.sh`, `.github/workflows/deploy.yml`, `docker-compose.prod.yml`.
|
||||
@@ -0,0 +1,120 @@
|
||||
# EnerVision · Recette et production sur la VM ENI, aujourd'hui
|
||||
|
||||
État au lundi 21 septembre 2026, 15h. Cible : deux environnements qui tournent sur la VM
|
||||
`eadl-2025-nantes-g3` (`10.101.200.37`) avant vendredi 25/09 9h, déployés automatiquement depuis
|
||||
GitHub. Ce document donne la solution retenue, ce qu'elle change dans le dépôt, et le déroulé de
|
||||
l'après-midi avec qui fait quoi.
|
||||
|
||||
## 1. La décision en une phrase
|
||||
|
||||
**Deux projets Docker Compose sur la même VM, un par environnement, déployés par un runner GitHub
|
||||
Actions installé sur la VM.** `dev` alimente la recette, `main` alimente la production. Terraform
|
||||
reste ce qu'il est : le module k3s, cible à terme, non utilisé pour cette mise en ligne.
|
||||
|
||||
| | Recette (`rec`) | Production (`prod`) |
|
||||
|---|---|---|
|
||||
| Branche | `dev` | `main` |
|
||||
| Environnement GitHub | `rec` (créé ce midi) | `prod` (créé ce midi) |
|
||||
| Dossier sur la VM | `/srv/enervision/rec` | `/srv/enervision/prod` |
|
||||
| Projet Compose | `enervision-rec` | `enervision-prod` |
|
||||
| URL | `https://rec.enervision.local:8443` | `https://enervision.local` |
|
||||
| Proxy HTTPS | `8443` | `443` |
|
||||
| Proxy HTTP (redirection) | `127.0.0.1:8081`, inutilisé | `80` |
|
||||
| PostgreSQL, Mailpit, Airflow | `127.0.0.1` : `5434`, `8026`, `8082` | `127.0.0.1` : `5433`, `8025`, `8080` |
|
||||
| Certificat | auto-signé, SAN `rec.enervision.local` | auto-signé, SAN `enervision.local` |
|
||||
| Déclenchement | chaque push sur `dev` | push sur `main`, après approbation dans GitHub |
|
||||
|
||||
Les deux noms d'hôte pointent sur la même IP. Deux lignes dans le `/etc/hosts` des postes de
|
||||
l'équipe suffisent. Deux noms distincts sont indispensables : le cookie de rafraîchissement
|
||||
`__Secure-ev_refresh` est posé par hôte, pas par port, et un seul nom ferait se déconnecter la
|
||||
prod à chaque connexion sur la recette.
|
||||
|
||||
## 2. Pourquoi c'est la solution la plus simple
|
||||
|
||||
- **Tout existe déjà.** L'overlay `docker-compose.prod.yml`, le proxy Nginx TLS, les scripts de
|
||||
certificat et `make stack-up` sont écrits et validés sur poste (PR #117, ADR 0007). Il ne
|
||||
manque que quatre variables pour que deux instances cohabitent sur une machine.
|
||||
- **Un projet Compose isole tout.** Volumes, réseau, noms de conteneurs sont préfixés par le nom
|
||||
du projet. Casser la recette ne touche pas la prod, ce qui est la raison d'être d'une recette.
|
||||
- **Le runner sur la VM est la seule façon d'atteindre une IP privée d'école depuis GitHub.** Les
|
||||
runners hébergés par GitHub ne voient pas `10.101.200.37`. Le runner se connecte en sortie
|
||||
vers GitHub, aucun port entrant n'est nécessaire. C'était le choix 16 du dossier EC01 : il
|
||||
redevient tenu.
|
||||
- **La promotion existe déjà dans la stratégie de branches** : `dev` puis `main` par PR. Le
|
||||
même code est déployé en recette, puis en production, sans troisième mécanisme.
|
||||
|
||||
Ce qu'on écarte, et pourquoi :
|
||||
|
||||
| Piste | Pourquoi pas cette semaine |
|
||||
|---|---|
|
||||
| k3s avec deux namespaces | Le cluster serait vide : aucun manifeste, aucun registre d'images, aucun stockage persistant. Trois jours de travail sans valeur visible au J10 |
|
||||
| Terraform de `feat/deploy` (nginx système + copie de fichiers) | Revue postée sur l'issue #21 : huit points bloquants, `rec` et `prod` ne passent pas `terraform validate`. On abandonne cette voie |
|
||||
| Azure ENI pour la prod | Deuxième infrastructure à provisionner, choix à justifier devant le jury (document 03), et rien n'est prêt côté Azure |
|
||||
| Images publiées sur GHCR | Meilleure pratique, mais un registre de plus à authentifier sur la VM. Les images se construisent sur la VM, où le runner tourne déjà. À faire ensuite, issue à ouvrir |
|
||||
| Let's Encrypt | Aucun domaine public ne résout vers la VM. Auto-signé assumé, chemin ACME déjà câblé |
|
||||
|
||||
## 3. Ce qui change dans le dépôt (une PR vers `dev`)
|
||||
|
||||
| Fichier | Changement | Raison |
|
||||
|---|---|---|
|
||||
| `apps/frontend/Dockerfile` | `FROM nginx:1.28-alpine` à la place de `dhi.io/nginx:...` | Le registre Docker Hardened Images demande une authentification. L'image frontend n'a jamais été construite, sur aucun poste : c'est le premier point où `make stack-up` échouerait sur la VM |
|
||||
| `docker-compose.prod.yml` | Ports du proxy en variables `PROXY_HTTP_PORT` et `PROXY_HTTPS_PORT`. Origine publique `PUBLIC_ORIGIN` pour CORS et le lien de réinitialisation. `TS_TUNE_MEMORY` sur la base | Deux proxys ne peuvent pas publier 80 et 443. L'origine de la recette porte un port. Deux TimescaleDB sur 8 Go se réserveraient chacune 2 Go sans réglage |
|
||||
| `.env.example` | `COMPOSE_PROJECT_NAME`, les variables ci-dessus, ports de la recette en commentaire | Le `.env` de chaque dossier est la seule différence entre les deux environnements |
|
||||
| `.github/workflows/deploy.yml` | Nouveau. `on: push` sur `dev` et `main`, `runs-on: [self-hosted, eni-g3]`, `environment: rec` ou `prod`, puis `git reset --hard origin/<branche>` et `make stack-up` dans le dossier de l'environnement | Le D de CI/CD, issue #21 |
|
||||
| `scripts/provision-host.sh` | Nouveau. Vérifie Docker et Compose 2.24.4 ou plus, crée `/srv/enervision/{rec,prod}`, clone les deux branches | Rejouable, et réutilisable par Terraform plus tard |
|
||||
| `docs/adr/0009-...md`, `10-infra.md`, `50-cicd.md`, `infra/proxy/README.md` | Décision, vue infra, vue CI/CD, tableau des ports | Règle du dépôt : la vue change dans la même PR que le composant |
|
||||
|
||||
Ce qui ne change pas : `docker-compose.yml`, la configuration Nginx, `infra/terraform`.
|
||||
|
||||
## 4. Déroulé de l'après-midi
|
||||
|
||||
| # | Qui | Quoi | Durée |
|
||||
|---|---|---|---|
|
||||
| 1 | **ineszang** (seule admin du dépôt) | Environnement `prod` : branche autorisée `main`, un relecteur requis. Environnement `rec` : branche `dev`. Settings > Actions : « Require approval for all outside collaborators ». Générer le jeton d'enregistrement du runner (Settings > Actions > Runners > New self-hosted runner, Linux x64) et le transmettre à Johan | 10 min |
|
||||
| 2 | **Johan** | Déposer sa clé sur la VM : `ssh-copy-id -i ~/.ssh/id_ed25519.pub root@10.101.200.37`, mot de passe du compte administrateur local des postes de l'école | 2 min |
|
||||
| 3 | Johan + Claude | **Fait à 15h** : branche locale `feat/deploy-rec-prod` avec tous les changements du §3, image frontend reconstruite avec succès, fusion Compose vérifiée pour les deux environnements. Reste : commit, push, PR vers `dev` | fait |
|
||||
| 4 | Claude, par SSH | `scripts/provision-host.sh` sur la VM. Écrire les deux `.env` (secrets générés sur la VM, jamais dans git). Certificats : `PUBLIC_HOST=rec.enervision.local PUBLIC_IP=10.101.200.37 make tls-selfsigned` dans `rec`, idem avec `enervision.local` dans `prod`. Puis `make stack-up` dans chaque dossier | 20 min plus la construction des images |
|
||||
| 5 | Johan, sur la VM | Installer le runner sous un utilisateur non-root membre du groupe `docker`, label `eni-g3`, en service systemd (`./config.sh --unattended --labels eni-g3`, `sudo ./svc.sh install && sudo ./svc.sh start`) | 10 min |
|
||||
| 6 | Équipe | Merger la PR dans `dev` : la recette se redéploie seule. Ouvrir la PR `dev` vers `main` : la prod se déploie après approbation dans l'onglet Environments | 15 min |
|
||||
| 7 | Tous | Vérifier depuis un poste de l'équipe, `/etc/hosts` renseigné : connexion, tableau de bord, Airflow par tunnel SSH | 15 min |
|
||||
|
||||
Contrôle en fin de chaîne, depuis la VM :
|
||||
|
||||
```bash
|
||||
curl -k https://localhost/api/v1/health/ready # prod
|
||||
curl -k https://localhost:8443/api/v1/health/ready # rec
|
||||
docker compose -p enervision-prod ps
|
||||
docker compose -p enervision-rec ps
|
||||
```
|
||||
|
||||
## 5. Ce qui peut faire échouer la journée, et la parade
|
||||
|
||||
| Risque | Parade |
|
||||
|---|---|
|
||||
| **8 Go de RAM pour deux stacks complètes** (deux Airflow, deux TimescaleDB, deux API) | Demander dès maintenant le passage à 32 Go, prévu par les consignes. En attendant : `TS_TUNE_MEMORY=2GB` et deux workers gunicorn pour Airflow. Si la RAM ne suit pas, démarrer la recette sans Airflow (`docker compose up -d --scale airflow-webserver=0 --scale airflow-scheduler=0`) |
|
||||
| **Compose trop ancien sur la VM** (les marqueurs `!override` et `!reset` exigent 2.24.4) | `docker compose version` en premier. Sinon installer le paquet `docker-compose-plugin` depuis le dépôt Docker |
|
||||
| **Pas de sortie Internet depuis la VM** | `curl -sI https://github.com` et `docker pull hello-world` avant tout. Sans sortie, ni construction d'image ni runner : déploiement manuel par `scp` d'images, plan B lourd |
|
||||
| **Runner auto-hébergé sur un dépôt public** | Le workflow de déploiement ne s'exécute que sur `push` vers `dev` et `main`, jamais sur `pull_request`. Réglage d'approbation des PR externes (étape 1). Runner sous un utilisateur dédié, jamais root |
|
||||
| **Premier démarrage avec un volume `pgdata` vide** | C'est le cas nominal sur la VM : `db/init` crée les bases `enervision`, `enervision_test` et `airflow`. Ne pas restaurer un volume de poste |
|
||||
| **Le jury accepte mal un certificat auto-signé** | Dire pourquoi avant qu'on le demande : aucun DNS public, ACME câblé et documenté, ADR 0007. Un clic « continuer » dans le navigateur |
|
||||
| **Conflit avec `feat/deploy`** (ineszang y a mergé `dev` à 14h06) | Partager ce document avant de pousser. La PR remplace `feat/deploy`, elle ne s'y ajoute pas |
|
||||
|
||||
## 6. Ce que ça donne pour la grille
|
||||
|
||||
- **EC03, CI/CD** : la chaîne ne s'arrête plus au merge. Deux environnements, déploiement
|
||||
automatique en recette, promotion approuvée en production, journal des déploiements dans
|
||||
l'onglet Environments de GitHub.
|
||||
- **EC04, cloud et sécurisation** : une application déployée et fonctionnelle, une seule surface
|
||||
exposée par environnement, secrets hors de git et hors de GitHub, base et Airflow joignables
|
||||
uniquement par tunnel SSH.
|
||||
- **Dossier EC01** : le choix 16 (runner auto-hébergé, déploiement automatique) passe de « non
|
||||
fait » à « tenu ». Le choix 12 (Ansible) reste non fait, et la réponse est prête : le
|
||||
durcissement de la machine n'est pas automatisé, le script de provisionnement en est la
|
||||
première brique, Terraform pourra l'appeler.
|
||||
|
||||
## 7. Après vendredi, si on continue
|
||||
|
||||
Dans l'ordre de valeur : images construites une fois en CI et publiées sur GHCR, puis déployées
|
||||
par digest (vraie promotion d'artefact). Racine Terraform `environments/eni-g3` qui provisionne
|
||||
la machine et le runner à partir du script. Sauvegarde de `pgdata` par `pg_dump` planifié.
|
||||
Monitoring (issue #26). Et seulement ensuite la bascule k3s, si elle garde un sens.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# 0011 - La surveillance de dérive vit dans le backend et écrit sa propre table
|
||||
# 0013 - La surveillance de dérive vit dans le backend et écrit sa propre table
|
||||
|
||||
- Statut : accepté
|
||||
- Date : 2026-09-22
|
||||
Reference in New Issue
Block a user