Architecture

API REST

API REST

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

34 endpoints, tous prefixés /api. Auth par cookie de session (HTTP-only). Validation Zod systématique.

Auth

Méthode Path Auth Description
POST /api/auth/register Non Crée user (username, email, password). Hash argon2id. Statut pending. Notif admin email
POST /api/auth/login Non Login → session 7j. Rate-limit 5 tentatives / 15 min
POST /api/auth/logout Oui Invalide la session
GET /api/confirm-user/:token Non Email confirmation one-time → role user

Games

Méthode Path Auth Description
GET /api/games Confirmed Liste tous jeux + metadata BGG
GET /api/games/:id Confirmed Détail jeu (rules_language, hasCardDatabase, etc.)
GET /api/games/:id/pdf Confirmed Stream PDF (Content-Type: application/pdf)
GET /api/games/:id/page-image/:page Confirmed PNG 300 DPI page N (rendu via pdftoppm)
GET /api/games/search?q= Confirmed Recherche fulltext (LIKE)
POST /api/games/ingest Confirmed + canAddGames Multipart : PDF + metadata. Si scheduled_start_at : queue scheduled, sinon démarrage immédiat
DELETE /api/games/:id Admin Supprime jeu, questions, purge collection Qdrant
DELETE /api/games/:id/scheduled Confirmed + canAddGames Annule ingestion scheduled. 409 si pas en scheduled

Ask (RAG)

Méthode Path Auth Description
POST /api/ask/retrieve Confirmed Retrieval seul (chunks sans génération) — pour évaluation
POST /api/ask/stream Confirmed RAG streaming SSE (question → retrieval → Claude). Body : { game_id, question, extensions, history, cardMentions, stickyCardMentions }
GET /api/ask/:questionId Confirmed Récupère la réponse persistée (fallback SSE après crash connexion)
PUT /api/ask/:questionId/feedback Confirmed Vote pouce ↑↓ + comment

Cards

Méthode Path Auth Description
GET /api/cards/search?gameId=&q=&limit= Confirmed Autocomplete par collection (BM25 ou full-text)
GET /api/cards/image/:pointId?w=&gameId= Confirmed Proxy image cachée (sharp resize, fallback CDN)

Decks

Méthode Path Auth Description
POST /api/decks/parse Confirmed Parse decklist texte → pointIds Qdrant. Whitelist flesh-and-blood-cards. Rate-limit 10/min

BGG

Méthode Path Auth Description
GET /api/bgg/hot Confirmed Top 20 jeux BGG (cache 6h)
GET /api/bgg/search?q= Confirmed Recherche BGG XML API
GET /api/bgg/game/:bggId Confirmed Détail jeu BGG
GET /api/bgg/game/:bggId/expansions Confirmed Extensions d'un jeu BGG

Lorcana

Méthode Path Auth Description
GET /api/lorcana-symbols/:symbolId Confirmed SVG symboles spécialisés Lorcana

Admin

Méthode Path Auth Description
GET /api/admin/health Admin Health Qdrant, TEI, reranker, Claude SSH, SMTP + stats
GET /api/admin/users Admin Liste users (id, username, role, canAddGames)
DELETE /api/admin/users/:id Admin Supprime user + questions, réassigne ses jeux à l'admin
POST /api/admin/users/:id/set-can-add-games Admin Toggle canAddGames
POST /api/admin/confirm-user/:userId Admin Force confirmation user pending → role user
GET /api/admin/feedback?gameId=&vote=&from=&to=&page= Admin Pagine feedbacks filtrés
GET /api/admin/feedback/:id Admin Détail feedback + diagnostics complets
POST /api/admin/feedback/export Admin Export CSV feedbacks filtrés
POST /api/admin/games/:id/sync-cards Admin Force sync collection Qdrant vs source
GET /api/admin/cards/list Admin Liste collections + counts
POST /api/admin/send-test-email Admin Test SMTP
POST /api/admin/send-password-reset/:userId Admin Force reset email

Health

Méthode Path Auth Description
GET /api/health Non { status: 'ok', timestamp } (Docker healthcheck)

Patterns globaux

Pas d'OpenAPI/Swagger — le tableau ci-dessus est la source de vérité.

Architecture — Backend

Architecture — Backend

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

