TQ-Slator/compose.yaml
Stephan Morand 9025c64c0b Socle complet de TQ-Slator : éditeur, API, CLI, administration
Système de gestion de traductions pensé en CMS headless : un back-office
pour ceux qui traduisent, une API pour ce qui consomme.

Architecture
- Symfony 7.4 / API Platform 4.3 / MariaDB 11.4, SPA React 19 servie en
  même origine — ce qui rend viable le cookie de session plutôt qu'un
  jeton en localStorage.
- Deux APIs séparées : Management (session ou clé) et Delivery
  (stateless, clé seule). Les fusionner ferait porter à chaque lecture de
  bundle le coût de la session.
- Stockage canonique en ICU MessageFormat, sérialisation par plateforme.
  Le format d'une plateforme ne contamine pas la base.
- Publication par releases immuables ; le déploiement est un déplacement
  de pointeur, donc le rollback aussi.
- Isolation multi-organisation par filtre Doctrine, avec un test
  d'architecture qui casse la CI si une entité échappe à l'invariant.

Éditeur, deux vues
- Par langue : source et cible, jamais douze colonnes. Grille virtualisée,
  saisie sans bouton « Enregistrer », panneau de contexte permanent.
- Par clé : une clé, toutes ses langues empilées et repliées. Répond à
  « ce libellé est-il prêt partout ? ».
- Mode Focus dans les deux : une file à vider, ⌘↵ pour enchaîner.
- Le traducteur ne voit jamais d'ICU : pastilles de variables, un champ
  par catégorie CLDR de la langue cible.

Administration
- Deux niveaux : projet (membres, clés API, plateformes) et organisation
  (annuaire des comptes, création de projets).
- Invitations par e-mail, jeton 256 bits stocké haché.
- Désactiver un compte coupe les sessions en cours, pas seulement les
  connexions suivantes.
- Les plateformes s'archivent ; ni elles ni les environnements ne se
  suppriment — la trace explique pourquoi telle clé existe.

CLI tqs
- PHAR autonome de 3 Mo, autoloader généré : le dépôt client ne dépend
  ni de Composer ni de la disponibilité de TQ-Slator.
- init / push / pull / status ; le sync est non destructif par défaut et
  son prune est scopé plateforme.

126 tests, PHPStan niveau 8.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 08:16:05 +02:00

119 lines
4.2 KiB
YAML

services:
php:
build:
context: .
target: app_dev
depends_on:
database:
condition: service_healthy
cache:
condition: service_started
environment:
SERVER_NAME: ':80'
# APP_ENV n'est PAS répété ici : le Dockerfile le positionne déjà, et le
# redéclarer dans Compose en ferait une variable que PHPUnit ne peut pas
# surcharger — `bin/phpunit` tournerait alors en environnement dev.
#
# Ce service est le seul à appliquer les migrations (voir entrypoint.dev.sh).
RUN_MIGRATIONS: '1'
DATABASE_URL: 'mysql://tqslator:tqslator@database:3306/tqslator?serverVersion=11.4.0-MariaDB&charset=utf8mb4'
REDIS_URL: 'redis://cache:6379'
MAILER_DSN: 'smtp://mailer:1025'
BACK_OFFICE_URL: 'http://localhost:8080'
# Caddy tente sinon d'obtenir un certificat pour un domaine public.
CADDY_GLOBAL_OPTIONS: 'auto_https off'
ports:
- '8080:80'
volumes:
- .:/app
- app_storage:/app/var/storage
restart: unless-stopped
# Consommateur Messenger : statistiques de complétion, construction des bundles,
# envoi des invitations. Séparé du serveur web pour que la publication d'une
# release ne bloque jamais une requête HTTP.
worker:
build:
context: .
target: app_dev
depends_on:
database:
condition: service_healthy
environment:
DATABASE_URL: 'mysql://tqslator:tqslator@database:3306/tqslator?serverVersion=11.4.0-MariaDB&charset=utf8mb4'
REDIS_URL: 'redis://cache:6379'
MAILER_DSN: 'smtp://mailer:1025'
BACK_OFFICE_URL: 'http://localhost:8080'
volumes:
- .:/app
- app_storage:/app/var/storage
command: ['php', 'bin/console', 'messenger:consume', 'async', '--time-limit=3600', '-vv']
# Le healthcheck hérité de l'image de base interroge l'endpoint admin de
# Caddy, que ce service ne fait pas tourner : il échouerait toujours.
#
# On ne le remplace pas. Le consommateur est PID 1 du conteneur : s'il meurt,
# le conteneur sort et `restart` le relance. Une sonde ne ferait que dupliquer
# ce que Docker sait déjà, au prix d'un paquet supplémentaire dans l'image
# (pgrep n'y est pas). Aucun service ne dépend de la santé du worker.
healthcheck:
disable: true
restart: unless-stopped
database:
image: mariadb:11.4
environment:
MARIADB_DATABASE: tqslator
MARIADB_USER: tqslator
MARIADB_PASSWORD: tqslator
MARIADB_ROOT_PASSWORD: root
command:
# Voir docs/01-architecture-proposal.md §3.6.
# La collation serveur doit correspondre à default_table_options de
# doctrine.yaml, sinon les tables créées hors migration divergent en silence.
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_uca1400_ai_ci
# Nécessaire pour les index UNIQUE larges (row format DYNAMIC).
- --innodb-default-row-format=dynamic
# Journalise les requêtes de plus de 200 ms sans index : c'est le filet qui
# attrapera la requête de grille avant qu'elle ne parte en production.
- --slow-query-log=1
- --long-query-time=0.2
- --log-queries-not-using-indexes=1
healthcheck:
test: ['CMD', 'healthcheck.sh', '--connect', '--innodb_initialized']
interval: 5s
timeout: 5s
retries: 20
start_period: 30s
ports:
- '3306:3306'
volumes:
- db_data:/var/lib/mysql
# Exécuté une seule fois, au premier démarrage sur un volume vierge.
- ./docker/mariadb/init.sql:/docker-entrypoint-initdb.d/10-test-database.sql:ro
restart: unless-stopped
cache:
image: redis:7-alpine
command: ['redis-server', '--save', '', '--appendonly', 'no']
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
interval: 5s
timeout: 3s
retries: 10
restart: unless-stopped
# Capture tous les e-mails sortants (invitations, notifications).
# Interface sur http://localhost:8025
mailer:
image: axllent/mailpit:latest
environment:
MP_SMTP_AUTH_ACCEPT_ANY: 1
MP_SMTP_AUTH_ALLOW_INSECURE: 1
ports:
- '8025:8025'
restart: unless-stopped
volumes:
db_data:
app_storage: