Compare commits
6 Commits
f7c522b18e
...
a13fe40eb4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a13fe40eb4 | ||
|
|
eb2c9cc6b6 | ||
|
|
bd48ef52d2 | ||
|
|
53edecf4e7 | ||
|
|
b5f2cb48fe | ||
|
|
fe0961ab39 |
@@ -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"]
|
||||
|
||||
@@ -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:
|
||||
|
||||
24
scripts/ensure-storage-volume.sh
Normal file
24
scripts/ensure-storage-volume.sh
Normal 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
|
||||
29
server/deploymentRouting.test.ts
Normal file
29
server/deploymentRouting.test.ts
Normal 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
28
todo.md
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user