Aller au contenu

sillage-sandbox

sillage-sandbox

Environnement Docker local pour faire tourner l’app apps/sillage/ bout-en-bout sans dépendre des instances de prod. Bundle une instance CRF (REDCap), un BaaS self-hosted (Appwrite), un mail-trap (Mailpit) et les scripts qui provisionnent automatiquement le tout, importent la trame sillage, peuplent REDCap en données synthétiques et écrivent un .env.local directement consommable par sillage.

Stack

ServiceURLRôle
sillage (app)http://localhost:5173L’app SvelteKit dockerisée (opt-in)
BaaS API (Appwrite)http://localhost:8090Sessions, comptes utilisateurs
BaaS Consolehttp://localhost:8091Admin UI Appwrite (SPA séparée)
CRF (REDCap)http://localhost:8888Source des demandes sillage
phpMyAdminhttp://localhost:8889Accès DB CRF (compte redcap)
Mailpit (UI)http://localhost:8025Capture tous les emails (magic-link inclus)
Mailpit (SMTP)localhost:1025Endpoint SMTP utilisé par Appwrite (interne)

La stack CRF + Mailpit vient de sandbox/crf-sandbox/ via include: Docker Compose v2.20+. La stack BaaS est déclarée inline dans docker-compose.yaml, services préfixés baas-. Le service baas est branché aux deux réseaux (baas-net et redcap-net) pour parler à Mailpit.

Prérequis

  • Docker Desktop ou compatible avec docker compose v2.20+
  • pnpm + Node ≥ 24 (pour les scripts tsx)
  • 4 Go de RAM libre minimum

Démarrage zéro-clic

Fenêtre de terminal
cd sandbox/sillage-sandbox
pnpm start

C’est tout. pnpm start crée .env depuis .env.example, génère _APP_OPENSSL_KEY_V1 (random 32-byte hex), lève les conteneurs, provisionne Appwrite + REDCap (avec un projet sillage dédié), importe la trame sillage, peuple les données, écrit apps/sillage/.env.local et lance un smoke test bout-en-bout du flow magic-link.

Variables d’environnement utiles :

VarValeursEffet
SEED_MODE(unset)Défaut auto : prod si PROD_CRF_URL + PROD_CRF_TOKEN sont set (dans .env ou .env.prod), sinon fake. Message affiché au lancement.
prodForce le pull des vrais records depuis PROD_CRF_URL / PROD_CRF_TOKEN
fakeForce 120 records synthétiques via @faker-js/faker
noneNe pré-remplit pas le projet
SKIP_E2E1Saute le smoke test final (le bootstrap s’arrête après le seed)

Exemples : pnpm start (auto), SEED_MODE=fake pnpm start (force fake même avec creds prod), SEED_MODE=none SKIP_E2E=1 pnpm start.

À la fin :

Fenêtre de terminal
cd ../../apps/sillage
pnpm dev

Ouvrir http://localhost:5173 et signer avec un email matchant ALLOWED_DOMAINS_REGEXP (par défaut @univ-lehavre.fr, @example.org ou @sillage.local). Le magic link arrive dans Mailpit (http://localhost:8025) — clique dessus, tu es loggué dans sillage.

sillage dockerisée (opt-in)

Par défaut, l’app tourne sur l’hôte via pnpm -F sillage dev (hot-reload, idéal pour développer). Pour la faire tourner dans Docker au lieu de l’hôte — utile pour reproduire un environnement build-once proche de la prod, ou pour démarrer la stack complète d’un seul docker compose — un service app est défini dans docker-compose.yaml. Il build l’image depuis apps/sillage/Dockerfile et l’expose sur http://localhost:5173.

Fenêtre de terminal
# 1. provisionne d'abord la stack (Appwrite + REDCap + .env rempli)
pnpm bootstrap # remplit APPWRITE_KEY et CRF_API_TOKEN dans .env
# 2. build + run l'app dockerisée (en plus des autres services)
docker compose up -d --build app
# 3. ouvre l'app
open http://localhost:5173

