Compare commits

...

6 Commits

Author SHA1 Message Date
Manus
a13fe40eb4 Checkpoint: Le stockage PDF devient un volume Docker externe explicitement nommé, avec nocopy. Le conteneur refuse de démarrer si /app/storage n’est pas un montage inscriptible, et son healthcheck contrôle aussi ce montage. Les tests, TypeScript et build ont été validés.
All checks were successful
Validation applicative / TypeScript, tests et build (push) Successful in 3m20s
2026-09-03 09:20:33 +00:00
Manus
eb2c9cc6b6 Checkpoint: Le volume persistant demat_facturation_storage est de nouveau monté sur /app/storage en production et les deux comptes Microsoft ADM.pareige et Olivier Pareige sont administrateurs, avec accès aux 146 factures globales. Les contrôles de conteneur, stockage et réponse PDF publique sont validés. 2026-09-03 07:36:04 +00:00
Manus
bd48ef52d2 Checkpoint: Le volume demat_facturation_storage est monté sur /app/storage, répertoire réellement servi par Express et utilisé pour les PDF source. Test de non-régression ajouté au compose. Les tests, TypeScript et build sont validés avant l’application ciblée en production. 2026-09-02 15:52:23 +00:00
Manus
53edecf4e7 Checkpoint: Incident 404 production corrigé : le domaine Traefik est désormais configurable via APP_DOMAIN, configuré avec le domaine production sur le serveur. Le service applicatif est sain et les routes publiques racine et Automatismes répondent HTTP 200. La base et les volumes n’ont pas été touchés. 2026-09-02 15:21:44 +00:00
Manus
b5f2cb48fe Checkpoint: Le routage Traefik utilise désormais APP_DOMAIN avec le domaine recette comme repli, ce qui évite qu’une image de production serve un routeur vers la recette. Test unitaire de configuration ajouté ; 53 tests, TypeScript et build validés. 2026-09-02 15:19:28 +00:00
Manus
fe0961ab39 Checkpoint: Version f7c522b1 déployée en recette et production : conteneurs sains, routes Automatismes et Paramètres en HTTP 200, migrations importTrigger et exportAutomationRules appliquées, dashboard production vérifié. Les deux factures SFR importées avant la création de leur règle ont été mises à jour avec OPEX, DSI SANTINOVA, ventilation SANTINOVA et abonnement Oui après confirmation. 2026-09-02 14:11:02 +00:00
5 changed files with 99 additions and 5 deletions

View File

@@ -63,10 +63,11 @@ COPY --from=builder /app/dist ./dist
# Copy drizzle migrations
COPY drizzle/ ./drizzle/
COPY drizzle.config.ts ./
COPY scripts/ensure-storage-volume.sh ./scripts/ensure-storage-volume.sh
# Create uploads directory
RUN mkdir -p /app/uploads
# Create uploads directory and make the PDF volume guard executable.
RUN mkdir -p /app/uploads && chmod 755 /app/scripts/ensure-storage-volume.sh
EXPOSE 3000
CMD ["node", "dist/index.js"]
CMD ["/app/scripts/ensure-storage-volume.sh"]

View File

@@ -40,6 +40,8 @@ services:
JWT_SECRET: demat-facturation-jwt-secret-recette-2026
MISTRAL_API_KEY: ${MISTRAL_API_KEY:-}
UPLOAD_DIR: /app/uploads
# Chemin unique utilisé par le serveur et vérifié au démarrage.
STORAGE_BASE_PATH: /app/storage
# Manus OAuth (optionnel en recette)
VITE_APP_ID: ${VITE_APP_ID:-demat-facturation-recette}
OAUTH_SERVER_URL: ${OAUTH_SERVER_URL:-}
@@ -48,17 +50,24 @@ services:
OWNER_NAME: ${OWNER_NAME:-Administrateur}
volumes:
- demat_facturation_uploads:/app/uploads
# Les factures source et leurs métadonnées sont servies sous /storage.
# Il est externe et n'est donc jamais recréé ni supprimé par Compose.
- type: volume
source: demat_facturation_storage
target: /app/storage
volume:
nocopy: true
networks:
- demat-facturation-internal
- web
labels:
- "traefik.enable=true"
- "traefik.http.routers.demat-facturation.rule=Host(`demat-facturation.recette.santinova-soft.org`)"
- "traefik.http.routers.demat-facturation.rule=Host(`${APP_DOMAIN:-demat-facturation.recette.santinova-soft.org}`)"
- "traefik.http.routers.demat-facturation.entrypoints=websecure"
- "traefik.http.routers.demat-facturation.tls.certresolver=letsencrypt"
- "traefik.http.services.demat-facturation.loadbalancer.server.port=3000"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
test: ["CMD-SHELL", "grep -qs ' /app/storage ' /proc/mounts && test -w /app/storage && curl -fsS http://localhost:3000/api/health"]
interval: 30s
timeout: 10s
retries: 3
@@ -69,6 +78,9 @@ volumes:
driver: local
demat_facturation_uploads:
driver: local
demat_facturation_storage:
external: true
name: ${STORAGE_VOLUME_NAME:-demat-facturation-dsi_demat_facturation_storage}
networks:
demat-facturation-internal:

