commit 48b7f979544a873fedfc27c00813bb47f22dd5ad Author: Valentin Le Moign Date: Sun Jul 5 18:38:04 2026 +0200 Stack d'orchestration Figures Libres (Docker Compose) Câble ensemble le générateur (dépôt generator/) et le CMS Grav d'exemple (dépôt grav-reference/, monté :ro), tous deux clonés côte à côte et non versionnés ici. Contient : docker-compose dev + surcouche prod (rsync/SSH), image Grav (docker/grav), doc de câblage. Co-Authored-By: Claude Opus 4.8 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b555c60 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Dépôts séparés, clonés côte à côte (chacun son propre remote) : +# generator/ = l'app Nuxt = le livrable (gitea figureslibres.io) +# grav-reference/ = le CMS d'exemple = son propre dépôt, ~4,8 Go +generator/ +grav-reference/ + +# Docs : PDF de référence + spec (déjà réalisée, désormais désynchro du code). +# Le Dockerfile de l'image Grav reste versionné (docker/grav/). +docs/ + +# Divers +.DS_Store +*.log +.env diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..df1ef53 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,46 @@ +# CLAUDE.md — stack Figures Libres (orchestration) + +Dépôt d'**orchestration** de l'environnement du générateur de portfolios. Il ne +contient **pas** de code applicatif : il câble ensemble trois checkouts posés +côte à côte, via Docker Compose. + +``` +portfolio-maker/ ← CE dépôt (stack) +├── docker-compose.yml # grav-reference (8089) + generator (8091) +├── docker-compose.prod.yml # surcouche déploiement (build Nitro + rsync prod) +├── docker/grav/Dockerfile # image PHP+Apache servant grav-reference/ +├── generator/ ← dépôt À PART (l'app) — cloné ici, git-ignoré +└── grav-reference/ ← dépôt À PART (CMS d'exemple) — cloné ici, git-ignoré +``` + +## Règle absolue : tout tourne dans Docker + +Node, build Nuxt, Playwright/Chromium, Ghostscript, ffmpeg, rsync — **rien sur +l'hôte**. Exception connue : vérifier le rendu d'un PDF (`pdftoppm`) peut se faire +sur l'hôte, les templates print étant piégeux (voir `generator/CLAUDE.md`). + +```bash +docker compose up -d # grav-reference + generator +``` + +- Générateur : http://localhost:8091 — mot de passe dev `figureslibres`. +- Grav de référence : http://localhost:8089 (CMS d'exemple, **pas** l'app à développer). + +## Les trois composants + +- **`generator/`** — l'app Nuxt = le livrable. Dépôt autonome (Dockerfile inclus), + poussé sur le gitea figureslibres.io. Toute la doc app est dans `generator/CLAUDE.md`. +- **`grav-reference/`** — le vrai site Grav, **son propre dépôt** (~4,8 Go), monté + `:ro`. Source de contenu en lecture seule, jamais modifié. +- **Ce dépôt** — compose, image Grav, doc de câblage. Ne versionne ni `generator/` + ni `grav-reference/` (ils ont chacun leur dépôt). + +## Déploiement (mode prod) + +Contenu tiré de la prod par rsync/SSH en lecture seule vers +`generator/data/content-cache/`, déclenché par le bouton « Rafraîchir ». Voir les +variables `.env` (non versionné) documentées dans `docker-compose.prod.yml`. + +```bash +docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d generator +``` diff --git a/README.md b/README.md new file mode 100644 index 0000000..b76c0f9 --- /dev/null +++ b/README.md @@ -0,0 +1,97 @@ +# portfolio-maker (stack) + +Dépôt d'**orchestration** de l'environnement du générateur de portfolios PDF de +Figures Libres. Il ne contient pas de code applicatif : il câble ensemble, via +Docker Compose, l'app et le CMS Grav d'exemple qui lui sert de source de contenu. + +## Trois dépôts, un environnement + +Ce dépôt attend deux autres checkouts **posés côte à côte** (chacun a son propre +dépôt git et n'est pas versionné ici) : + +``` +portfolio-maker/ # CE dépôt (stack) +├── docker/grav/Dockerfile # Image PHP+Apache servant grav-reference/ +├── docker-compose.yml # Services grav-reference (8089) + generator (8091) +├── docker-compose.prod.yml # Surcouche déploiement (build Nitro + rsync prod) +├── generator/ ←──────── dépôt À PART : l'app Nuxt (Dockerfile inclus) = le livrable +└── grav-reference/ ←──────── dépôt À PART : le vrai site Grav (~4,8 Go), monté :ro +``` + +- **`generator/`** — le générateur (Nuxt). Détails d'architecture : voir son propre + `README.md` / `CLAUDE.md`. Poussé sur le gitea figureslibres.io. +- **`grav-reference/`** — CMS Grav d'exemple, en **lecture seule** (monté `:ro`). + C'est la source de contenu, **pas** l'app à développer. + +> ⚠️ Tout tourne dans Docker : Node, build Nuxt, Playwright/Chromium, Ghostscript, +> ffmpeg, rsync — rien n'est installé sur l'hôte. + +> 📦 **Non versionné dans ce dépôt** : `generator/` et `grav-reference/` (dépôts +> séparés, à cloner côte à côte), ainsi que `docs/` (PDF de référence + spec déjà +> réalisée). Le `Dockerfile` de l'image Grav, lui, reste versionné dans `docker/grav/`. + +## Lancer en dev + +```bash +docker compose up -d # grav-reference + generator +``` + +- Grav de référence : http://localhost:8089 +- Générateur de portfolios : http://localhost:8091 — mot de passe partagé : + `figureslibres` (variable `PORTFOLIO_PASSWORD`) + +Le générateur lit directement `grav-reference/user/pages/02.projets/` (mode DEV +de `ContentSource`). Le bouton « Rafraîchir » relit le contenu local. + +Commandes utiles (toujours via le conteneur) : + +```bash +docker compose run --rm --no-deps generator npm install # dépendances +docker compose exec generator npm run build # build de prod +docker logs -f figureslibres-portfolio-generator # logs du dev server +``` + +## Parcours utilisateur + +1. Se connecter (mot de passe partagé) — on arrive sur la liste des **portfolios**. +2. Créer un **portfolio** : ajouter des projets via la modale « Ajouter un + projet » (recherche plein texte, filtres catégorie/tag — seuls les projets + publiés sont proposés). Chaque projet = **deux pages ou plus**, fidèles au + gabarit `book_v3` (PDF de référence dans `docs/`, non versionné) : + - une page **couverture** : bandeau 55 mm couleur de la catégorie (titre en + fonte d'« utilité » — Playfair/Manifont/Avara —, pastille, présentation + Syne 12/16 pt) + image de couverture (celle de la grille du CMS) ; + - des pages **grille** : filet 5 mm, marges 8 mm, gouttières 3,8 mm, images + **entières** (jamais tronquées) calées sur la grille — 8 dispositions au + choix, présentées par icônes. Les vidéos ne sont pas sélectionnables. + Couleur de bandeau et texte noir/blanc réglables (palette du gabarit + + couleurs du site). Pages spéciales : couverture, sommaire, page libre. + Aperçu WYSIWYG A3 paysage, sauvegarde automatique. +3. **Générer le PDF** (nom de fichier personnalisable) : rendu Playwright/Chromium + au format **A3 paysage** (1190,55 × 841,89 pt, comme le gabarit), images + redimensionnées à 2× leur taille d'affichage (sharp), post-traitement + Ghostscript, métadonnées. Le contenu utilisé est **figé** + (`data/frozen//`) : régénérer redonne le même PDF ; + « Réactualiser depuis Grav » resynchronise volontairement. + +## Déploiement (mode PROD) + +Le contenu est tiré de la prod par **rsync/SSH en lecture seule** vers +`generator/data/content-cache/`, déclenché par le bouton « Rafraîchir ». + +```bash +# .env (non versionné) +PORTFOLIO_PASSWORD=… +SESSION_SECRET=… +INTERNAL_TOKEN=… +SYNC_REMOTE=user@prod:/chemin/vers/user/pages/02.projets/ +SYNC_SSH_KEY_FILE=/chemin/local/vers/cle_privee + +docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d generator +``` + +## Données + +Tout est sur disque dans `generator/data/` (sauvegarde = copie du dossier) : +`portfolios/*.json` (compositions), `exports/*.pdf`, `frozen//` (snapshots), +`cache/` (miniatures, posters vidéo — régénérable), `content-cache/` (rsync prod). diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..40dea28 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,23 @@ +# Surcouche de déploiement (spec §5.2) : +# docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d generator +# +# - contenu tiré de la prod Figures Libres par rsync/SSH (bouton « Rafraîchir ») +# - build Nuxt puis serveur Nitro (pas de serveur de dev) +# - variables à définir dans un fichier .env (non versionné) : +# PORTFOLIO_PASSWORD=… mot de passe partagé +# SESSION_SECRET=… secret de session (longue chaîne aléatoire) +# INTERNAL_TOKEN=… jeton interne du rendu print +# SYNC_REMOTE=user@host:/chemin/vers/user/pages/02.projets/ +# SYNC_SSH_KEY_FILE=~/.ssh/portfolio_sync clé privée SSH (lecture seule) +services: + generator: + command: sh -c "npm run build && npm run preview" + environment: + NUXT_CONTENT_SOURCE: prod + NUXT_SYNC_REMOTE: ${SYNC_REMOTE:?SYNC_REMOTE requis en mode prod} + NUXT_SYNC_SSH_KEY: /run/secrets/sync-ssh-key + NITRO_HOST: 0.0.0.0 + NITRO_PORT: 3000 + volumes: + - ./generator:/app + - ${SYNC_SSH_KEY_FILE:?SYNC_SSH_KEY_FILE requis en mode prod}:/run/secrets/sync-ssh-key:ro diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4fbfe58 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,46 @@ +services: + # Instance Grav de référence (site "Figures Libres") servant de source de + # contenu pour le développement du générateur de portfolios. + # Ce n'est PAS l'app à développer : c'est le CMS d'exemple, en lecture. + grav-reference: + build: + context: ./docker/grav + args: + PUID: "1000" + PGID: "1000" + container_name: figureslibres-grav-ref + restart: unless-stopped + ports: + - "8089:80" + volumes: + - ./grav-reference:/var/www/html + + # Générateur de portfolios PDF (app Nuxt) — voir docs/spec-generateur-portfolios.md. + # Tout tourne dans ce conteneur : Node, build Nuxt, Playwright/Chromium, Ghostscript. + generator: + build: + # L'app est un dépôt autonome cloné dans ./generator (Dockerfile inclus). + context: ./generator + image: figureslibres-portfolio-generator + container_name: figureslibres-portfolio-generator + restart: unless-stopped + user: "1000:1000" + working_dir: /app + command: npm run dev + environment: + HOME: /tmp + NUXT_SESSION_SECRET: ${SESSION_SECRET:-dev-secret-figures-libres} + NUXT_PORTFOLIO_PASSWORD: ${PORTFOLIO_PASSWORD:-figureslibres} + NUXT_INTERNAL_TOKEN: ${INTERNAL_TOKEN:-dev-internal-token} + NUXT_CONTENT_SOURCE: ${CONTENT_SOURCE:-dev} + NUXT_CONTENT_DIR: /content/02.projets + NUXT_DATA_DIR: /app/data + # Mode prod (voir docker-compose.prod.yml) : synchro rsync/SSH + NUXT_SYNC_REMOTE: ${SYNC_REMOTE:-} + NUXT_SYNC_SSH_KEY: ${SYNC_SSH_KEY:-} + ports: + - "8091:3000" + volumes: + - ./generator:/app + # Contenu Grav monté en LECTURE SEULE (contrainte ferme, §18). + - ./grav-reference/user/pages/02.projets:/content/02.projets:ro diff --git a/docker/grav/Dockerfile b/docker/grav/Dockerfile new file mode 100644 index 0000000..a94b00a --- /dev/null +++ b/docker/grav/Dockerfile @@ -0,0 +1,47 @@ +FROM php:8.2-apache + +# Match the container's web user to the host user so Grav can write to the +# bind-mounted app/ folder (cache, logs, tmp, user uploads, ...). +ARG PUID=1000 +ARG PGID=1000 +RUN groupmod -o -g "${PGID}" www-data \ + && usermod -o -u "${PUID}" www-data + +# System deps for Grav's PHP extensions +RUN apt-get update && apt-get install -y --no-install-recommends \ + libzip-dev \ + libpng-dev \ + libjpeg-dev \ + libfreetype6-dev \ + libcurl4-openssl-dev \ + libonig-dev \ + libxml2-dev \ + unzip \ + && docker-php-ext-configure gd --with-freetype --with-jpeg \ + && docker-php-ext-install -j"$(nproc)" \ + gd \ + curl \ + mbstring \ + zip \ + dom \ + xml \ + fileinfo \ + exif \ + opcache \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Grav ships its own .htaccess; enable rewrite + allow overrides +RUN a2enmod rewrite headers + +RUN sed -ri -e 's!AllowOverride None!AllowOverride All!g' /etc/apache2/apache2.conf + +# Sensible PHP settings for Grav +RUN { \ + echo 'memory_limit=256M'; \ + echo 'upload_max_filesize=32M'; \ + echo 'post_max_size=32M'; \ + echo 'max_execution_time=120'; \ + } > /usr/local/etc/php/conf.d/grav.ini + +WORKDIR /var/www/html