Notes importantes :

  • Build depuis la racine du monorepo. Le build.context du service est ../.. : le build pnpm a besoin du pnpm-lock.yaml, du pnpm-workspace.yaml et des package.json des paquets workspace (@univ-lehavre/atlas-*) dont sillage dépend. Le Dockerfile est multi-stage (builder lourd → runner Alpine minimal, user non-root node).
  • PUBLIC_* figées au build. sillage lit $env/static/public (PUBLIC_APPWRITE_ENDPOINT, PUBLIC_APPWRITE_PROJECT, PUBLIC_LOGIN_URL, PUBLIC_REDCAP_URL). En SvelteKit ces valeurs sont inlinées au build : elles sont passées en build.args (depuis .env). Changer une de ces valeurs impose donc un --build (rebuild de l’image), pas un simple restart. Elles pointent vers l’hôte (localhost:8090 / 8888) parce que c’est le navigateur de l’utilisateur, sur l’hôte, qui appelle le BaaS et le CRF — pas le conteneur.
  • Variables privées au runtime. APPWRITE_KEY, REDCAP_API_TOKEN et ALLOWED_DOMAINS_REGEXP ($env/static/private) sont lues à l’exécution par l’adapter Node ; elles arrivent par environment: depuis .env. Aucun secret n’est figé dans l’image. Lance donc pnpm bootstrap avant up app, sinon APPWRITE_KEY / CRF_API_TOKEN sont vides et l’auth/CRF échoue.
  • ADR 0021. Le sandbox ne dépend pas de l’app au niveau npm : il la lance via Docker à partir de son Dockerfile, sans jamais l’importer. Le package.json du sandbox ne référence pas @univ-lehavre/atlas-sillage.
  • Quand préférer quoi ? pnpm -F sillage dev pour itérer (hot-reload). Le service app dockerisé pour valider le build de prod ou démarrer la stack entière d’un coup.

Commandes

CommandeEffet
pnpm startWipe + docker:up + bootstrap + smoke en un coup (zero-touch)
pnpm stopArrête les conteneurs et efface les volumes anonymes
pnpm docker:upDémarre tous les conteneurs (BaaS + CRF + Mailpit)
pnpm docker:downArrête les conteneurs et efface les volumes anonymes
pnpm docker:logsTail des logs
pnpm bootstrapOrchestrateur complet (BaaS + CRF + seed + .env sillage)
pnpm bootstrap:baasProvisionne Appwrite (account root + org + projet + clé API)
pnpm bootstrap:crfInstalle REDCap, crée un projet sillage dédié, importe la trame
pnpm seedGénère et importe N records synthétiques (défaut 120, voir .env)
pnpm pull:prodPull opt-in des records de prod (nécessite PROD_CRF_* dans .env)
pnpm test:smokePlaywright smoke level-5 ; auto-spawn d’sillage dev via webServer

Les commandes up/down sont préfixées docker: parce que pnpm up est une commande native pnpm (= update) qui shadow-erait nos scripts.

