Merge pull request #36 from R0m1k3/claude/combohr-app-recreation-plan-yu7wre

Réparer l'intégration continue, et poser des ports internes non communs
This commit is contained in:
LogiFlow authored and GitHub committed 2026-08-10 10:11:05 +02:00
commit d4b655f369
6 files changed
+70 -19

No files matched your search

+6 -2
View File
@@ -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"]
+28 -12
View File
@@ -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
+4 -1
View File
@@ -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';
+3 -1
View File
@@ -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
+12 -1
View File
@@ -819,7 +819,18 @@ async function seedPlanning(accountId: string): Promise<void> {
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,
+17 -2
View File
@@ -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.