diff --git a/Dockerfile b/Dockerfile index 7f86827..fb38091 100644 --- a/Dockerfile +++ b/Dockerfile @@ -71,7 +71,11 @@ RUN chmod +x ./docker/entrypoint.sh RUN mkdir -p /data/documents /secrets && chown -R nextjs:nodejs /data /secrets USER nextjs -EXPOSE 3000 -ENV PORT=3000 HOSTNAME=0.0.0.0 +# Port peu courant jusque **dans** l'image : le reverse-proxy porte alors la +# même valeur partout, et rien ne rappelle un 3000 par défaut. Surchargeable +# par la pile, qui aligne publication, sonde de santé et serveur sur une +# unique variable. +EXPOSE 9317 +ENV PORT=9317 HOSTNAME=0.0.0.0 CMD ["./docker/entrypoint.sh"] diff --git a/docker-compose.yml b/docker-compose.yml index 302f4b8..6aa7783 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,11 @@ services: # du même réseau. Le changer reste recommandé. POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-planflow-interne} POSTGRES_DB: ${POSTGRES_DB:-planflow} + # Port d'écoute **dans** le conteneur. Un port non commun ne protège de + # rien — sur un réseau Docker, chaque conteneur a son adresse et deux + # services peuvent écouter le même port sans se gêner — mais il lève + # l'ambiguïté quand plusieurs bases cohabitent derrière le même proxy. + PGPORT: ${POSTGRES_PORT_INTERNAL:-5439} # Transmis au script d'initialisation, qui crée le rôle de connexion de # l'application — celui-ci ne doit surtout pas être le superutilisateur. APP_DB_USER: ${APP_DB_USER:-planflow_app} @@ -53,13 +58,17 @@ services: sleep 1 done ) & - exec docker-entrypoint.sh postgres + exec docker-entrypoint.sh postgres -p "$$PGPORT" volumes: - db-data:/var/lib/postgresql/data # Crée le rôle applicatif à la première initialisation du volume. - ./docker/init-app-role.sh:/docker-entrypoint-initdb.d/10-init-app-role.sh:ro healthcheck: - test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER:-planflow} -d ${POSTGRES_DB:-planflow}'] + test: + [ + 'CMD-SHELL', + 'pg_isready -p ${POSTGRES_PORT_INTERNAL:-5439} -U ${POSTGRES_USER:-planflow} -d ${POSTGRES_DB:-planflow}', + ] interval: 5s timeout: 5s retries: 10 @@ -78,10 +87,9 @@ services: networks: - interne - nginx_default - # Le conteneur reste sur 5432 pour l'application, et le port hôte est publié - # sur un port non commun, configurable : plusieurs instances PostgreSQL - # coexistent sur le même serveur, et s'en tenir au 5432 par défaut les - # ferait entrer en collision. + # Ni le port du conteneur ni celui de l'hôte ne sont les ports usuels : + # plusieurs instances PostgreSQL coexistent sur un même serveur, et s'en + # tenir au 5432 partout invite la confusion. # # **Lié à la boucle locale**, et c'est important : la publication sert à se # connecter depuis le serveur (sauvegarde, psql), pas depuis le réseau. Sans @@ -89,7 +97,7 @@ services: # données RH derrière un mot de passe par défaut, joignable depuis # l'extérieur. Pour un accès distant, passer par un tunnel. ports: - - '${POSTGRES_BIND:-127.0.0.1}:${POSTGRES_PORT:-55432}:5432' + - '${POSTGRES_BIND:-127.0.0.1}:${POSTGRES_PORT:-55432}:${POSTGRES_PORT_INTERNAL:-5439}' # Pose le rôle applicatif à **chaque** démarrage de la pile. # @@ -110,6 +118,7 @@ services: condition: service_healthy environment: PGHOST: db + PGPORT: ${POSTGRES_PORT_INTERNAL:-5439} PGPASSWORD: ${POSTGRES_PASSWORD:-planflow-interne} POSTGRES_USER: ${POSTGRES_USER:-planflow} POSTGRES_DB: ${POSTGRES_DB:-planflow} @@ -134,10 +143,14 @@ services: condition: service_completed_successfully environment: NODE_ENV: production + # Port d'écoute du serveur **dans** le conteneur. C'est celui que vise + # le reverse-proxy ; il doit rester le même que celui du healthcheck et + # de la publication, d'où l'unique variable. + PORT: ${APP_PORT_INTERNAL:-9317} # Le compte applicatif, **pas** le superutilisateur d'amorçage : un # superutilisateur contourne la row-level security, y compris déclarée en # FORCE, et l'isolation ne reposerait plus que sur la couche applicative. - DATABASE_URL: postgresql://${APP_DB_USER:-planflow_app}:${APP_DB_PASSWORD:-planflow-app-interne}@db:5432/${POSTGRES_DB:-planflow} + DATABASE_URL: postgresql://${APP_DB_USER:-planflow_app}:${APP_DB_PASSWORD:-planflow-app-interne}@db:${POSTGRES_PORT_INTERNAL:-5439}/${POSTGRES_DB:-planflow} # Laissée vide, elle est **produite au premier démarrage** et conservée # dans le volume `secrets`. La renseigner ici reste possible pour un # déploiement qui gère ses secrets par ailleurs — mais une variable @@ -158,6 +171,7 @@ services: POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-planflow-interne} POSTGRES_DB: ${POSTGRES_DB:-planflow} POSTGRES_HOST: db + POSTGRES_PORT_INTERNAL: ${POSTGRES_PORT_INTERNAL:-5439} APP_DB_USER: ${APP_DB_USER:-planflow_app} APP_DB_PASSWORD: ${APP_DB_PASSWORD:-planflow-app-interne} volumes: @@ -167,21 +181,23 @@ services: - secrets:/secrets networks: # `interne` pour joindre la base, `nginx_default` pour être joignable par - # le reverse-proxy — qui atteint le conteneur sur son port 3000, sans + # le reverse-proxy — qui atteint le conteneur sur son port interne, sans # passer par le port publié. - interne - nginx_default ports: # Port peu courant : le service est censé passer par le reverse-proxy, et - # un 3000 publié sur l'hôte se heurte à tout ce qui traîne. - - '${APP_PORT:-9317}:3000' + # un 3000 publié sur l'hôte se heurte à tout ce qui traîne. Le port + # **interne** l'est aussi, pour que la configuration du reverse-proxy + # porte partout la même valeur. + - '${APP_PORT:-9317}:${APP_PORT_INTERNAL:-9317}' healthcheck: test: [ 'CMD', 'node', '-e', - "fetch('http://127.0.0.1:3000/api/sante').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))", + "fetch('http://127.0.0.1:${APP_PORT_INTERNAL:-9317}/api/sante').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))", ] interval: 15s timeout: 5s diff --git a/docker/bootstrap-role.mjs b/docker/bootstrap-role.mjs index 8ba4294..a55bec5 100644 --- a/docker/bootstrap-role.mjs +++ b/docker/bootstrap-role.mjs @@ -22,7 +22,10 @@ const superUser = process.env.POSTGRES_USER ?? 'planflow'; const superPassword = process.env.POSTGRES_PASSWORD ?? ''; const database = process.env.POSTGRES_DB ?? 'planflow'; const host = process.env.POSTGRES_HOST ?? 'db'; -const port = Number(process.env.POSTGRES_PORT_INTERNAL ?? 5432); +// Le port d'écoute **dans** le conteneur de la base, que la pile choisit peu +// commun. Le défaut suit celui du compose, pas celui de PostgreSQL : une +// valeur qui diverge se solderait par un refus de connexion à l'amorçage. +const port = Number(process.env.POSTGRES_PORT_INTERNAL ?? 5439); const appRole = process.env.APP_DB_USER ?? 'planflow_app'; const appPassword = process.env.APP_DB_PASSWORD ?? 'planflow-app-interne'; diff --git a/docker/init-app-role.sh b/docker/init-app-role.sh index e61b914..2435009 100755 --- a/docker/init-app-role.sh +++ b/docker/init-app-role.sh @@ -28,7 +28,9 @@ DB_USER="${POSTGRES_USER:-planflow}" # Dans le conteneur `db`, l'hôte est local ; depuis `db-init`, c'est `db`. # L'un et l'autre doivent aboutir au même SQL. if [ -n "${PGHOST:-}" ]; then - set -- -h "$PGHOST" -p "${PGPORT:-5432}" -U "$DB_USER" -d "$DB_NAME" -v ON_ERROR_STOP=1 + # Défaut aligné sur celui de la pile, et non sur celui de PostgreSQL : la + # base écoute un port peu commun, et 5432 ne joindrait rien. + set -- -h "$PGHOST" -p "${PGPORT:-5439}" -U "$DB_USER" -d "$DB_NAME" -v ON_ERROR_STOP=1 else set -- -U "$DB_USER" -d "$DB_NAME" -v ON_ERROR_STOP=1 fi diff --git a/prisma/seed.ts b/prisma/seed.ts index 013784f..b143160 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -819,7 +819,18 @@ async function seedPlanning(accountId: string): Promise { isoWeek: week.isoWeek, }, }, - update: {}, + // L'état de publication est **réimposé**, pas seulement posé à la + // création. Un seed qui laisse en place ce qu'il trouve ne produit un + // état de départ connu que la première fois : la semaine précédente, + // publiée par définition, reste en brouillon dès lors qu'elle a déjà + // été semée quand elle était la semaine courante. Les tests qui + // s'appuient dessus échouent alors sur une base de développement et + // passent en intégration continue, où le semis est neuf — l'écart le + // plus coûteux à diagnostiquer. + update: { + status: week === previous ? 'PUBLISHED' : 'DRAFT', + publishedAt: week === previous ? new Date() : null, + }, create: { accountId, teamId: team.id, diff --git a/tests/e2e/heures.spec.ts b/tests/e2e/heures.spec.ts index 52144e4..c15acf2 100644 --- a/tests/e2e/heures.spec.ts +++ b/tests/e2e/heures.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; -import { formatMonthParam, monthOf, previousMonth } from '../../src/domain/planning/month'; +import { formatMonthParam, monthOf } from '../../src/domain/planning/month'; /** * Heures et périodes de paie. @@ -10,7 +10,22 @@ import { formatMonthParam, monthOf, previousMonth } from '../../src/domain/plann * plutôt qu'un bouton. */ -const MONTH = formatMonthParam(previousMonth(monthOf(new Date()))); +/** + * Mois **courant**, et non le précédent. + * + * Le seed ne pose que deux semaines de planning : la courante et la + * précédente. Le mois précédent n'en contient donc aucune, sauf quand on joue + * la suite dans les premiers jours d'un mois — le tableau est vide le reste du + * temps, et l'assertion échoue pour une raison sans rapport avec ce qu'elle + * vérifie. Le défaut ne se voyait pas sur une base de développement, qui garde + * les créneaux des exécutions précédentes ; l'intégration continue, elle, + * repart d'un semis neuf. + * + * Le mois courant, lui, contient toujours aujourd'hui — donc toujours des + * créneaux, et aucun n'a d'heures réelles puisque rien dans le semis ni dans la + * suite n'en saisit. + */ +const MONTH = formatMonthParam(monthOf(new Date())); /** * Mois propre à cette exécution.