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
| Service | URL | Rôle |
|---|---|---|
| sillage (app) | http://localhost:5173 | L’app SvelteKit dockerisée (opt-in) |
| BaaS API (Appwrite) | http://localhost:8090 | Sessions, comptes utilisateurs |
| BaaS Console | http://localhost:8091 | Admin UI Appwrite (SPA séparée) |
| CRF (REDCap) | http://localhost:8888 | Source des demandes sillage |
| phpMyAdmin | http://localhost:8889 | Accès DB CRF (compte redcap) |
| Mailpit (UI) | http://localhost:8025 | Capture tous les emails (magic-link inclus) |
| Mailpit (SMTP) | localhost:1025 | Endpoint 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 composev2.20+ - pnpm + Node ≥ 24 (pour les scripts
tsx) - 4 Go de RAM libre minimum
Démarrage zéro-clic
cd sandbox/sillage-sandboxpnpm startC’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 :
| Var | Valeurs | Effet |
|---|---|---|
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. |
prod | Force le pull des vrais records depuis PROD_CRF_URL / PROD_CRF_TOKEN | |
fake | Force 120 records synthétiques via @faker-js/faker | |
none | Ne pré-remplit pas le projet | |
SKIP_E2E | 1 | Saute 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 :
cd ../../apps/sillagepnpm devOuvrir 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.
# 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'appopen http://localhost:5173Notes importantes :
- Build depuis la racine du monorepo. Le
build.contextdu service est../..: le build pnpm a besoin dupnpm-lock.yaml, dupnpm-workspace.yamlet despackage.jsondes paquets workspace (@univ-lehavre/atlas-*) dont sillage dépend. LeDockerfileest multi-stage (builder lourd → runner Alpine minimal, user non-rootnode). - 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 enbuild.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_TOKENetALLOWED_DOMAINS_REGEXP($env/static/private) sont lues à l’exécution par l’adapter Node ; elles arrivent parenvironment:depuis.env. Aucun secret n’est figé dans l’image. Lance doncpnpm bootstrapavantup app, sinonAPPWRITE_KEY/CRF_API_TOKENsont 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. Lepackage.jsondu sandbox ne référence pas@univ-lehavre/atlas-sillage. - Quand préférer quoi ?
pnpm -F sillage devpour itérer (hot-reload). Le serviceappdockerisé pour valider le build de prod ou démarrer la stack entière d’un coup.
Commandes
| Commande | Effet |
|---|---|
pnpm start | Wipe + docker:up + bootstrap + smoke en un coup (zero-touch) |
pnpm stop | Arrête les conteneurs et efface les volumes anonymes |
pnpm docker:up | Démarre tous les conteneurs (BaaS + CRF + Mailpit) |
pnpm docker:down | Arrête les conteneurs et efface les volumes anonymes |
pnpm docker:logs | Tail des logs |
pnpm bootstrap | Orchestrateur complet (BaaS + CRF + seed + .env sillage) |
pnpm bootstrap:baas | Provisionne Appwrite (account root + org + projet + clé API) |
pnpm bootstrap:crf | Installe REDCap, crée un projet sillage dédié, importe la trame |
pnpm seed | Génère et importe N records synthétiques (défaut 120, voir .env) |
pnpm pull:prod | Pull opt-in des records de prod (nécessite PROD_CRF_* dans .env) |
pnpm test:smoke | Playwright smoke level-5 ; auto-spawn d’sillage dev via webServer |
Les commandes
up/downsont préfixéesdocker:parce quepnpm upest 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 dansdocker-compose.yaml. Unpnpm stop(oudocker compose down --volumes) les efface — donc chaquepnpm startrepart 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 :
POST /v1/accountcrée le compte root (sur une install fraîche, le premier compte est promu root automatiquement). Si le compte existe déjà, on continue.POST /v1/account/sessions/emailrécupère un cookie console.POST /v1/teamscrée l’organisation avec un ID stable (org-sillage-sandbox).POST /v1/projectscrée le projetsillage(regiondefault— la seule acceptée en self-hosted, les régionsfra/nycsont propres à Appwrite Cloud).POST /v1/projects/{id}/keyscrée une clé serveur avec les scopes minimum (users.read,users.write,sessions.write).- Persistance de
PUBLIC_APPWRITE_PROJECTetAPPWRITE_KEYdans.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
- 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). - INSERT SQL minimal dans
redcap_projectspour créer un projetsillagedédié (auto-incremented id). Idempotent : si le projet existe déjà, on le réutilise. - INSERT dans
redcap_user_rightspour donner àsite_adminun token API généré (16 bytes hex).ON DUPLICATE KEY UPDATErend le step ré-entrant. - Drop de la FK
redcap_data_dictionaries.doc_id→redcap_edocs_metadata.doc_id: l’API metadata d’import insère avecdoc_id=0(sentinelle) et violerait sinon la contrainte. - Import du data dictionary
data-dictionaries/136-ecrin-v2-alpha.jsonviaPOST /api/?content=metadata&action=importdans le projet sillage. - 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 :
cp .env.prod.example .env.prod# édite avec ton vrai URL + tokenpnpm pull:prod # demande confirmationSEED_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 :
pnpm test:smoke # headlesspnpm test:smoke:headed # avec UILe 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/.../keysne 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.ymlrenommemailpitouredcap-net, le branchement casse. - Image
appnon hot-reload : build-once. Toute modif du code de sillage (ou d’une PUBLIC_*) imposedocker compose up -d --build app. Pour itérer, préférerpnpm -F sillage devsur 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).