TCG integrations

Ajouter un nouveau TCG

Ajouter un nouveau TCG

Dernière mise à jour : 2026-05-10

Page critique — procédure complète pour intégrer un TCG inédit (ex. Pokemon, Yu-Gi-Oh!, Star Wars Unlimited, Sorcery Contested Realm).

Référence : skill global add-tcg listé dans ~/.claude/skills/add-tcg/. Ce skill fournit une checklist 6-axes ; cette page détaille les fichiers + snippets à toucher.

Vue d'ensemble : 6 axes

Axe But Fichiers principaux
1 Cartes (data + ingestion) services/cards/sources/<tcg>.ts, scripts/<tcg>-cards/
2 Symboles UI inline frontend/src/lib/<tcg>-symbols.ts, frontend/public/<tcg>-icons/
3 Decompose-query (synergy/deckbuilding) services/rag/retrieve/multi-query-<tcg>.ts
4 Deckbuilding (pool + spec) services/rag/deckbuilding/{spec,pool,validate}.ts
5 Set matching services/cards/set-aliases.ts
6 Méta (tier list + tournois) services/meta/<tcg>.ts, cron/meta-sync.ts

Tous les axes ne sont pas obligatoires. Pour un TCG sans format constructed (ex. jeu de société), les axes 4 et 6 ne s'appliquent pas.


Axe 1 — Cartes (data + ingestion)

1.1 Choisir la source

1.2 Créer la source

src/services/cards/sources/<tcg>.ts doit implémenter l'interface CardSource :

import { CardSource } from './types.js';

export const myTcgSource: CardSource = {
  collection: 'mytcg-cards',  // doit matcher le `hasCardDatabase` qu'on assignera aux jeux
  
  async load() {
    // Lit la source (file, API, package) → renvoie { cards: NormalizedCard[], hash: string }
    // Le hash sert au cards-sync pour détecter les changements
  },
  
  normalizeCard(rawCard) {
    // Transforme le format source → schema interne (Qdrant payload)
    return {
      id: ...,
      name: ...,
      // … champs communs + spécifiques au TCG
    };
  },
  
  getImageUrl(card) {
    // CDN éditeur si dispo, sinon chemin local
    return card.image_url ?? `/cards/<tcg>/${card.id}.png`;
  },
};

1.3 Enregistrer dans le registry

src/services/cards/sources/registry.ts :

import { myTcgSource } from './<tcg>.js';

export const cardSources = new Map([
  ['magic-cards', magicSource],
  ['lorcana-cards', lorcanaSource],
  // ...
  ['mytcg-cards', myTcgSource],   // ← ajouter ici
]);

⚠️ Sans cet enregistrement, cards-cache.ts ne load pas la collection.

1.4 Scripts d'ingestion

Modèle à dupliquer : scripts/riftbound-cards/. Crée :

scripts/<tcg>-cards/
├── fetch-cards.ts       # Télécharge / API call → cache JSON
├── ingest.ts            # Push Qdrant via cards-sync
├── link-game.ts         # CLI pour lier un jeu à hasCardDatabase = '<tcg>-cards'
└── (test-retrieve.ts)   # Optionnel : debug retrieval

1.5 Commandes npm

package.json :

{
  "scripts": {
    "cards:<tcg>:fetch": "tsx --env-file=.env scripts/<tcg>-cards/fetch-cards.ts",
    "cards:<tcg>:ingest": "tsx --env-file=.env scripts/<tcg>-cards/ingest.ts",
    "cards:<tcg>:link-game": "tsx --env-file=.env scripts/<tcg>-cards/link-game.ts"
  }
}

1.6 Variables d'env

⚠️ 3 fichiers en parallèle :

  1. src/config.ts :
    MY_TCG_CARDS_DATA_DIR: z.string().default('/app/data/<tcg>-cards'),
    
  2. .env.example : MY_TCG_CARDS_DATA_DIR=/app/data/<tcg>-cards
  3. unraid/boardgame-referee.xml : ajouter le <Config Type="Variable">... correspondant

Sinon l'admin Unraid ne pourra pas la setter via l'UI.

1.7 Vérifier

docker exec boardgame-referee npm run cards:<tcg>:fetch
docker exec boardgame-referee npm run cards:<tcg>:ingest
# /admin/services doit montrer la collection avec count > 0

Axe 2 — Symboles UI inline

2.1 Récupérer les PNG officiels

Toujours vérifier le CDN officiel d'abord — FAB/MTG/Riftbound ont tous les PNG officiels sur le site de règles ou press-kit (cf. mémoire feedback_tcg_official_icons_cdn.md).

Stocker dans frontend/public/<tcg>-icons/.

2.2 Helper de remplacement

Modèle : frontend/src/lib/riftbound-symbols.ts. Crée <tcg>-symbols.ts :

const SYMBOL_RE = /\{([rpdhicut])\}/g;  // whitelist STRICTE — sinon collision avec d'autres TCG

export function replaceMyTcgTokens(html: string): string {
  return html.replace(SYMBOL_RE, (_, key) => {
    return `<img src="/<tcg>-icons/${key}.png" class="card-symbol" alt="${key}">`;
  });
}

export const myTcgEnabled = true;

⚠️ Regex whitelist obligatoire : \{R\} MTG (majuscule) collisione avec \{r\} FAB (minuscule). La regex doit cibler exactement les tokens du TCG en cours, jamais plus large.

2.3 Hook dans le markdown

frontend/src/composables/useArbiterMarkdown.ts :

import { replaceMyTcgTokens, myTcgEnabled } from '../lib/<tcg>-symbols.js';

// Dans la fonction principale, AVANT `renderHtml` :
if (game.value?.hasCardDatabase === '<tcg>-cards' && myTcgEnabled) {
  html = replaceMyTcgTokens(html);
}

2.4 Vérifier

Poser une question dont la réponse contient un symbole. Vérifier le rendu image dans la bulle Oracle (DevTools → inspecter le DOM, vérifier que <img> apparaît).


Axe 3 — Decompose-query (intent synergy / deckbuilding)

3.1 Multi-query TCG-specific

Modèle : src/services/rag/retrieve/multi-query-mtg.ts. Crée multi-query-<tcg>.ts :

export async function decomposeMyTcgQuery(question: string): Promise<MyTcgSpec | null> {
  const prompt = `... (prompt Haiku qui sort un JSON structuré pour ce TCG)`;
  // Appel Haiku via promptStream avec timeout DECOMPOSE_TIMEOUT_MS
  // Parse JSON, valide via Zod
  // Retourne null si parse fail (le retrieval continuera sans filters spécifiques)
}

Le MyTcgSpec est un type Zod avec les champs structuraux du TCG (couleurs, format, hero, types, etc.).

3.2 Dispatch dans synergy-expansion.ts

switch (game.hasCardDatabase) {
  case 'magic-cards':
    spec = await decomposeMtgQuery(question);
    break;
  case 'mytcg-cards':
    spec = await decomposeMyTcgQuery(question);  // ← ajouter
    break;
  // …
}

3.3 Vérifier

Poser une question synergy spécifique au TCG ("liste-moi les de couleur <X>"). Vérifier dans /admin/feedback/<id> que les filters Qdrant matchent la décomposition.


Axe 4 — Deckbuilding (pool + spec)

4.1 Spec defaults

src/services/rag/deckbuilding/spec.ts : ajouter une entrée dans le DEFAULT_SPECS_BY_TCG :

'mytcg-cards': {
  format: 'standard',
  mainboard: 60,
  sideboard: 15,
  maxCopies: 3,
  // selon le TCG : runes, battlefields, equipment, etc.
}

4.2 Pool éligible

src/services/rag/deckbuilding/pool.ts : étendre fetchEligibleCards avec le filtre Qdrant approprié :

case 'mytcg-cards':
  filter = {
    must: [{ key: 'card_<tcg>_legal_formats', match: { value: spec.format } }],
  };
  break;

Si le TCG n'a pas de legal_formats (collection mono-format), pas de filter Qdrant — fais un post-filtrage TS sur les champs structurels (couleurs, héro, etc.).

4.3 Validation

src/services/rag/deckbuilding/validate.ts : ajouter les contraintes spécifiques :

4.4 Vérifier

Poser "fais-moi une decklist autour de <archétype>". Vérifier que le total + max copies sont respectés et que les anchor cards sont incluses.


Axe 5 — Set matching

src/services/cards/set-aliases.ts : ajouter les alias FR/EN du nouveau TCG :

export const SET_ALIASES = new Map([
  // MTG
  ['Strixhaven', 'STX'],
  // … existants
  
  // <tcg>
  ['Surge', 'SUR'],
  ['Nouveau set FR', 'NSF'],
]);

Permet à l'utilisateur de citer un set par son nom complet, le filtre Qdrant s'applique sur le code 3 lettres.

Vérifier

Poser une question qui mentionne un set par son nom complet. Vérifier le filter dans /admin/feedback/<id>.


Axe 6 — Méta (tier list + tournois)

6.1 Source méta stable

Identifier une source qui :

6.2 Service méta

Modèle : services/meta/mobalytics-riftbound.ts (scrape) ou services/meta/mtgtop8.ts (scrape avec rate-limit). Crée services/meta/<tcg>.ts :

export async function syncMyTcgMeta(): Promise<MetaSnapshot> {
  // 1. Fetch / scrape la source
  // 2. Transformer en MetaSnapshot { tier_list, tournament_decks }
  // 3. Retourner pour ingest via services/meta/ingest.ts
}

6.3 Cron meta-sync

src/cron/meta-sync.ts : ajouter l'appel au sync à la fréquence configurée :

