Mettre à jour les cartes d'un TCG
Mettre à jour les cartes d'un TCG
Dernière mise à jour : 2026-05-
1011
⭐ Page critique — workflow de référence quand un nouveau set sort.
TableauTL;DR
Depuis 2026-05-11, la majorité des resyncs se font directement depuis /admin → section "Bases de référencecartes". Plus besoin de SSH dans le container pour les usages courants.
| TCG | Workflow | ||
|---|---|---|---|
|
|||
| MTG | Scryfall |
|
|
| Lorcana | LorcanaJSON.org | |
|
@flesh-and-blood/cards (bundlé) |
(~5-10 min). npm |
||
Local ${TM_CARDS_DATA_DIR} |
(~5 min) |
||
| Ark Nova | Local ${ARK_NOVA_CARDS_DATA_DIR} |
(~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 :
pipelines.ts — whitelist statique CARD_SYNC_PIPELINES: Record<collection, { label, steps: [{ label, npm }] }>. C'est le seul endroit qui mappe une collection à des commandes npm. Sécurité : aucune commande shell n'est jamais construite à partir d'un paramètre client. La collection envoyée par le frontend sert uniquement de clé dans cette map.
queue.ts — file d'attente en mémoire avec lock global. Un seul activeJob: CardSyncJob | null. Expose isAnyRunning(), createJob(coll), pushEvent(type, data), markFinished(), getEvents(coll, fromIndex). Rétention 10 min après finished=true pour permettre la reprise UI.
runner.ts — orchestrateur. Pour chaque étape :
npm run <step.npm> avec cwd=PROJECT_ROOT (calculé via fileURLToPath(import.meta.url)).
Stdout / stderr lus ligne par ligne via readline → events log typés { stepIndex, line, stream }.
Exit code ≠ 0 → throw → event error + upsert card_collection_meta.last_status = 'error'.
Succès : event step:done avec durationMs.
À la fin : query Qdrant pour cardsCount, upsert card_collection_meta (status='done', last_synced_at, duration_ms).
types.ts — types partagés (CardSyncJob, CardSyncEvent, CardSyncPipeline, CardSyncEventType).
index.ts — API publique : startPipeline(collection), isAnyRunning(), getActiveJob(), getEvents(), resolvePipeline(), listPipelineCollections().
Table SQLite card_collection_meta
Définie dans src/schema.ts, exposée par src/repositories/card-collection-meta.repo.ts (get, listAll, upsert).
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.
/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
frontend/src/components/admin/AdminCardDecksSection.vue — section "Bases de cartes" enrichie (badges fraîcheur, 3 modes de bouton selon supportsLiveResync / hasPipeline).
frontend/src/components/admin/CardSyncPipelineModal.vue — modal de progression. Ouvre un EventSource sur /admin/cards/:collection/pipeline/stream, accumule les events, rend les étapes + console + chrono. Garde les 500 dernières lignes de logs côté client (les scripts MTG produisent ~30 000 lignes).
frontend/src/views/AdminView.vue — orchestration : appelle pipelineActive() au mount pour la reprise auto, gère l'ouverture/fermeture du modal, émet start-pipeline / resync vers la section.
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 embarquéedans le package npm @flesh-and-blood/cards embarqué dans l'image DockerDocker. viaCliquer "Lancer le packagepipeline" npm.depuis Le resync /admin ne voitramène PAS lesde nouvelles cartes tant que l'image n'est pas rebuilt.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 → bouton"Lancer "Resyncle FABpipeline" cards"sur #Flesh Leand frontend stream les compteurs added / updated / removedBlood
MTG
Pipeline admin = cards:mtg:download → cards:mtg:extract → cards:mtg:ingest enchaînés. Si tu veux les lancer manuellement :
# Tout dans le container (les data dirs sont des volumes montés)
# 1. Télécharge le bulk Scryfall (~1 GB)
docker exec boardgame-referee npm run cards:mtg:download
# 2. Extrait + déduplique par oracle_id, version FR
docker exec boardgame-referee npm run cards:mtg:extract
docker exec boardgame-referee npm run cards:mtg:ingest
# 3.Traduction Traduit lesdes cartes manquantesnon encore traduites (Haiku batchbatch, ~3000)hors pipeline admin)
docker exec boardgame-referee npm run cards:mtg:translate-missing
# (peut prendre 2-3h selon ton quota Haiku)
# 4. Ingest dans Qdrant
docker exec boardgame-referee npm run cards:mtg:ingest
# 5. 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 → bouton "Resync MTG cards" (optionnelvolontairement — l'ingestc'est aune déjàétape push)lente et batch-bornée). À lancer en CLI quand nécessaire.
Lorcana
Pipeline admin = cards:lorcana:download → cards: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
# /admin → Resync Lorcana cards (optionnel)
⚠️ Pas relancer la sync méta DotGG tant que DotGGqu'elle ne reprend pas (figée 21 nov 2025).
Riftbound
#Mode 1. Fetch"live" depuis APIl'admin Riot= liveun clic. En CLI pour debug :
docker exec boardgame-referee npm run cards:riftbound:fetch
# 2. Ingest dans Qdrant
docker exec boardgame-referee npm run cards:riftbound:ingest
# /admin → Resync Riftbound cards (optionnel)
Pas de rebuild image nécessaire (sauf si tu changes le code de normalisation).
Terraforming Mars
Pipeline admin = cards:tm:parse → cards:tm:ingest. Manuellement :
docker exec boardgame-referee npm run cards:tm:parse-htmlparse
docker exec boardgame-referee npm run cards:tm:ingest
# /admin → Resync TM cards (optionnel)
Ark Nova
Pipeline admin = cards:ark-nova:slice-sprites → cards:ark-nova:extract → cards:ark-nova:ingest. Manuellement :
# 1. Re-découper les sprites si data change
docker exec boardgame-referee npm run cards:ark-nova:slice
# 2. Re-parse JSONslice-sprites
docker exec boardgame-referee npm run cards:ark-nova:extract
# 3. Push Qdrant
docker exec boardgame-referee npm run cards:ark-nova:ingest
#
Diff →incrémental Resyncsans Arkréembed Nova(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 (optionnel)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.
ResyncSécurité depuis— l'adminpourquoi UIla whitelist statique
Le service utilise /adminsync-jobschild_process.spawn pour exécuter des commandes npm. Aucune partie de la commande n'est construite à partir d'input client :
scriptName vient uniquement de CARD_SYNC_PIPELINES (whitelist en dur dans le code)
La collection envoyée par le client sert juste de clé dans cette map (un nom non whitelisté → shell: →true, env: { ...userInput }
AppellePour une POSTajouter /api/admin/games/:id/sync-cardsquinouvelle pipeline :
CompareAjouterlalescollectionscriptsQdrantnpmcourantedansà la source (cache local ou API)package.jsonAjouteAjouterlesunenouvelles,entréemetdansàCARD_SYNC_PIPELINESjour les modifiées, retire les disparues(src/services/cards/sync-jobs/pipelines.ts)Stream les progrès via EventSource (UI affiche live : added / updated / removed / total)
⚠️ Le bouton Resync ne télécharge PAS une nouvelle source (Scryfall, LorcanaJSON, API Riot, etc.). C'est aux scripts npm run cards:<tcg>:* ci-dessus de faire ça.
Ordre canonique : (1) mettre à jour la source, (2) cliquer Resync.
Quand utiliser quoi
dockerGET exec/admin/cards)
cards:riftbound:fetch:ingestVérifier que ça a marché
- Badge fraîcheur vert dans
/admin/servicesadmin: santé Qdrant + count par collection(avant≤/7aprèsjours)devraitavecavoircompteurbougé)de cartes mis à jour. Tester autocompleteAutocomplete : sur/playdu jeu concerné, taper@<carte récente>→carteslaressortentcarte ressort.Tester une questionQuestion synergy : "donne-moiQuelles sont les nouvelles cartes du setX"X ?" → l'oracle doit pouvoir lesciterciter.- Si 0 résultat : vérifier que
games.has_card_database(enBDD)BDD SQLite) pointe sur la collection Qdrantexacteexacte.(flesh-and-blood-cards,Reconnectmagic-cards, etc.). Connect àvianpm run cards:<tcg>:link-gamesi le lien manque.<gameId>
Restart cache mémoire après gros update
Le 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
Sinon⚡ 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
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 CARD_SYNC_PIPELINES existe bien dans package.json