Couches

src/
├── routes/           # Contrats HTTP (Hono), validation Zod, auth
├── handlers/         # Logique métier pure (Phase 4 MVC)
├── services/         # Domaine RAG / Qdrant / TEI / Claude / cards / méta / OCR
├── repositories/     # Data access Drizzle — SEUL endroit qui importe drizzle-orm
├── middleware/       # auth.ts (sessions, roles), security.ts (CORS, headers)
├── lib/              # logger, schemas Zod partagés, utils, with-timeout
├── cron/             # ingest-scheduler, meta-sync, forum-sync
├── config.ts         # Validation Zod env vars (boot-time)
├── schema.ts         # Tables Drizzle (users, games, questions)
├── db.ts             # Connexion SQLite + migrations
├── types.ts          # Types globaux (SessionUser, AppEnv)
└── index.ts          # App Hono, montage routes, CORS, init

Règles d'archi

  1. routes/ ne fait que parser/valider l'entrée (Zod), appeler un handler ou un service, renvoyer la réponse Hono.
  2. handlers/ ne connaît pas Hono. Reçoit des données validées + dépendances, retourne un Result discriminé. Pour les erreurs attendues : type DeleteGameResult = { ok: true } | { ok: false; status: 404; error: string }.
  3. services/ : logique métier, pas de DB directe — passe par les repos.
  4. repositories/ : SEUL endroit qui importe db, drizzle-orm ou les tables du schéma. Nommer les fonctions par intention métier (getByBggId, setIngestStatus).
  5. config.ts : seul endroit qui peut lire process.env.X. Tous les autres fichiers font import { config }.
  6. logger.ts : seul endroit où console.* est autorisé. Convention : préfixer le scope dans le message (logger.info('[meta-sync] ...')).
  7. Aucun fichier > 350 lignes. Si un fichier enfle, le découper : types.ts pour les interfaces, un fichier par responsabilité, index.ts comme barrel.
  8. Imports services : toujours via le barrel (from '../services/rag/index.js'), jamais un sous-module direct.