if (config.META_MY_TCG_ENABLED) {
  await ingestSnapshot(await syncMyTcgMeta());
}

6.4 Variables d'env méta

Encore une fois, 3 fichiers en parallèle :

META_MY_TCG_ENABLED=false
META_MY_TCG_RATE_LIMIT_MS=1500
META_MY_TCG_USER_AGENT=Mozilla/5.0 (compatible; ...)
# … selon le TCG

6.5 Vérifier

docker exec boardgame-referee npm run meta:<tcg>:sync

Vérifier que des chunks [META] apparaissent dans Qdrant et sortent dans une question méta.


Verification end-to-end

  1. Créer un jeu via /add-game (ou npm run cards:<tcg>:link-game) avec has_card_database = '<tcg>-cards'
  2. Tester autocomplete @<carte> → cartes ressortent
  3. Tester citation [[card:<Nom>]] dans une réponse Oracle → bouton zoom OK
  4. Tester intent synergy → diagnostics montrent les filters TCG
  5. Tester intent deckbuilding → decklist respecte spec (total + max copies + anchors)
  6. Tester import deck (si applicable) → mapping deck → stickyCardMentions

Pièges classiques

Piège Conséquence Fix
Oublier d'enregistrer dans registry.ts Cards-cache ne load pas la collection Ajouter dans cardSources
Oublier additionalDirectories dans le settings.json oracle Vision Read échoue Ajouter dans le settings.json + permissions.allow
process.env.X dans une route au lieu de config.X Silencieusement vide en prod Centraliser dans src/config.ts
Regex symboles trop large Collision avec un autre TCG Whitelist stricte caractère par caractère
Oublier unraid/boardgame-referee.xml Admin Unraid ne peut pas setter la var Mettre à jour les 3 fichiers (config.ts + .env.example + xml)
Forget link-game après ingest Le jeu n'a pas hasCardDatabase Lancer npm run cards:<tcg>:link-game <gameId>
Bundlée image (FAB-style) sans rebuild Resync ne voit pas la nouvelle data Build + redeploy AVANT resync

Aperçu des TCG supportés

Aperçu des TCG supportés

Dernière mise à jour : 2026-05-10

6 TCG intégrés. Chaque TCG a une page dédiée dans ce chapitre. Workflow de mise à jour des cartes : voir mettre-a-jour-cartes.md. Workflow d'ajout d'un nouveau TCG : voir ajouter-un-tcg.md.

Tableau récap

TCG Collection Qdrant Source data Type source Fréquence maj Symboles UI Deckbuilding Méta
Magic: The Gathering magic-cards Scryfall bulk JSON + traduction Haiku Téléchargée À chaque set Standard (~3-4×/an) mana-font (webfont) ✅ 17Lands + MTGGoldfish + MTGTop8
Disney Lorcana lorcana-cards LorcanaJSON.org Téléchargée (MIT) À chaque set PNG Ravensburger ⚠️ partiel ❌ DotGG figée nov 2025
Flesh and Blood flesh-and-blood-cards Package npm @flesh-and-blood/cards Bundlée image Docker À chaque set PNG LSS ✅ fabtcg.com tournois
Riftbound riftbound-cards API Riot card-gallery Live API À chaque set PNG Riot press-kit ✅ Mobalytics + RiftboundStats
Terraforming Mars terraforming-mars-cards HTML parsing + cards.json Statique locale Quasi-statique (cards.json) ❌ pas de format
Ark Nova ark-nova-cards JSON + sprites Statique locale Quasi-statique (sprites locales)

Caractéristiques transverses

Côté backend

Côté frontend

Côté RAG

Côté méta

Limites actuelles

Ark Nova

Ark Nova

Dernière mise à jour : 2026-05-10

Jeu de société (zoo), pas un TCG. Traité avec base de cartes pour l'autocomplete @ et l'enrichissement RAG.

Source

Code

Fichier Rôle
src/services/cards/sources/ark-nova.ts CardSource
scripts/ark-nova-cards/extract-cards.ts Parse JSON local
scripts/ark-nova-cards/slice-sprites.ts Découpe sprite sheets en PNG individuelles
scripts/ark-nova-cards/ingest.ts Push Qdrant
scripts/ark-nova-cards/link-game.ts Lie ligne games

Payload Qdrant ark-nova-cards

{
  id: pointId,
  name: string,
  category: 'animal' | 'sponsor',
  latin_name: string,         // pour les animaux
  size: 'small' | 'medium' | 'large',
  conservation_point: number,  // points conservation
  endangered: boolean,
  image_url: string,          // chemin local PNG découpé
  text: string,               // texte des effets
}

Pas de méta ni deckbuilding

Ark Nova n'a pas de format compétitif constructed. Le mode deckbuilding ne s'applique pas — retrieval synergy seul.

Pas de symboles UI dédiés