Volumes anonymes. Les volumes Appwrite (/storage/*) et MongoDB (/data/db) sont déclarés anonymes dans docker-compose.yaml. Un pnpm stop (ou docker compose down --volumes) les efface — donc chaque pnpm start repart d’un état fraîchement bootstrappé. Le cold-bootstrap ajoute ~30-60s à chaque relance, en échange de la disparition des bugs de drift d’état entre sessions et entre sandboxes (cf. la sandbox amarre qui partage le projet REDCap id=1).

Tous les scripts sont idempotents — re-lance-les sans crainte.

Comment ça marche

Bootstrap Appwrite

bootstrap-baas.ts attaque l’API Appwrite directement :

  1. POST /v1/account crée le compte root (sur une install fraîche, le premier compte est promu root automatiquement). Si le compte existe déjà, on continue.
  2. POST /v1/account/sessions/email récupère un cookie console.
  3. POST /v1/teams crée l’organisation avec un ID stable (org-sillage-sandbox).
  4. POST /v1/projects crée le projet sillage (region default — la seule acceptée en self-hosted, les régions fra/nyc sont propres à Appwrite Cloud).
  5. POST /v1/projects/{id}/keys crée une clé serveur avec les scopes minimum (users.read, users.write, sessions.write).
  6. Persistance de PUBLIC_APPWRITE_PROJECT et APPWRITE_KEY dans .env.

Les endpoints /v1/teams, /v1/projects, /v1/projects/.../keys sont ceux que la console appelle — stables sur la branche 1.x mais pas part du contrat REST public. Si une future major Appwrite réorganise tout, c’est ce script qu’il faudra patcher.

Bootstrap REDCap

bootstrap-crf.ts :

  1. Délègue l’install REDCap à pnpm -F atlas-crf-sandbox docker:install (création du schéma + projet par défaut id=1 + son token API — le projet 1 reste intact, c’est celui utilisé par les tests de contrat du crf-sandbox).
  2. INSERT SQL minimal dans redcap_projects pour créer un projet sillage dédié (auto-incremented id). Idempotent : si le projet existe déjà, on le réutilise.
  3. INSERT dans redcap_user_rights pour donner à site_admin un token API généré (16 bytes hex). ON DUPLICATE KEY UPDATE rend le step ré-entrant.
  4. Drop de la FK redcap_data_dictionaries.doc_idredcap_edocs_metadata.doc_id : l’API metadata d’import insère avec doc_id=0 (sentinelle) et violerait sinon la contrainte.
  5. Import du data dictionary data-dictionaries/136-ecrin-v2-alpha.json via POST /api/?content=metadata&action=import dans le projet sillage.
  6. Persistance de CRF_API_TOKEN (le token du projet sillage) dans .env.

Le projet par défaut id=1 (créé par install-crf.sh) reste isolé et continue de servir les tests de contrat du crf-sandbox.

Mail-trap

Le service mailpit du crf-sandbox écoute SMTP sur mailpit:1025 (dans le réseau redcap-net). Le service baas est aussi attaché à ce réseau et lit _APP_SMTP_HOST=mailpit / _APP_SMTP_PORT=1025.

Mais Appwrite n’envoie pas les emails depuis son process principal : il les pousse dans une queue Redis (utopia-queue.queue.v1-mails) consommée par un worker dédié. On déclare donc en plus baas-worker-mails (même image Appwrite, entrypoint: worker-mails) qui consomme la queue et appelle Mailpit. Sans ce service, les emails restent coincés dans Redis. Les emails sortants sont visualisables sur http://localhost:8025.

Seed fake data

seed-fake-data.ts parcourt le data dictionary et génère N records (défaut 120, paramétrable via SEED_RECORD_COUNT ou --count=N) répartis entre quatre scénarios : incomplet (20%), en cours d’avis (30%), validé (40%), refusé (10%). Le branching logic REDCap ([field]=val OR ...) est interprété pour ne remplir que les champs réellement visibles selon les autres réponses. Les valeurs sont générées par @faker-js/faker (locale fr).

Re-lancer le seed est idempotent (les record_id sont stables, REDCap upsert) — utiliser FAKER_SEED=42 pnpm seed pour un seed déterministe.

Pull depuis la prod (opt-in)

pull-from-prod.ts tire les vrais records depuis le REDCap officiel et les ré-injecte en local. Nécessite PROD_CRF_URL et PROD_CRF_TOKEN.

Pattern recommandé : mettre ces deux valeurs dans .env.prod (gitignored, persistant) plutôt que dans .env (régénéré à chaque reset). Le fichier est sourcé en plus du .env standard par tous les scripts qui en ont besoin :

Fenêtre de terminal
cp .env.prod.example .env.prod
# édite avec ton vrai URL + token
pnpm pull:prod # demande confirmation
SEED_MODE=prod pnpm start # ou via le bootstrap orchestré

Le script demande une confirmation interactive avant de pull (skip avec --yes). Les champs présents en prod mais absents en local sont droppés avec un warning. L’URL doit inclure le /api/ final (REDCap exige le path d’API).

Privacy : les records pull-és se retrouvent en clair dans ta MariaDB locale (côté REDCap). Un pnpm stop (ou pnpm start qui wipe en début de script) les efface.

Test E2E

Le smoke end-to-end est piloté par Playwright — voir tests/e2e/smoke.spec.ts. Il couvre le scénario complet : signup via la modale → poll Mailpit → visite du magic-link → création de demande via /api/v1/surveys/new → reload → assert section Compléter → logout. La suite se skip toute seule si Mailpit ou Appwrite ne sont pas joignables.

Lancer :

Fenêtre de terminal
pnpm test:smoke # headless
pnpm test:smoke:headed # avec UI

Le webServer de playwright.config.ts spawn pnpm -F sillage dev automatiquement (reuseExistingServer: true). Pre-requis stack : pnpm bootstrap joué (Appwrite + REDCap + .env sillage provisionnés).

Limites connues

  • Endpoints Appwrite privés : /v1/teams, /v1/projects, /v1/projects/.../keys ne sont pas dans le contrat REST public. Stables sur 1.x mais à re-vérifier sur futur upgrade major.
  • Appwrite minimal : on déclare l’API + MongoDB + Redis + worker-mails. Pas de traefik, ni de workers Functions/Builds/Webhooks. Suffisant pour Account/Users/Sessions utilisés par sillage, insuffisant si sillage se met à utiliser Appwrite Functions ou Webhooks.
  • Empreinte mémoire : ~3-4 Go pour le full stack.
  • Couplage réseau avec crf-sandbox : si son docker-compose.yml renomme mailpit ou redcap-net, le branchement casse.
  • Image app non hot-reload : build-once. Toute modif du code de sillage (ou d’une PUBLIC_*) impose docker compose up -d --build app. Pour itérer, préférer pnpm -F sillage dev sur l’hôte.

Convention de nommage

Les variables d’env et noms de services évitent les noms de marques tiers : CRF_* plutôt que REDCAP_* côté config sandbox, baas-* plutôt que appwrite-* côté services. Les REDCAP_* du compose crf-sandbox inclus sont laissés tels quels (out-of-scope).