Routes (vue d'ensemble)

Préfixe Module
/api/auth/* routes/auth.ts — register, login, logout, change password, reset
/api/games/* routes/games.ts — CRUD jeux, PDF, ingest, page-image
/api/ask/* routes/ask.ts — RAG retrieve + stream + feedback
/api/cards/* routes/cards.ts — autocomplete, image proxy
/api/decks/parse routes/decks.ts — import deck FAB
/api/bgg/* routes/bgg.ts — search, hot, expansions
/api/lorcana-symbols/* routes/lorcana-symbols.ts
/api/admin/* routes/admin.ts — health, users, feedback, sync cards
/api/health routes/health.ts — pour healthcheck Docker
/api/confirm-user/:token routes/auth.ts — lien email confirmation

Tableau exhaustif des 34 endpoints : voir architecture/api-rest.md.

Middleware

Services principaux

services/
├── qdrant.ts            # Client Qdrant (search, upsert, scroll, health)
├── tei.ts               # Client TEI bge-m3
├── reranker.ts          # Client TEI reranker
├── claude-ssh.ts        # SSH Claude Code avec streaming JSON
├── claude-local.ts      # Mode dev local (bypass SSH)
├── claude-quota.ts      # Détection ClaudeQuotaError + parse resetAt
├── validate-model.ts    # Regex stricte modèles Claude (anti-injection shell)
├── bm25.ts              # BM25 sparse retrieval (Qdrant natif)
├── email.ts             # SMTP (notif admin, password reset)
├── bgg.ts + bgg-forums.ts  # API BoardGameGeek + scrape forums Rules
├── hierarchy.ts         # LLM hiérarchie chapter/section
├── conflict-detect.ts   # Détecte conflits extension/base via similarité + LLM
├── contextual-cache.ts  # Cache JSON contextes générés
├── contextual-llm.ts    # LLM contextuel par chunk (Contextual Retrieval B)
├── cards-cache.ts       # Cache mémoire cartes + recherche par nom
├── cards-sync.ts        # Sync collections Qdrant vs sources locales
├── query-expand.ts      # Expansion query (HyDE, synonymes)
├── pdf-images.ts        # Rendu PDF→PNG via pdftoppm (300 DPI)
│
├── chunking/            # Pipeline chunking (chunker, contextual, pdf-extract, types)
├── ocr/                 # Phase 1 : tesseract auto (decideOCR, ocrPages, cache)
├── qdrant/              # Wrappers bas niveau (client, collections, points, payload)
├── ingest/              # Machine à états (queue, coordinator, stages)
├── rag/                 # Pipeline RAG (retrieve, answer, classify, decompose, deckbuilding)
├── cards/sources/       # Normalisations par TCG (magic, lorcana, fab, riftbound, tm, ark-nova) + registry
└── meta/                # Méta-game (17lands, mtggoldfish, mtgtop8, mobalytics, fabtcg, riftboundstats, ingest)

Patterns importants

Architecture — Frontend

Architecture — Frontend

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

Structure

frontend/src/
├── views/             # 15 pages routables (LoginView, PlayView, AdminView, etc.)
├── components/        # ~50 composants Vue, par domaine
│   ├── admin/         # AdminGamesSection, AdminUsersSection, AdminCardDecksSection, AdminFeedbackDetail
│   ├── add-game/      # Wizard 3 étapes (BGG search → upload → ingest ritual)
│   ├── play/          # PlayComposer, PlayChatMessages, PlayBackground, PlayHeader
│   ├── deck-import/   # DeckImportForm, DeckImportPreview
│   ├── card-zoom/     # CardZoomStats (modal zoom cartes)
│   ├── home/          # HomeGameGrid, HomeResumeBanner
│   └── (standalone)   # ChatMessage, ArbiterResponse, CardPreview, NavSidebar, etc.
├── composables/       # ~14 composables (useAskStream, useMentionAutocomplete, useArbiterMarkdown, usePlaySession…)
├── stores/            # Pinia (auth, games, session)
├── services/          # `api.ts` — couche client unique vers backend
├── lib/               # Utilitaires : fab-symbols.ts, mana.ts, riftbound-symbols.ts, lorcana-symbols.ts
├── assets/            # CSS tokens : tokens.css, main.css, motion-tokens.css, TCG-specific (fab.css, lorcana.css, riftbound.css)
└── router/index.ts    # Routes + guards auth/admin

Routeur

router/index.ts (78 lignes). router.beforeEach :

Route Composant Auth Admin
/login LoginView N N
/register RegisterView N N
/confirm-success ConfirmSuccessView N N
/reset-password ResetPasswordView N N
/pending PendingView Y N
/ HomeView Y N
/play PlayView Y N
/history HistoryView Y N
/add-game AddGameView Y N
/ingest/:id IngestView Y N
/me AccountView Y N
/me/settings SettingsView Y N
/admin AdminView Y Y
/admin/feedback AdminFeedbackView Y Y

State management — Pinia stores

stores/auth.ts

stores/games.ts

stores/session.ts

Composables clés

Composable Rôle
useAskStream Wrapper SSE /api/ask/stream + fallback polling 3s × 15 si SSE casse
useEventStream Bas niveau : fetch streaming + parsing SSE générique (filtre les heartbeats)
useMentionAutocomplete Popover @-cartes : debounce 150ms, nav clavier, sync mentions au submit
useArbiterMarkdown marked.parse + injection citations + tokens TCG (mana, FAB, Riftbound, Lorcana)
usePlaySession Orchestre PlayView : SSE, table mode, sticky mentions, deck attachment, card lookup
useTableMode Toggle localStorage 'table-mode'
useDeckAttachment AttachedDeck { deckName, format, hero, cards[] }
useCardLookup Map cardKey → { id, name, imageUrl, orientation }
useStickyMentions buildStickyMentions() : extrait + dédoublonne, FIFO 20 (80 si deck)
useDeckImport parseDeck(gameId) : POST /api/decks/parse

Service api.ts (351 lignes)

Couche client unique pour tous les appels backend. Centralise :

Types exportés : Game, CardSearchResult, MentionedCard, DeckParseResponse, etc.

Design tokens

frontend/src/assets/tokens.css (palette OKLCH dark-first) :

Sémantiques (main.css @theme) :

Build & dev

Commande Effet
npm run dev Vite dev :5173 + proxy /api → :3000
npm run build type-check (vue-tsc --build) + vite build (en parallèle)
npm run build-only vite build seul (dist/)
npm run type-check vue-tsc --build (pas --noEmit : compile full)
npm test / npm run test:watch Vitest

Vite config : alias @src/, plugin @tailwindcss/vite, proxy /api → :3000.

Pièges connus

  1. Scoped vs Tailwind hidden : un display: flex en CSS scoped écrase le hidden lg:flex du template. Préférer Tailwind utilities partout.
  2. vue-tsc --noEmit : passe parfois en local mais foire en CI (vue-tsc --build). Tester avec npm run build.
  3. Unicode / accents dans card names : utiliser normalizeCardKey() (useArbiterMarkdown.ts:74-81) — NFC + lowercase + apostrophe/tiret normalization. Évite les bugs sur Lorcana / Magic japonais.
  4. \b (word boundary) sans flag u : ne reconnaît que [A-Za-z0-9_]. Un nom finissant par é ne match pas. Utiliser un lookahead Unicode-aware avec flag u : (?=\\s|$|[^\\p{L}\\p{N}_]).
  5. Modales async : CardZoomModal + DeckImportModal chargés via defineAsyncComponent() — gain ~50 KB gzip sur le bundle initial PlayView.

Intégrations externes

Intégrations externes

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

Pour chaque service tiers : à quoi il sert, endpoints consommés, où sont les credentials, quel est le mode dégradé.

Qdrant

TEI bge-m3 (embeddings)

TEI Reranker bge-v2-m3

Claude Code CLI (Anthropic)

BoardGameGeek (XML API v2)

Sources de cartes par TCG

TCG Endpoint / source
MTG https://api.scryfall.com/bulk-data → JSON all_cards (téléchargé localement)
Lorcana https://github.com/LorcanaJSON/LorcanaJSON (raw GitHub, MIT)
FAB npm @flesh-and-blood/cards + @flesh-and-blood/types (bundlé dans l'image Docker)
Riftbound https://riftbound.leagueoflegends.com/en-us/card-gallery (API JSON Riot)
Terraforming Mars HTML parsing local + cards.json
Ark Nova JSON local + sprites découpées

Sources méta-game

Source TCG Méthode
17Lands MTG (draft analytics) API publique
MTGGoldfish MTG (constructed metagame) Scrape (Cheerio) + rate-limit 1.5s
MTGTop8 MTG (top 8s tournois) Scrape + rate-limit 1.5s
Mobalytics Riftbound (tier list) Scrape
RiftboundStats Riftbound (tournois) API
fabtcg.com FAB (tournois LSS) Scrape (header META_FAB_USER_AGENT Cloudflare-compatible)

Tous gérés via src/services/meta/*.ts. Cron meta-sync.ts pilote la fréquence (par défaut hebdo, configurable).

Email (SMTP)

Reverse proxy (Nginx Proxy Manager)

Aucune autre intégration

Modèle de données

Modèle de données

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

SQLite (Drizzle ORM)

Schéma : src/schema.ts. Migrations : migrations/*.sql + meta JSON.

Diagramme entité-relation

erDiagram
    users ||--o{ games : "addedBy"
    users ||--o{ questions : "userId"
    games ||--o{ games : "parentGameId (extensions)"
    games ||--o{ questions : "gameId"

    users {
        text id PK
        text username UNIQUE
        text passwordHash "argon2"
        text email
        text role "admin/user/pending"
        bool canAddGames
        text createdAt
    }
    games {
        text id PK
        text name
        text parentGameId FK "nullable"
        bool isExtension
        text contentType "base/extension/advanced_rules/faq"
        text rulesLanguage "fr/en"
        text sourceFile "/app/pdfs/<slug>-<ts>.pdf"
        int chunksCount
        text ingestStatus "idle/running/done/error/scheduled"
        text ingestScheduledAt
        text addedBy FK
        text createdAt
        int bggId "nullable"
        text imageUrl
        text bggType
        text bggMechanics "JSON array"
        text bggCategories "JSON array"
        text hasCardDatabase "magic-cards/lorcana-cards/etc."
    }
    questions {
        text id PK
        text userId FK
        text gameId FK
        text question
        text answer "nullable, injecté post-génération"
        real bestScore
        text createdAt
        text vote "up/down/null"
        text feedbackComment
        text diagnostics "JSON blob"
    }

Migrations livrées

Fichier Apport
0000_new_forge.sql Initial : users, games, questions
0001_vellum_bgg.sql Colonnes BGG (bggId, imageUrl, mechanics, categories)
0002_noisy_the_initiative.sql ingestScheduledAt
0003_pale_rhodey.sql contentType, isExtension, parentGameId, rulesLanguage
0004_sharp_the_captain.sql hasCardDatabase
0005_pretty_lady_bullseye.sql bggType
0006_goofy_terror.sql ingestStatus enum complet
0007_clever_spectrum.sql Schéma conflicts (extension detection)
0008_panoramic_rocket_raccoon.sql Contextual cache support
0009_daffy_tana_nile.sql Feedback (vote, feedbackComment)
0010_daily_sentinels.sql Diagnostics RAG (JSON blob)

Workflow migration :

# 1. Modifier src/schema.ts
# 2. Générer la migration
npm run db:generate
# 3. Appliquer
npm run db:migrate

Qdrant (Vector DB)

Collections

Vecteurs

Payload des chunks règles (rules_<slug>)

{
  // Identité
  chunk_id: string,
  source_file: string,         // chemin PDF
  source_kind: 'pdf' | 'forum',
  
  // Hiérarchie
  hierarchy_path: string[],     // ["Chap 3", "Section 2"]
  hierarchy_level: number,
  section_title: string,
  page_start: number,
  page_end: number,
  
  // Sémantique
  is_extension: boolean,
  is_advanced_rules: boolean,
  is_forum_chunk: boolean,
  game_name: string,
  game_id: string,
  
  // Conflit (extensions seulement)
  conflict_type: 'replaces' | 'modifies' | 'extends' | null,
  conflict_base_chunk_id: string | null,
  conflict_base_page: number | null,
  conflict_summary: string | null,
  
  // Texte
  text: string,                 // chunk brut
  contextual_text: string,      // 1-2 phrases LLM préfixées
}

Payload des chunks cartes (par TCG, variantes)

Champs communs : id, name, name_en, set_label, rarity, type, image_url, text (effet/ability).

Champs MTG : card_mtg_color_identity, card_mtg_legal_formats, card_mtg_layout, mana_cost, cmc, faces (double-face).

Champs FAB : card_legal_heroes, pitch, talents, class, intelligence, defense.

Champs Riftbound : card_domains, energy, card_type (Unit/Champion Unit/Spell/Gear/Battlefield/Legend/Rune), might.

Champs Lorcana : ink, lore, willpower, strength, card_type.

Champs TM : cost, victory_points, tags[], requirements.

Champs Ark Nova : category (animal/sponsor), latin_name, size, conservation_point.

Payload des chunks méta ([META])

{
  meta_type: 'tier' | 'tournament_deck' | 'forum_thread',
  meta_format: string,          // ex: 'standard', 'classic-constructed', 'spiritforged'
  meta_set: string,             // ex: 'BLB' (17Lands)
  meta_source: string,          // '17lands' / 'mtggoldfish' / 'mobalytics' / 'fabtcg'
  meta_archetype: string,
  meta_placement: number,       // 1-N pour les top 8s
  meta_date: string,            // ISO
  ...
}

Filesystem

/app/data/                                 # Volume persistant
├── database.db + .db-shm + .db-wal        # SQLite (WAL mode)
├── card-images-cache/                     # Cache sharp-resize
├── magic-cards/                           # Bulk Scryfall + cards.json FR
├── lorcana-cards/                         # LorcanaJSON FR + EN
├── terraforming-mars-cards/
├── ark-nova-cards/
├── ocr-v1-<slug>.json                     # Cache OCR par jeu
├── contexts-v2-<slug>.json                # Cache Contextual Retrieval B
├── conflicts-v1-<slug>.json               # Cache détection conflits
└── logs/server.log                        # Logger (rotation 50Mo / 30j)

/app/pdfs/                                 # Volume persistant
├── <slug>-<timestamp>.pdf                 # PDF uploadé
└── images/<slug>/page-XX.png              # PNG rendus 300 DPI

/app/ssh/                                  # Volume RO
└── id_ed25519                             # Clé SSH oracle (dédiée)