Les pictos (taille, conservation, abreuvoir, partenaire scientifique…) restent dans le texte descriptif des cartes. Si tu veux ajouter des PNG : créer frontend/src/lib/ark-nova-symbols.ts + assets frontend/public/ark-nova-icons/.

Mise à jour

Quasi-statique. Si une nouvelle extension sort :

  1. Mettre à jour le JSON local et/ou les sprites
  2. docker exec boardgame-referee npm run cards:ark-nova:slice (re-découpe sprites)
  3. docker exec boardgame-referee npm run cards:ark-nova:extract (re-parse JSON)
  4. docker exec boardgame-referee npm run cards:ark-nova:ingest (push Qdrant)
  5. /admin → Resync ARK NOVA cards (optionnel)

Cas d'usage typique

Disney Lorcana

Disney Lorcana

Dernière mise à jour : 2026-05-10

Source

Code

Fichier Rôle
src/services/cards/sources/lorcana.ts CardSource (load, normalize, getImageUrl)
src/services/cards/sources/lorcana-normalize.ts Symboles ink/lore/willpower normalisés
scripts/lorcana-cards/download.ts Télécharge allCards.json FR + EN
scripts/lorcana-cards/ingest.ts Push Qdrant
scripts/lorcana-cards/ingest-symbols.ts Symboles spécialisés (ink, lore, willpower)

Payload Qdrant lorcana-cards

{
  id: pointId,
  name: string,            // FR
  name_en: string,         // EN
  version: string,         // "Bandit rusé"
  set_label: string,
  rarity: string,
  card_type: 'Character' | 'Item' | 'Action' | 'Location' | 'Song',
  ink: number,             // coût en ink (mana)
  willpower: number,
  strength: number,
  lore: number,            // points de lore générés
  text: string,            // abilities
  image_url: string,       // CDN Ravensburger officiel (api.lorcana.ravensburger.com)
}

Symboles UI

Token texte Symbole PNG
{E} Exert /lorcana-icons/exert.png
{I} Ink /lorcana-icons/ink.png
{L} Lore /lorcana-icons/lore.png
{S} Strength /lorcana-icons/strength.png
{W} Willpower /lorcana-icons/willpower.png

