Case study · warhammer40k

Warhammer 40K Codex

Codex numérique fan, lore-first et image-dominant. Angular 19 custom gothique, NestJS 11, stockage JSON, Anthropic SDK pour quelques générations narratives. 100% du code écrit en pair-programming avec Claude Code.

Pourquoi ce projet

Une encyclopédie Warhammer 40K il en existe déjà des dizaines — Lexicanum, Fandom, Omnis Bibliotheca. L'objectif n'était pas de les battre sur l'exhaustivité, mais de proposer une expérience lore-first AAA gothique : maximum d'images, sidebar permanente, cards image-first, polices Cinzel dorées sur fond presque noir. Pas un dashboard data, une lecture immersive. Le projet sert aussi de terrain pour stresser des patterns Claude Code complexes — scraping multi-sources, calibration manuelle d'une carte galactique, hiérarchies de données profondes.

Architecture

Architecture
Loading diagram…
Frontend Angular ↔ NestJS ↔ JSON files + Anthropic SDK + sources externes (wiki, Lexicanum, Reddit)

Le backend lit les JSON au constructor (pattern readFileSync sur data/factions.json, units.json, subfactions.json, etc.). Pas de base de données — le bind-mount Docker data/warhammer/ sur le NAS permet de patcher le contenu en prod via scp + docker compose restart, sans rebuild d'image.

Stack & data flow

  • Frontend — Angular 19 standalone, RxJS 7 + Signals, SCSS pur custom (zéro mat-form-field sur les filtres : tout est en pills gothiques, tokens --gold #c9a24a / --red #7b1113 / --bg #050403).
  • Backend — NestJS 11, Anthropic SDK 0.91 pour générer les descriptions narratives unités/séries (max 1024 tokens, claude-sonnet-4-6). Endpoint POST /api/units/:id/description avec PIN guard.
  • Imagerie unit-detail — datasheet locale prioritaire (backend/public/datasheets/<unit-id>.jpg, 119/133 unités) avec HEAD-check côté frontend, fallback wiki Fandom via /api/wiki-image?q=.
  • Galerie — 1468 images persos montées en read-only depuis /volume2/photo/Jeux/Warhammer 40K, plus image-meta.json pour les catégorisations multi-tags user, plus modal d'import (Wiki Fandom / Reddit / URL directe) qui pousse dans data/warhammer/imported/.
  • Live — SSE /api/events pour pousser les changements de solde Claude et les invalidations de cache côté front.
  • Déploiement — Docker compose multi-stage (node:20-alpinenginx:alpine) sur NAS Synology 1821+, http://nas:4201.

Le process avec Claude Code

0 ligne de code humaine sur l'app. La boucle a été : je décris ce que je veux (souvent à partir d'un mockup PNG ou d'une spec textuelle rangée dans UX/), Claude génère / refactor / teste, je valide ou course-corrige. Quelques patterns qui ont vraiment compté :

  • CLAUDE.md projet + mémoires user-level — un CLAUDE.md de 130 lignes au root du repo (architecture, conventions code, anti-patterns, pièges connus, identité visuelle) chargé à chaque session, plus une vingtaine de fichiers mémoire user-level (warhammer_design.md, warhammer_imagery.md, warhammer_lore_sources.md, etc.) qui consolident les décisions UX figées et les sources textuelles préférées (Omnis Bibliotheca FR > Lexicanum EN > Fluff Bible PDF).
  • Pipeline OpenCV pour la galaxy map — la carte canon GW (3740×2300 px, 213 labels) a été parsée par un script Python dédié scripts/detect_planets.py : 2 passes contraste local (texte sur fond noir / sur fond rouge pour les rifts warp), fusion multi-lignes avec contraintes strictes (vertical_gap_max=6, x_overlap_min=0.65), OCR Tesseract --psm 6 avec upscale 4×, plus détection des battle markers rouges via circularité > 0.55 pour caler les coords canon de Cadia, Vigilus, Pisina. 24 markers placés au pixel près par OCR.
  • Calibration Leaflet pixel-image-space — pour les ~40 markers que l'OCR ne couvre pas, mode 🎯 Calibration intégré à la page lore-galaxy : L.Marker draggables en CRS.Simple, dragend qui logge et copie en clipboard un JSON { id: { cx, cy } }, patch en batch via regex Python sur lore-galaxy.component.ts. Idempotent — un re-patch ne casse rien.
  • Éditeur SVG Bézier pour les Segmenta — après cinq tentatives ratées d'extraire les frontières des cinq Segmenta (k-means, sliding-window, OpenCV contours), pivot vers un éditeur HTML/SVG interactif scripts/segmenta-editor.html avec drag de vertices, anchors quadratiques, snap-to-Terra (rayons 5°, cercles concentriques). Cinq polygones tracés en ~10 minutes, export JSON, 24 samples par courbe Bézier réinjectés dans le code.
  • Scraping multi-sources — Lexicanum (MediaWiki anglais sans Cloudflare) pour les 71 chapitres successeurs Space Marines, Omnis Bibliotheca pour le texte FR, PDF Fluff Bible local pour le canon, le tout reformulé via Claude. Sub-agents lancés en parallèle quand l'enrichissement portait sur plusieurs factions indépendantes.

Pivots & lessons learned

  • Sectors abandonnés — l'idée d'ajouter un niveau "secteur" entre Segmentum et planète a été tentée puis coupée. Pas de sources canon cohérentes, valeur ajoutée UX faible, complexité de modèle non négligeable. Décision figée le 8 mai 2026, mémoire dédiée pour éviter qu'on ne le relance.
  • Galaxy Bézier a tenu — à l'inverse, les Segmenta tracés à la main via éditeur SVG fonctionnent bien : 130 / 52 / 105 / 158 / 131 samples par Segmentum, toggle ON par défaut. Outil interactif > algos de détection automatique sur ce type de tâche géométrique.
  • Single-screen ne marche pas sur les pages détail — tentative initiale en mode "tout dans le viewport sans scroll" sur faction-detail : overflow horizontal, UX cassée. Les pages détail scrollent, point. Seul le dashboard reste single-screen.
  • Filtres Material 19 retirés — les pills custom gothiques (border doré 28% alpha, fond noir 42%, font-weight 900, uppercase letter-spacing .08em) battent mat-select sur ce design. Material reste pour les composants utilitaires (snackbar, dialog) mais a disparu des filtres.
  • Lore feed STATIQUE — première version générait des phrases d'ambiance via Claude au load. Coût crédits élevé pour un gain négligeable. Maintenant lore-feed.json avec dix entrées hardcodées, endpoint qui retourne trois random.
  • Seed JSON masqué par bind-mount — au premier lancement sur le NAS, le bind-mount data/warhammer écrase le /app/data baked dans l'image et le backend plante en boucle (ENOENT factions.json). Solution : cp -n backend/seed/*.json data/warhammer/ au bootstrap. Documenté dans le CLAUDE.md projet pour ne plus jamais le re-découvrir.