Sync push_then_sync : préserve les absences 'publiee_escada' contre écrasement/orphelines après push (PDF Escada stale). UI reconnaît le statut (calendrier, éditeur, KPIs) au lieu d'afficher 'présent'. Sync_esacada : timeout grille 20s → 45s + retry après reload (AUTOMAT 1 échouait à la 1re classe après changement de langue). Telegram : ajoute liste d'erreurs + tail du log dans les notifs d'échec même en mode normal — avant on avait juste 'a échoué (code 1)'. UX : - Calendrier toujours visible (même sans absences) et démarre sur le mois courant (pas sur le 1er mois d'absence) ; tous les jours cliquables pour pouvoir ajouter une absence. - Date du jour pré-sélectionnée aussi via navigate_to (clic depuis /classe). - KPIs cards taggées kpi-card/kpi-value pour CSS responsive mobile. - Badge 'DEV' dans la sidebar (APP_ENV=dev) — invisible en prod. - Badge 'Built with Reflex' masqué. - KPIs retirés du dashboard /accueil. Prod : - Dockerfile.prod multi-stage (Reflex export bundle + runtime slim). - docker-compose.prod.yml séparé (port 3002, projet eptm-dashboard-prod). - .gitignore + .dockerignore nettoyés. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
12 KiB
Déploiement en production — EPTM Dashboard
Procédure de bascule de l'app Streamlit legacy (absences.service,
dashboard.eptm-automation.ch) vers l'app Reflex (en prod sur le
même sous-domaine).
État : stack prod construite et démarrée le 2026-05-12. Cutover NPM encore à faire (cf. § « Étape manuelle restante »).
État actuel (2026-05-11)
Internet :80/:443
│
▼
┌────────────────────────────────────┐
│ NPM (nginx-proxy-manager) │
│ container "npm" sur proxy_net │
└────────────────────────────────────┘
│
├── dashboard.eptm-automation.ch
│ → 172.17.0.1:8501
│ → Streamlit legacy (systemd "absences.service")
│ /opt/absences/.venv/bin/streamlit run src/app.py
│
└── dev.dashboard.eptm-automation.ch
→ eptm-dashboard-app-1:3001 (Reflex dev)
+ /_event → :8001
(network proxy_net)
- Streamlit tourne en systemd
absences.service(enabled, depuis le 2026-05-09). - L'app Reflex dev est containerisée, déjà sur
proxy_net, accessible via NPM. - Pas encore de Dockerfile/compose prod : il faut les créer.
État cible
Internet :80/:443 → NPM
│
├── dashboard.eptm-automation.ch
│ → eptm-dashboard-prod-app-1:3002 (Reflex prod)
│ + /_event → :8002
│
└── dev.dashboard.eptm-automation.ch
→ eptm-dashboard-app-1:3001 (Reflex dev, inchangé)
+ /_event → :8001
- Streamlit
absences.servicearrêté + disabled (binaires gardés en backup quelques semaines, suppression plus tard). - Deux stacks compose côte à côte sur
proxy_net:- dev : 3001 (frontend Vite) + 8001 (backend granian) — séparés car HMR
- prod : 3002 uniquement (Reflex 0.9+ exige frontend + backend même
port en prod ; granian sert tout depuis 3002, y compris
/_eventen WS)
- Projets compose nommés :
eptm-dashboard-deveteptm-dashboard-prod(pour éviter quecompose upd'une stack recrée le container de l'autre). - Aucun port host exposé — tout passe par NPM sur
proxy_net. - NPM continue de gérer les certs Let's Encrypt (déjà émis pour ce sous-domaine, juste à conserver — pas de renouvellement à forcer).
✅ Décisions prises (2026-05-12)
| # | Question | Hypothèse par défaut |
|---|---|---|
| # | Question | Décision |
| --- | --- | --- |
| 1 | Données prod | Isolées : ./data_prod/ séparé de ./data/ |
| 2 | DB initiale prod | Copie de data/absences.db au cutover |
| 3 | Streamlit après cutover | Disabled ~1 mois (rollback plan B), purge en tâche TODO |
| 4 | Build | Local sur le serveur (pas de CI / registry pour démarrer) |
| 5 | Anciens Dockerfile + docker-compose.yml |
Renommés .back, nouveaux fichiers .prod créés à côté |
| 6 | .env.prod |
Minimal : REFLEX_SECRET_KEY + Telegram bot + TZ. SMTP/Escada/etc. restent dans settings.json |
| 7 | Emplacement data_prod/ |
/opt/eptm-dashboard/data_prod/ (gitignored) |
| 8 | browser_profile/ côté prod |
Non copié — session SSO à refaire au 1er sync (Chromium non-headless une fois) |
| 9 | Templates AcroForm + docs .md |
Déménagés depuis data/ vers le repo (./templates/ et ./docs/) → propagés par COPY . du Dockerfile, plus besoin de sync manuel |
| 10 | Crontab prod | Séparée de la dev (lignes distinctes pour les deux containers) |
Étape manuelle restante : cutover NPM + Streamlit
L'image prod est buildée, le container eptm-dashboard-prod-app-1 tourne sur
proxy_net:3002. Pour basculer dashboard.eptm-automation.ch :
1. Reconfigurer NPM (UI sur https://npm.eptm-automation.ch:81)
Sur le proxy host existant dashboard.eptm-automation.ch (qui pointe
actuellement sur Streamlit 172.17.0.1:8501) :
- Forward Hostname / IP :
eptm-dashboard-prod-app-1 - Forward Port :
3002 - Cache Assets : décoché (Reflex bundle Vite est déjà optimisé)
- Block Common Exploits : OK
- Websockets Support : ✅ coché (indispensable — Reflex utilise WS)
- Onglet SSL : conserver le certificat Let's Encrypt déjà émis, Force SSL ON, HSTS ON, HTTP/2 ON
⚠️ Pas besoin de custom location
/_eventcette fois (à la différence du dev) : en prod le backend et le frontend sont sur le même port (3002).
2. Stopper Streamlit
sudo systemctl stop absences
sudo systemctl disable absences # rollback toujours possible avec `enable + start`
3. Vérifier
curl -fsS -o /dev/null -w "HTTP %{http_code}\n" https://dashboard.eptm-automation.ch/
# → 200 attendu, server: granian dans les headers
4. Ajouter une ligne crontab pour les cron_tick prod
crontab -e
# Ajouter :
* * * * * docker exec eptm-dashboard-prod-app-1 python /app/scripts/cron_tick.py >> /var/log/cron-prod.log 2>&1
(La ligne dev existante reste — les deux containers exécutent leur propre
cron_tick avec leur propre table cron_jobs.)
Fichiers créés (déjà en place)
1. Dockerfile.prod (multi-stage)
# Stage 1 : builder — installe deps + export frontend
FROM python:3.13-slim AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
nodejs npm curl unzip && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN reflex export --frontend-only --no-zip
# Stage 2 : runtime — backend granian uniquement
FROM python:3.13-slim
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
curl ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app /app
RUN pip install --no-cache-dir -r requirements.txt
ENV FRONTEND_PORT=3002 BACKEND_PORT=8002
EXPOSE 3002 8002
CMD ["reflex", "run", "--env", "prod"]
NB : à vérifier — Reflex 0.9.x peut exiger
reflex run --backend-onlycôté runtime si le frontend export est servi par le même process. TODO : tester avec un build pilote avant cutover.
2. docker-compose.prod.yml
services:
app:
build:
context: .
dockerfile: Dockerfile.prod
image: eptm-dashboard-prod
container_name: eptm-dashboard-prod-app-1
init: true
restart: unless-stopped
volumes:
- ./data-prod:/app/data # ← séparé de dev (cf. Q1)
- ./logs-prod:/logs
env_file:
- .env.prod
environment:
- FRONTEND_PORT=3002
- BACKEND_PORT=8002
- API_URL=https://dashboard.eptm-automation.ch
- REFLEX_HOT_RELOAD_EXCLUDE_PATHS=/app/data
- TZ=Europe/Zurich
networks:
- default
- proxy_net
networks:
proxy_net:
external: true
3. .env.prod
À auditer — copier les valeurs sensibles depuis le settings.json plutôt
que de tout mettre en env (séparation cleane). Variables minimales :
DATA_DIR=/app/data(déjà override probable)(à voir avec l'user — TZ, secrets, etc.)
Procédure de cutover (jour J)
J−1 (préparation)
- Backup DB Streamlit (si pertinent — vérifier si Streamlit a sa propre DB) :
sudo cp /opt/absences/data /opt/backups/absences-$(date +%F).tgz # à adapter - Backup DB Reflex dev (qui deviendra la prod) :
cp /opt/eptm-dashboard/data/eptm.db /opt/backups/eptm-pre-cutover-$(date +%F).db - Créer
Dockerfile.prod,docker-compose.prod.yml,.env.prod(cf. ci-dessus). - Préparer
./data-prod/(copie de./data/au moment opportun).
J0 (cutover, ~10 min de fenêtre)
# 1. Build l'image prod (peut être fait avant la fenêtre — pas de downtime)
cd /opt/eptm-dashboard
docker compose -f docker-compose.prod.yml build app
# 2. Snapshot DB de dev → data-prod
cp -r data/ data-prod/
# (optionnel : purger data-prod/browser_profile et data-prod/pdfs si volumineux
# et resync-able depuis Escada)
# 3. Démarrer le container prod (encore inaccessible — NPM pointe encore sur Streamlit)
docker compose -f docker-compose.prod.yml up -d app
docker logs -f eptm-dashboard-prod-app-1 # vérifier "App running" puis Ctrl-C
# 4. Stopper Streamlit
sudo systemctl stop absences
sudo systemctl disable absences
# 5. Reconfigurer NPM proxy host #2 (dashboard.eptm-automation.ch)
# via UI https://npm.eptm-automation.ch :
# - Forward Hostname / IP : eptm-dashboard-prod-app-1
# - Forward Port : 3002
# - WebSocket support : ON
# - Custom location :
# Location : /_event
# Forward : eptm-dashboard-prod-app-1:8002
# Advanced : proxy_read_timeout 86400;
# - SSL : conserver le certificat Let's Encrypt déjà émis pour
# ce domaine, Force SSL, HSTS, HTTP/2
# 6. Recharger NPM si l'UI ne le fait pas auto :
docker exec npm nginx -s reload
# 7. Vérification
curl -I https://dashboard.eptm-automation.ch
# → doit retourner 200 + headers Reflex (Server: granian)
J+1 → J+30 (stabilisation)
- Garder
absences.serviceendisabled(rollback rapide possible). - Surveiller
docker logs -f eptm-dashboard-prod-app-1+data-prod/logs/. - Si stable après ~1 mois : purger
/opt/absences/, retirer le user systemd file.
Rollback (si quelque chose plante après le cutover)
# 1. Rebasculer NPM sur Streamlit
# UI NPM → proxy host #2 → Forward : 172.17.0.1:8501
# 2. Relancer Streamlit
sudo systemctl start absences
# 3. Stopper le container prod Reflex
docker compose -f docker-compose.prod.yml down
# 4. Investiguer les logs Reflex tranquillement
docker logs eptm-dashboard-prod-app-1 > /tmp/cutover-fail.log
Workflow déploiements suivants
Une fois la prod en place :
# 1. Dev : commit & push
git add -A && git commit -m "feat: xxx" && git push
# 2. Build nouvelle image prod
cd /opt/eptm-dashboard
docker compose -f docker-compose.prod.yml build app
# 3. Redémarrer le container (downtime ~10s)
docker compose -f docker-compose.prod.yml up -d app
# 4. Vérifier
docker logs -f eptm-dashboard-prod-app-1
curl -I https://dashboard.eptm-automation.ch
Optionnel : tagger les versions prod (git tag -a prod-2026-05-15 && git push --tags).
Caveats / pièges à surveiller
- WebSocket NPM : déjà OK en dev (cf. memory NPM). Reproduire exactement
la même config sur proxy host #2 : WS support cocheé + custom location
/_eventavecproxy_read_timeout 86400. Sans ça, le state Reflex ne fonctionne pas. API_URL: doit matcher l'URL publique exacte (https://dashboard.eptm-automation.ch), sinon le frontend ne joint pas le backend.- Ports internes prod :
3002/8002(pas3001/8001qui sont pris par dev). reflex exportprend 1-5 min (npm install + bundle). À faire avant la fenêtre de cutover.data-prod/browser_profile/: éviter de copier — le profil Chrome contient une session SSO valide pour le compte de dev. La prod doit re-login au premier sync Escada (ouvrir un Chromium visible avecheadless=Falseune fois, ou pré-importer le profile sur un compte service dédié).- localStorage des users connectés : le LocalStorage du browser des
utilisateurs survit au cutover (clés
username,theme, etc.) — ils ne seront pas déconnectés. - DNS / certs :
dashboard.eptm-automation.chpointe déjà sur l'IP publique du serveur, le cert NPM est déjà émis et auto-renouvelé. Pas d'action DNS/cert nécessaire. - Backups DB : ajouter un cron quotidien après cutover :
0 3 * * * cp /opt/eptm-dashboard/data-prod/eptm.db /opt/backups/eptm-$(date +\%F).db
Annexe : commandes de check post-déploiement
# Container prod up ?
docker ps --filter name=eptm-dashboard-prod-app-1
# Logs récents
docker logs --tail 50 eptm-dashboard-prod-app-1
# Proxy NPM répond ?
curl -fsS -o /dev/null -w "HTTP %{http_code}\n" https://dashboard.eptm-automation.ch/login
# WS handshake OK ?
curl -fsS -o /dev/null -w "HTTP %{http_code}\n" \
-H "Upgrade: websocket" -H "Connection: Upgrade" \
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
-H "Sec-WebSocket-Version: 13" \
https://dashboard.eptm-automation.ch/_event
# → attend HTTP 101 (Switching Protocols)