PNG officiels Ravensburger (vérifier les ToS si on les redistribue publiquement, pour usage perso self-host c'est OK).

Helper : frontend/src/lib/lorcana-symbols.ts. Activé uniquement si game.hasCardDatabase === 'lorcana-cards'.

⚠️ Méta : DotGG figée nov 2025

L'API DotGG (api.dotgg.gg/cgfw/) servait pour Lorcana méta-game (tier list, archétypes), mais sa data est figée au 21 novembre 2025. Le projet a tenté l'intégration 2× (cf. mémoire project_lorcana_integration.md), refermée chaque fois.

Action : ne pas relancer une sync méta Lorcana tant que DotGG ne reprend pas. Si une autre source apparaît (Mobalytics? officiel Disney?), créer un nouveau service services/meta/<source>.ts.

Deckbuilding (partiel)

Le deckbuilding fonctionne sur les axes :

Mais sans méta dispo, les decklists d'inspiration ne sont pas alimentées. Tu auras une liste cohérente mais pas forcément alignée sur le métagame compétitif.

Pièges connus

Source images

CDN officiel Ravensburger : https://api.lorcana.ravensburger.com/v1/.... Pas de cache local pour images cartes (proxy direct via /api/cards/image/:pointId).

Flesh and Blood (FAB)

Flesh and Blood (FAB)

Dernière mise à jour : 2026-05-10

Source

Code

Fichier Rôle
src/services/cards/sources/flesh-and-blood.ts CardSource utilisant le package npm
scripts/flesh-and-blood-cards/ingest.ts Push Qdrant
scripts/flesh-and-blood-cards/link-game.ts Lie une ligne games à hasCardDatabase = 'flesh-and-blood-cards'
src/services/decks/parse-decklist.ts Parser tolérant Fabrary / FABDB / GEM
src/services/decks/match-decklist.ts Index byFullName + byBaseName du cache cartes
src/routes/decks.ts POST /api/decks/parse

Payload Qdrant flesh-and-blood-cards

{
  id: pointId,
  name: string,
  name_en: string,
  set_label: string,
  rarity: string,
  card_type: 'Action' | 'Attack' | 'Defense Reaction' | 'Equipment' | 'Hero' | ...,
  pitch: 1 | 2 | 3 | null,    // red/yellow/blue
  attack: number,
  defense: number,
  cost: number,
  card_legal_heroes: string[],  // ['Briar', 'Lexi', 'Dash', ...]
  classes: string[],            // ['Ranger', 'Wizard']
  talents: string[],            // ['Earth', 'Lightning']
  text: string,
  image_url: string,
}

Symboles UI

Token Symbole PNG
{r} Red pitch /fab-icons/icon_r.png
{p} Power /fab-icons/icon_p.png
{d} Defense /fab-icons/icon_d.png
{h} Health /fab-icons/icon_h.png
{i} Intelligence /fab-icons/icon_i.png
{c} Cost /fab-icons/icon_c.png
{u} Currency /fab-icons/icon_u.png
{t} Tap /fab-icons/icon_t.png

PNG officiels LSS depuis rules.fabtcg.com (cf. mémoire project_fab_symbols_done.md, commit 2a77d94).

Helper : frontend/src/lib/fab-symbols.ts. Whitelist regex stricte [rpdhicut] minuscules — sinon collision avec {R} MTG (majuscules).

Import de deck (Fabrary)

Workflow : Fabrary → "Copy as Text" → coller dans DeckImportModal → preview → attacher au chat.

Choix de design : on ne fetch PAS Fabrary. L'app Fabrary est une SPA React avec API GraphQL AWS AppSync protégée par Cognito Identity Pool + signature SigV4. Implémenter ce flow côté serveur serait fragile (endpoint non documenté) et lourd. Le format texte standard FAB est stable, fonctionne avec tous les builders communauté (Fabrary, FABDB, GEM), et se passe d'auth.

Parser tolérant (parse-decklist.ts)

Accepte :

⚠️ Le parser exige une quantité numérique en tête : sans ça, les lignes parasites du footer Fabrary (Voir le deck complet @ https://..., Fait avec ♥) seraient comptées comme cartes à qty=1 et pollueraient unmatched.

Décodage automatique decodeURIComponent quand le presse-papier mobile renvoie du %20/%0A.

Matcher (match-decklist.ts)

Whitelist côté backend

POST /api/decks/parse vérifie :

Pour étendre à un autre TCG : ajouter à SUPPORTED_COLLECTIONS + créer parser+matcher dédié pour le format de ce TCG.

Méta-game

META_FAB_TOURNAMENT_ENABLED=true active la sync services/meta/fabtcg.ts :

Workflow update

⚠️ Cas spécial : la data FAB est bundlée dans l'image Docker via le package npm. Mettre à jour les cartes nécessite un rebuild + redeploy AVANT le resync. Cf. mettre-a-jour-cartes.md.

Magic: The Gathering

Magic: The Gathering

Dernière mise à jour : 2026-05-10

Source

Code

Fichier Rôle
src/services/cards/sources/magic.ts Implémente CardSource (load, normalize, getImageUrl)
scripts/magic-cards/download-bulk.ts Télécharge all-cards.json Scryfall
scripts/magic-cards/extract-cards.ts Filtre + dédupe par oracle_id, version FR
scripts/magic-cards/translate-missing.ts Bat ch Haiku traduit cartes manquantes (~3000/batch)
scripts/magic-cards/ingest.ts Pousse vers Qdrant magic-cards
scripts/meta-mtg/sync.ts Sync 17Lands API
scripts/meta-mtg/sync-constructed.ts Scrape MTGGoldfish
scripts/meta-mtg/sync-tournament.ts Scrape MTGTop8
scripts/meta-mtg/fix-legalities.ts Re-vérifie les formats Standard après rotation

Payload Qdrant magic-cards

{
  id: pointId,
  name: string,            // FR
  name_en: string,         // EN
  set_label: string,       // ex: "Strixhaven (STX)"
  rarity: 'common' | 'uncommon' | 'rare' | 'mythic',
  card_type: string,       // "Creature — Human Wizard"
  mana_cost: string,       // "{2}{U}{U}"
  cmc: number,             // 4
  card_mtg_color_identity: string[],  // ['U', 'B']
  card_mtg_legal_formats: string[],   // ['standard', 'modern', 'legacy', ...]
  card_mtg_layout: string, // 'normal' | 'modal_dfc' | 'transform' | etc.
  faces: [...],            // pour double-face
  power: string,
  toughness: string,
  text: string,            // ability text (effet)
  image_url: string,       // CDN Scryfall
}

Symboles UI

Méta-game

3 sources combinées :

Toutes les sources poussent dans Qdrant comme chunks [META]. Le RAG retrieval méta filtre sur meta_format + meta_set selon la question.

Deckbuilding

Set matching

set-aliases.ts normalise les noms d'extensions :

Permet à l'utilisateur de citer un set par son nom complet, le filtre Qdrant s'applique sur le code 3 lettres.

Multi-query MTG

retrieve/multi-query-mtg.ts : Haiku décompose la question en JSON :

{
  colors: string[],       // ['R', 'G']
  cmcMax: number,         // 3
  types: string[],        // ['Creature']
  themes: string[],       // ['elf', 'tribal']
  format: string,         // 'standard'
  set: string             // 'BLB'
}

Chaque champ devient un filter Qdrant natif (payload matching) → réduit le candidate set avant le rerank.

Workflow update

Voir mettre-a-jour-cartes.md pour le détail (workflow 4 étapes : download → extract → translate-missing → ingest, + fix-legalities post-rotation).

Mettre à jour les cartes d'un TCG

Mettre à jour les cartes d'un TCG

Dernière mise à jour : 2026-05-11

Page critique — workflow de référence quand un nouveau set sort.

TL;DR

Depuis 2026-05-11, la majorité des resyncs se font directement depuis /admin → section "Bases de cartes". Plus besoin de SSH dans le container pour les usages courants.

TCG Source Mode admin Workflow
Riftbound API Riot live "Resync. (live)" — inline Un clic. Diff + upsert via syncCollection.
MTG Scryfall bulk JSON "Lancer le pipeline" — modal Download → extract → ingest (~25-40 min)
Lorcana LorcanaJSON.org "Lancer le pipeline" — modal Download → ingest (~5-10 min)
FAB Package npm @flesh-and-blood/cards (bundlé) "Lancer le pipeline" — modal Ingest seul (~5-10 min). ⚠️ npm update + redeploy d'abord pour avoir les nouvelles cartes (cf. § FAB)
Terraforming Mars Local ${TM_CARDS_DATA_DIR} "Lancer le pipeline" — modal Parse-html → ingest (~5 min)
Ark Nova Local ${ARK_NOVA_CARDS_DATA_DIR} "Lancer le pipeline" — modal Slice-sprites → extract → ingest (~5 min)

Le bouton "Lancer le pipeline" exécute les commandes npm correspondantes côté serveur via child_process.spawn. Les logs sont streamés en SSE dans le modal de progression. Un seul pipeline tourne à la fois (lock global pour ne pas saturer TEI).

Architecture

Service src/services/cards/sync-jobs/

Quatre fichiers + un index :

Table SQLite card_collection_meta

Définie dans src/schema.ts, exposée par src/repositories/card-collection-meta.repo.ts (get, listAll, upsert).

Champ Type Rôle
collection text PK Nom de la collection Qdrant (magic-cards, lorcana-cards, …)
last_synced_at text (ISO) Date du dernier succès. NULL = jamais synchronisé via l'admin
last_status text enum idle / running / done / error
last_error text Message d'erreur si last_status='error', NULL sinon
cards_count integer Nombre de points Qdrant après le dernier succès
duration_ms integer Durée du dernier pipeline en ms

Migration : migrations/0011_tiny_nemesis.sql. Appliquée automatiquement au boot via runMigrations() (src/db.ts).

Une ligne par collection (partagée entre le jeu de base et ses extensions). Alimentée par le runner.ts à chaque exécution.

Routes admin

Toutes sous /api/admin/cards/*, protégées par requireAuth + requireAdmin.

Route Méthode Rôle
/admin/cards GET Liste enrichie (dédoublonnée par collection) avec chunksCount, supportsLiveResync, hasPipeline, pipelineSteps, lastSyncedAt, lastStatus, lastError, durationMs
/admin/cards/:collection/resync POST → SSE Mode "live" (Riftbound) : syncCollection() qui diffe contre Qdrant. 409 pour les sources sans supportsLiveResync
/admin/cards/:collection/pipeline/start POST Lance le pipeline npm en arrière-plan. 202 si OK, 404 si pas de pipeline, 409 si un autre tourne
/admin/cards/:collection/pipeline/stream GET → SSE Replay historique + events temps réel (pipeline:start, step:start, log, step:done, pipeline:done, error, heartbeat)
/admin/cards/pipeline/active GET État du lock global. Renvoie { collection, startedAt, finished } | null. Utilisé par l'UI pour rouvrir le modal au refresh

Frontend

Workflow par TCG (en ligne de commande, si besoin de debug)

Tous les pipelines admin se basent sur ces commandes — elles restent disponibles en CLI si tu veux faire un dry-run, un debug, ou si l'admin est cassé.

FAB ⚠️ cas spécial (data bundlée)

La data FAB est dans le package npm @flesh-and-blood/cards embarqué dans l'image Docker. Cliquer "Lancer le pipeline" depuis /admin ne ramène PAS de nouvelles cartes tant que l'image n'est pas rebuilt avec un package à jour. Pour vraiment ajouter de nouvelles cartes :

# 1. Mettre à jour le package localement (machine dev)
npm update @flesh-and-blood/cards @flesh-and-blood/types

# 2. Commit + push
git add package.json package-lock.json
git commit -m "chore(deps): update FAB cards to <version>"
git push

# 3. Attendre que la CI Gitea build/push l'image
# (jobs `test` + `build` dans .gitea/workflows/build.yml)

# 4. Sur Unraid : pull + restart le container
docker compose -f /mnt/user/appdata/boardgame-referee/docker-compose.yml pull app
docker compose -f /mnt/user/appdata/boardgame-referee/docker-compose.yml up -d --force-recreate app

# 5. /admin → "Lancer le pipeline" sur Flesh and Blood

MTG

Pipeline admin = cards:mtg:downloadcards:mtg:extractcards:mtg:ingest enchaînés. Si tu veux les lancer manuellement :

docker exec boardgame-referee npm run cards:mtg:download
docker exec boardgame-referee npm run cards:mtg:extract
docker exec boardgame-referee npm run cards:mtg:ingest

# Traduction des cartes non encore traduites (Haiku batch, hors pipeline admin)
docker exec boardgame-referee npm run cards:mtg:translate-missing

# Si Standard a tourné (rotation), refixer les légalités
docker exec boardgame-referee npm run meta:mtg:fix-legalities

Note : cards:mtg:translate-missing peut prendre 2-3h et n'est pas dans le pipeline admin (volontairement — c'est une étape lente et batch-bornée). À lancer en CLI quand nécessaire.

Lorcana

Pipeline admin = cards:lorcana:downloadcards:lorcana:ingest. Manuellement :

docker exec boardgame-referee npm run cards:lorcana:download
docker exec boardgame-referee npm run cards:lorcana:ingest

# Symboles inline (1 seule fois, ne change pas entre sets)
docker exec boardgame-referee npm run cards:lorcana:ingest-symbols

⚠️ Pas relancer la sync méta DotGG tant qu'elle ne reprend pas (figée 21 nov 2025).

Riftbound

Mode "live" depuis l'admin = un clic. En CLI pour debug :

docker exec boardgame-referee npm run cards:riftbound:fetch
docker exec boardgame-referee npm run cards:riftbound:ingest

Pas de rebuild image nécessaire (sauf si tu changes le code de normalisation).

Terraforming Mars

Pipeline admin = cards:tm:parsecards:tm:ingest. Manuellement :

docker exec boardgame-referee npm run cards:tm:parse
docker exec boardgame-referee npm run cards:tm:ingest

Ark Nova

Pipeline admin = cards:ark-nova:slice-spritescards:ark-nova:extractcards:ark-nova:ingest. Manuellement :

docker exec boardgame-referee npm run cards:ark-nova:slice-sprites
docker exec boardgame-referee npm run cards:ark-nova:extract
docker exec boardgame-referee npm run cards:ark-nova:ingest

Diff incrémental sans réembed (cards:sync)

Pour les mises à jour mineures (correction d'effet erraté, fix d'une traduction) sans repasser par le pipeline complet :

docker exec -e COLLECTION=flesh-and-blood-cards boardgame-referee npm run cards:sync

scripts/cards/sync.ts appelle syncCollection() qui diffe par card_id + hash(card_text) et n'embed que ce qui a changé. Idempotent.

C'est aussi ce que fait le mode "Resync. (live)" Riftbound côté admin.

Sécurité — pourquoi la whitelist statique

Le service sync-jobs utilise child_process.spawn pour exécuter des commandes npm. Aucune partie de la commande n'est construite à partir d'input client :

Pour ajouter une nouvelle pipeline :

  1. Ajouter les scripts npm dans package.json
  2. Ajouter une entrée dans CARD_SYNC_PIPELINES (src/services/cards/sync-jobs/pipelines.ts)
  3. Build + redeploy (aucun changement frontend nécessaire, la liste est servie dynamiquement par GET /admin/cards)

Vérifier que ça a marché

  1. Badge fraîcheur vert dans /admin (≤ 7 jours) avec compteur de cartes mis à jour.
  2. Autocomplete : sur /play du jeu concerné, taper @<carte récente> → la carte ressort.
  3. Question synergy : "Quelles sont les nouvelles cartes du set X ?" → l'oracle doit pouvoir les citer.
  4. Si 0 résultat : vérifier que games.has_card_database (en BDD SQLite) pointe sur la collection Qdrant exacte. Reconnect via npm run cards:<tcg>:link-game si le lien manque.

Restart cache mémoire après gros update

services/cards-cache.ts charge les collections en mémoire au boot. Après un gros update (>1000 cartes), restart le container pour s'assurer que le cache est warm avec la nouvelle data :

docker compose -f /mnt/user/appdata/boardgame-referee/docker-compose.yml restart app

Note : syncCollection() (mode "live" Riftbound) et le runner pipeline n'invalident pas automatiquement ce cache — le hot reload des cartes en mémoire est un TODO connu. En attendant, restart après gros update.

Debug

Symptôme Piste
Bouton désactivé sans raison apparente Un autre pipeline tourne — GET /admin/cards/pipeline/active ou regarder le log container
Modal ne s'ouvre pas au refresh pipelineActive() n'a pas trouvé le job → expiré (>10 min après fin) ou backend redémarré
Logs vides dans la console EventSource déconnecté (vérifie network tab, status code) ou heartbeat manquant côté serveur
last_status=error permanent Lire last_error (truncated dans l'UI, complet en BDD) ; inspecter les logs container pour le traceback complet
Pipeline qui finit en 0s avec exit code 0 Script npm introuvable — vérifier que la commande dans CARD_SYNC_PIPELINES existe bien dans package.json

Riftbound

Riftbound

Dernière mise à jour : 2026-05-10

TCG Riot Games (univers League of Legends).

Source

Code

Fichier Rôle
src/services/cards/sources/riftbound.ts CardSource (load depuis cache)
scripts/riftbound-cards/fetch-cards.ts Appelle l'API Riot, écrit cache JSON
scripts/riftbound-cards/normalize.ts Nettoyage HTML → domaines, énergie, types, tags
scripts/riftbound-cards/ingest.ts Push Qdrant
scripts/riftbound-cards/test-retrieve.ts Debug retrieval Qdrant
scripts/riftbound-cards/link-game.ts Lie une ligne games
services/meta/mobalytics-riftbound.ts Tier list Mobalytics (scrape)
services/meta/riftboundstats.ts Tournois RiftboundStats (API)

Payload Qdrant riftbound-cards

{
  id: pointId,
  name: string,
  name_en: string,
  set_label: string,
  rarity: string,
  card_type: 'Unit' | 'Champion Unit' | 'Spell' | 'Gear' | 'Battlefield' | 'Legend' | 'Rune',
  card_domains: ('Fury' | 'Calm' | 'Mind' | 'Body' | 'Chaos' | 'Order')[],
  energy: number,
  might: number,            // valeur d'attaque
  text: string,             // abilities
  tags: string[],
  image_url: string,         // URL officielle Riot
}

Symboles UI

Token Symbole PNG
:rb_might: Might (force) /riftbound-icons/might.png
[rune_fury] Domain Fury /riftbound-icons/fury.png
[rune_calm] Domain Calm /riftbound-icons/calm.png
[rune_mind] Domain Mind /riftbound-icons/mind.png
[rune_body] Domain Body /riftbound-icons/body.png
[rune_chaos] Domain Chaos /riftbound-icons/chaos.png
[rune_order] Domain Order /riftbound-icons/order.png

PNG officiels Riot press-kit (cf. mémoire project_riftbound_support.md, commit 3de9cdd).

Helper : frontend/src/lib/riftbound-symbols.ts. Activé si game.hasCardDatabase === 'riftbound-cards'.

Méta-game

Deux sources :

Mobalytics (tier list)

RiftboundStats (tournois)

Deckbuilding

Multi-query Riftbound

retrieve/multi-query-riftbound.ts : Haiku décompose la question en JSON {domains, energyMax, types, themes, legend} → filters Qdrant natifs.

Autocomplete @card

Pattern @ (mention-style) choisi 2026-04-13 (cf. mémoire project_riftbound_cards_ux.md). Première implémentation TCG du pattern, ensuite généralisée à MTG/FAB/Lorcana.

Workflow update

Source live API → workflow simple : npm run cards:riftbound:fetch puis npm run cards:riftbound:ingest. Pas de rebuild image Docker nécessaire (sauf si tu changes le code de normalisation).

Détails dans mettre-a-jour-cartes.md.

Terraforming Mars

Terraforming Mars

Dernière mise à jour : 2026-05-10

Pas un TCG au sens strict (jeu de société avec base de cartes fixe), mais traité comme tel pour l'autocomplete @.

Source

Code

Fichier Rôle
src/services/cards/sources/terraforming-mars.ts CardSource
scripts/terraforming-mars-cards/parse-html.ts Parse les règles HTML → cards.json
scripts/terraforming-mars-cards/ingest.ts Push Qdrant
scripts/terraforming-mars-cards/link-game.ts Lie ligne games

Payload Qdrant terraforming-mars-cards

{
  id: pointId,
  name: string,
  cost: number,
  card_type: 'Project' | 'Corporation' | 'Prelude' | 'Standard',
  effect: string,             // texte de l'effet
  victory_points: number,
  tags: string[],             // ['Plant', 'Microbe', 'Building', ...]
  requirements: string,       // ex: "Temperature -14°C+"
  image_url: string,          // chemin local
}

Images

Servies localement via proxy /api/cards/image/:pointId (resize sharp). Pas de CDN externe.

Symboles UI

Pas de symboles dédiés pour TM — les ressources (oxygène, température, océan, MC, plant, animal, microbe…) sont rendues comme texte descriptif dans les abilities. Si on veut ajouter des PNG plus tard : créer frontend/src/lib/tm-symbols.ts + assets dans frontend/public/tm-icons/.

Pas de méta ni deckbuilding

Mise à jour

Quasi-statique. Le jeu de base est figé, seules les extensions occasionnelles ajoutent des cartes :

docker exec boardgame-referee npm run cards:tm:parse-html
docker exec boardgame-referee npm run cards:tm:ingest

Puis /admin → Resync TM cards (si tu veux passer par l'UI).

Cas d'usage typique