View File

@@ -0,0 +1,24 @@
#!/bin/sh
# Refuse de lancer l'application si le répertoire PDF attendu n'est pas un
# volume Docker réellement monté. Cette barrière évite de démarrer avec un
# dossier de conteneur vide après une reconstruction.
set -eu
storage_path="${STORAGE_BASE_PATH:-/app/storage}"
if [ ! -d "$storage_path" ]; then
echo "[StorageGuard] Répertoire de stockage absent : $storage_path" >&2
exit 1
fi
if ! awk -v expected="$storage_path" '$2 == expected { mounted = 1 } END { exit mounted ? 0 : 1 }' /proc/mounts; then
echo "[StorageGuard] Volume persistant non monté sur $storage_path ; démarrage annulé." >&2
exit 1
fi
if [ ! -w "$storage_path" ]; then
echo "[StorageGuard] Volume persistant non inscriptible : $storage_path" >&2
exit 1
fi
exec node dist/index.js

View File

@@ -0,0 +1,29 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
describe("routage et persistance de déploiement", () => {
it("utilise le domaine fourni par l’environnement avec la recette comme repli", () => {
const composeFile = readFileSync(join(process.cwd(), "docker-compose.yml"), "utf8");
expect(composeFile).toContain(
"Host(`${APP_DOMAIN:-demat-facturation.recette.santinova-soft.org}`)"
);
expect(composeFile).toContain("source: demat_facturation_storage");
expect(composeFile).toContain("target: /app/storage");
});
it("refuse de recréer le volume PDF et contrôle son montage avant le démarrage", () => {
const composeFile = readFileSync(join(process.cwd(), "docker-compose.yml"), "utf8");
const dockerfile = readFileSync(join(process.cwd(), "Dockerfile"), "utf8");
const guardScript = readFileSync(join(process.cwd(), "scripts", "ensure-storage-volume.sh"), "utf8");
expect(composeFile).toContain("STORAGE_BASE_PATH: /app/storage");
expect(composeFile).toContain("external: true");
expect(composeFile).toContain("nocopy: true");
expect(composeFile).toContain("grep -qs ' /app/storage ' /proc/mounts");
expect(dockerfile).toContain('CMD ["/app/scripts/ensure-storage-volume.sh"]');
expect(guardScript).toContain("Volume persistant non monté");
expect(guardScript).toContain("/proc/mounts");
});
});

28
todo.md
View File

@@ -877,3 +877,31 @@
- [x] Coloriser les onglets Import et Export
- [x] Simplifier les libellés de l’onglet Export comme demandé
- [x] Vérifier le rendu en sandbox
## Déploiement des automatismes et paramètres
- [x] Synchroniser la version validée vers Gitea recette et déployer le service applicatif
- [x] Vérifier les routes, le conteneur et les migrations en recette
- [x] Synchroniser la même version vers Gitea production et déployer le service applicatif
- [x] Vérifier les routes, le conteneur, les migrations et le dashboard en production
## Filtre SANTINOVA des abonnements SFR
- [x] Identifier les deux factures SFR et leurs valeurs de destinataire, service et ventilation
- [x] Vérifier la logique du filtre SANTINOVA dans Factures abonnements : le filtre est correct, mais les deux factures ont été importées avant la création de la règle SFR
- [x] Appliquer la règle SFR aux deux factures existantes après confirmation utilisateur, puis vérifier le filtre Ventilation = SANTINOVA
## Incident 404 de production
- [x] Identifier l’origine du 404 : le label Traefik de production contient par erreur le domaine recette
- [x] Corriger uniquement le composant en cause sans toucher aux données ni aux volumes
- [x] Vérifier le retour HTTP 200 sur l’URL publique de production
## Incident factures et PDF de production
- [x] Vérifier les données factures, la connexion applicative à la base et les volumes PDF sans aucune suppression
- [x] Identifier les causes : le volume historique `/app/storage` contenant les PDF n’était plus monté, et les comptes Microsoft d’Olivier sont encore de rôle utilisateur sans facture propre
- [x] Corriger le composant PDF sans toucher aux factures ni aux volumes persistants
- [x] Vérifier l’ouverture d’un PDF en production
- [x] Promouvoir ADM.pareige et Olivier Pareige au rôle administrateur afin d’afficher les factures globales
## Prévention du démontage des PDF après reconstruction
- [x] Auditer les montages de stockage et la chaîne de reconstruction en recette et production
- [x] Ajouter un contrôle bloquant avant reconstruction et une vérification après démarrage du volume `/app/storage`
- [ ] Vérifier les garde-fous sur les deux environnements sans modifier les données