Maintenance
Backup et recovery
Backup et recovery
Dernière mise à jour : 2026-05-10
⚠️ À COMPLÉTER
Aucune stratégie de backup applicative n'est implémentée dans le code (pas de script scripts/backup.sh, pas de cron dans src/cron/, pas de doc dans ARCHITECTURE.md / CONTRIBUTING.md).
La protection des données dépend actuellement entièrement de la stratégie Unraid native (RAID + parity + snapshots ZFS si activé sur le pool appdata).
À clarifier / décider :
- Pool storage :
appdataest-il sur cache SSD ZFS avec snapshots, ou sur array (parity + array disks) ? - Snapshots ZFS : si oui, fréquence ? Rétention ?
- Backup off-site : il y a-t-il un backup vers un autre Unraid / cloud ?
- Périodicité testée : as-tu déjà testé un restore ? Quand ?
Données à protéger
| Quoi | Où | Volume | Critique ? |
|---|---|---|---|
SQLite (database.db) |
/mnt/user/appdata/boardgame-referee/data/database.db |
~10-50 MB | Élevé : users, jeux, questions, feedbacks |
| PDFs uploadés | /mnt/user/appdata/boardgame-referee/pdfs/*.pdf |
100 MB - quelques GB | Élevé : irrécupérables si perdus (à moins d'avoir l'original) |
| PNG rendus | /mnt/user/appdata/boardgame-referee/pdfs/images/ |
Quelques GB | Faible : régénérables via pdftoppm |
| Caches JSON | /mnt/user/appdata/boardgame-referee/data/{ocr,contexts,conflicts}-*-<slug>.json |
Quelques MB | Moyen : régénérables via Claude (mais cher en quota) |
| Data sources cartes | /mnt/user/appdata/boardgame-referee/data/{magic,lorcana,terraforming-mars,ark-nova}-cards/ |
Quelques GB | Moyen : re-téléchargeables sauf TM/Ark Nova qui sont locaux statiques |
| Card image cache | /mnt/user/appdata/boardgame-referee/data/card-images-cache/ |
Quelques GB | Faible : régénérable sharp depuis CDN |
| Qdrant collections | /mnt/user/appdata/boardgame-referee/qdrant/storage/ |
Quelques GB | Élevé : ré-ingestion = qq heures + quota Claude |
| Clé SSH oracle | /mnt/user/appdata/boardgame-referee/ssh/id_ed25519 |
<1 KB | Critique : sans elle, plus d'oracle SSH |
| NPM config | /mnt/user/appdata/nginx-proxy-manager/ |
Quelques MB | Élevé : recréer manuellement long |
Recommandations (à valider)
Niveau minimum
- SQLite :
sqlite3 .backupquotidien vers un autre disque ou cloud - PDFs : rsync vers un autre disque ou cloud (ils sont irrécupérables)
- Clé SSH oracle : copie sur un support sécurisé (Bitwarden chiffré ou USB)
Niveau confortable
- Qdrant snapshots : pris au moment des grosses ré-ingestions (cf.
deploiement/rollback.mdpour les commandes) - Caches JSON : optionnel, ils valent juste ce qu'ils ont coûté en quota Claude
- NPM config : backup
database.sqliteNPM quotidien
Snapshots Qdrant
Manuel pour l'instant :
# Snapshot d'une collection
curl -X POST http://192.168.10.100:6333/collections/<collection>/snapshots
# Renvoie un nom de snapshot
# Liste les snapshots
curl http://192.168.10.100:6333/collections/<collection>/snapshots
# Télécharger un snapshot pour archive
curl -O http://192.168.10.100:6333/collections/<collection>/snapshots/<snapshot-name>
Restore SQLite
# Stop le container
docker compose -f /mnt/user/appdata/boardgame-referee/docker-compose.yml stop app
# Restore depuis backup
cp /backup/path/database.db.bak /mnt/user/appdata/boardgame-referee/data/database.db
# Ou si WAL : restore aussi .db-wal et .db-shm
# Restart
docker compose -f /mnt/user/appdata/boardgame-referee/docker-compose.yml up -d app
Restore Qdrant
# Re-create la collection depuis le snapshot
curl -X PUT http://192.168.10.100:6333/collections/<collection>/snapshots/recover \
-H "Content-Type: application/json" \
-d '{"location": "<snapshot-url>"}'
Test de restore
⚠️ Une procédure de restore non testée n'existe pas. Quand tu as une stratégie de backup en place, force-toi à tester un restore au moins une fois par trimestre :
- Sur un container test sur Unraid avec un volume vide
- Restore SQLite + Qdrant
- Vérifier qu'on peut login + voir les jeux + reposer une question
Questions à toi (Thymon)
- Quel est ton workflow Unraid actuel pour
appdata? (RAID? Snapshots? Off-site?) - As-tu déjà perdu un fichier sur ce projet ? (incident antérieur qui a forcé un fix)
- Veux-tu qu'on ajoute un script de backup applicatif (sqlite3 .backup + rsync PDFs) en cron sur Unraid ?
Mise à jour des dépendances
Mise à jour des dépendances
Dernière mise à jour : 2026-05-10
Politique : manuel
Pas de Renovate / Dependabot configurés. Mises à jour manuelles, en bloc, quand tu en as envie.
Pourquoi pas Renovate ?
- Volume de PR auto trop élevé sur un projet d'un seul mainteneur
- Préférence pour batch les mises à jour (1× / 2-3 mois)
- Évite la friction "PR Renovate qui casse la CI le mardi matin"
Si tu changes d'avis : Renovate self-hosted via Docker, conf simple renovate.json à la racine.
Workflow de batch update
Backend
# 1. Voir les outdated
npm outdated
# 2. Patch updates (safe — bug fixes)
npm update
# 3. Minor / major sélectifs
npm install hono@latest
npm install drizzle-orm@latest
npm install zod@latest
npm install @types/node@latest
# 4. Vérifier
npm run build
npm test
# 5. Tester en dev
npm run dev
# Aller sur /play, poser quelques questions, vérifier RAG OK
# 6. Si tout OK : commit + push → CI build + push image → pull en prod
git add package.json package-lock.json
git commit -m "chore(deps): bump backend deps"
git push
Frontend
cd frontend
npm outdated
npm update
# Major upgrades
npm install vue@latest vue-router@latest pinia@latest
npm install tailwindcss@latest @tailwindcss/vite@latest
npm install marked@latest
npm run build
npm test
npm run dev
# Tester l'UI
cd ..
git add frontend/package.json frontend/package-lock.json
git commit -m "chore(deps): bump frontend deps"
git push
Cas spéciaux
@flesh-and-blood/cards + @flesh-and-blood/types
Ces deux packages contiennent les données cartes FAB. Mettre à jour = nouveau set FAB. Workflow détaillé : tcg-integrations/mettre-a-jour-cartes.md.
npm update @flesh-and-blood/cards @flesh-and-blood/types
# Build + push obligatoire pour que le container voie les nouvelles cartes
mana-font (frontend)
Webfont Andrew Gioia pour symboles MTG. Update rare (pas de nouveau symbole MTG depuis longtemps). Si nouvelle release :
cd frontend
npm install mana-font@latest
# Vérifier visuellement qu'aucun symbole existant ne casse
pdfjs-dist
Couche d'extraction PDF. Updates régulières mais souvent breaking. Tester sur quelques PDFs de jeux différents avant de push.
better-sqlite3
Driver SQLite synchrone. Update prudemment — il y a souvent des prebuilds Node-version-specific. Si update casse en CI :
- Vérifier compat Node 22
- Si Alpine cible : prebuild musl absent → bloque tout, voir
pipeline-cicd.mdpour le contexte
Audit de sécurité
npm audit
cd frontend && npm audit
Souvent du bruit (vulnérabilités CVSS bas dans des transitive deps). Filtrer :
npm audit --audit-level=high
Update Node 22 → 23 / 24
Pas urgent. Mais quand tu décides :
- Modifier
engines.nodedanspackage.json - Modifier le base image dans
Dockerfile(FROM node:24-bookworm-slim) - Modifier le base image dans
.gitea/workflows/build.yml(container test) npm cilocalement avec Node 24, vérifier que tout compile / test
Pas de major version pinning
package.json utilise ^ partout (= compatible minor). Pas de pinning strict. Si tu veux pin une version pour reproduire un bug :
npm install hono@4.7.6 --save-exact
--save-exact retire le ^.
Logs
Logs
Dernière mise à jour : 2026-05-10
Logger central
Tous les logs serveur passent par src/lib/logger.ts. Conventions :
- Aucun
console.*danssrc/(saufsrc/config.tsqui boot avant le logger) - Préfixer le scope :
logger.info('[meta-sync] ...'),logger.error('[ssh] ...') - Niveaux :
debug/info/warn/error(filtrable viaLOG_LEVEL) - Format prod : JSON ligne (parseable via
jq) - Format dev : pretty print (lisible sans outil)
Locations
- Stdout / stderr Docker :
docker logs boardgame-referee - Fichier persistant :
/app/data/logs/server.log(volume Unraiddata/)
Rotation
Géré par entrypoint.sh au boot :
- Si
server.log > 50 Mo→ archive enserver-YYYYMMDD-HHMMSS.log - Purge automatique des archives
> 30 jours
Pas de rotation runtime (only at boot). Si tu fais beaucoup de DEBUG en prod, pense à restart le container ou logrotate manuellement.
Filtrer
# Live tail
docker exec boardgame-referee tail -f /app/data/logs/server.log
# Live tail avec scope filter (jq pour le JSON)
docker exec boardgame-referee tail -f /app/data/logs/server.log | jq 'select(.scope == "rag")'
# Grep classique
docker exec boardgame-referee grep "claude-quota" /app/data/logs/server.log | tail -50
# Toutes les erreurs des 24 dernières heures
docker exec boardgame-referee grep -E '"level":"error"' /app/data/logs/server.log | tail -100
Niveaux par défaut
| Environnement | LOG_LEVEL |
|---|---|
| Dev | debug |
| Prod | info |
Pour passer en debug en prod (temporaire) :
# Modifier .env Unraid : LOG_LEVEL=debug
# Restart container :
docker compose -f /mnt/user/appdata/boardgame-referee/docker-compose.yml restart app
⚠️ N'oublie pas de remettre en info après — debug est verbeux et remplit vite les 50 Mo.
Logs notables à surveiller
| Pattern | Sens |
|---|---|
[claude-quota] ClaudeQuotaError |
Pause auto enclenchée, vérifier resetAt |
[ssh] command rejected |
Wrapper oracle a bloqué une commande (debug /tmp/oracle-debug.log si nécessaire) |
[ingest] stage error |
Stage d'ingestion en échec |
[meta-sync] purge |
Purge hebdo des chunks méta périmés |
[forum-sync] |
Cron forums BGG (3h du matin par défaut) |
[hyde] timeout |
HyDE Haiku a dépassé 25s, fallback question brute |
[contextual-cache] flushed |
Cache JSON sauvé sur disque (avant quota error ou checkpoint) |
[rag] retrieve |
Diagnostics retrieval (timings, scores) en debug |
Pas de log aggregation externe
Pas de Loki / Grafana / ELK / Sentry actuellement. Tout reste dans le fichier Unraid + Docker logs.
Si un jour tu veux ajouter Sentry ou un log shipper :
- Ajouter une lib (
@sentry/node) - Wrapper le logger pour aussi pousser vers Sentry sur
level === 'error' - Configurer
SENTRY_DSNenv var - Le code reste centralisé dans
src/lib/logger.ts— les autres fichiers n'ont pas à savoir qu'on log ailleurs
Debug d'une question
Pour relire les diagnostics d'une question donnée (lecture seule) :
docker exec boardgame-referee npm run debug:question -- --id <questionId>
Affiche : question, réponse, chunks retrouvés, HyDE, timings — équivalent CLI du panneau /admin/feedback/<id>.
Monitoring
Monitoring
Dernière mise à jour : 2026-05-10
État actuel : minimal
L'app expose un seul endpoint health, qui sert au healthcheck Docker. Pas de Prometheus / Grafana / OpenTelemetry / Sentry actuellement.
GET /api/health
{
"status": "ok",
"timestamp": "2026-05-10T12:34:56.789Z"
}
Public (pas d'auth). Utilisé par le healthcheck Docker (docker-compose.yml) :
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/api/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
Si 3 échecs consécutifs → container marqué unhealthy. Pas de restart auto en place — c'est manuel.
GET /api/admin/health (admin only)
Plus riche, utilisé par la page /admin :
{
"qdrant": { "ok": true, "collections": 14, "stats": {...} },
"tei": { "ok": true, "model": "BAAI/bge-m3" },
"reranker": { "ok": true },
"claude_ssh": { "ok": true, "latency_ms": 5234 },
"smtp": { "ok": true, "configured": true },
"stats": {
"totalGames": 23,
"totalQuestions": 542,
"totalUsers": 3
}
}
Si tu vois Qdrant/TEI/Reranker ok: false, le RAG est cassé. Investigation :
docker logs <service>côté Unraid- Tester le service directement :
curl http://192.168.10.100:8099/health(TEI)
Ajouter un monitoring externe
Suggestions (non implémentées) :
Uptime Kuma
- Le plus simple. Container Unraid à part.
- Monitor : HTTP GET
https://rules.thymon.fr/api/healthtoutes les 60s - Notifications : ntfy / email / Telegram
- Permet de monitorer aussi NPM, Qdrant, TEI, etc. depuis un seul endroit.
Prometheus + Grafana
- Plus poussé. Hono peut exposer des métriques
/metrics(lib@hono/prometheus-metricsou équivalent). - Métriques utiles : latence RAG p50/p99, count erreurs Claude, queue ingestion, etc.
- Container Prometheus + Grafana sur Unraid.
Sentry
- Erreurs côté backend + frontend
- DSN à mettre dans
SENTRY_DSNenv var - Wrapper
src/lib/logger.tspour pousser surlevel === 'error'
Alerting
Aucun alerting actuellement. Si l'app meurt à 3h du matin, tu le vois quand tu poses ta première question le lendemain.
Pour ajouter du basique :
- Uptime Kuma + notif ntfy / Telegram
- ou cron qui ping
/api/healthtoutes les 5 min et envoie un mail si fail (script externe)
Métriques applicatives à logger
Si tu veux instrumenter sans monitoring formel :
- Logger
infoles timings RAG ([rag] question_id=X total=4521ms hyde=2103ms retrieve=854ms rerank=121ms llm=1443ms) - Logger
infoles ingestion durées et tailles ([ingest] game=X chunks=234 ocr_pages=12 duration_s=380) - Logger
warnles fallbacks (HyDE timeout, retrieval vide, reranker down…)
Ensuite tu peux faire grep + jq sur /app/data/logs/server.log pour des stats ad hoc.
Troubleshooting
Troubleshooting
Dernière mise à jour : 2026-05-10
Pièges connus déjà rencontrés, à actualiser au fil des incidents.
Build / CI
Le job test échoue avec une erreur better-sqlite3
- Cause : tu as changé l'image de base CI vers Alpine. Pas de prebuild musl pour
better-sqlite3. - Fix : remettre
node:22-bookworm-slim(Debian Slim glibc).
vue-tsc --noEmit passe en local mais foire en CI
- Cause : la CI fait
vue-tsc --build(compile full), pas--noEmit(type-check seul). - Fix : tester avec
npm run build(qui appellevue-tsc --build) avant de push.
Backend runtime
Boolean("false") === true — env var qui ne fait pas ce qu'on attend
- Symptôme :
VISION_ENABLED=falseest interprété commetrue. - Cause :
z.coerce.boolean()en JS — toute chaîne non vide est truthy. - Fix : utiliser le helper
envBool()défini en haut desrc/config.ts. Comprendtrue/false/1/0/yes/no/on/off.
process.env.X retourne undefined en prod
- Cause : la var n'a pas été ajoutée dans
unraid/boardgame-referee.xml, donc l'admin Unraid ne l'a pas setée dans l'UI. - Fix : règle des 3 fichiers —
src/config.ts+.env.example+unraid/boardgame-referee.xml. Sinon la var est silencieusement absente.
Une regex \b ne match pas un nom finissant par é
- Cause :
\bsans flagune reconnaît que les boundaries[A-Za-z0-9_]. - Fix : lookahead Unicode-aware avec flag
u:(?=\\s|$|[^\\p{L}\\p{N}_]). Piège historiquement déclenché sur les mentions@Lorcana.
Claude répond "je ne vois aucune image"
- Cause :
claude-ssh.tslance le CLI avec--system-promptau lieu de--append-system-prompt. Le contrat d'utilisation des outils est écrasé. - Fix : toujours
--append-system-prompt.
ClaudeQuotaError non détectée → "answer empty" générique
- Cause : le wording du message de quota a changé côté Anthropic.
- Fix : étendre
QUOTA_MARKERSdansclaude-quota.ts. Cf.pipeline-rag/pause-quota-claude.md.
Retrieval / RAG
Citation [BASE p.4 : "..."] ouvre la mauvaise page
- Cause : plusieurs livrets indexés partagent la page 4 (base + extension). La regex de citation ne capturait pas le nom du livret.
- Fix : cf. ARCHITECTURE.md § "Citations cliquables — format nom du livret". Le format est désormais
[<nom du livret>, p.X : "..."]avec résolutionnameToIdcôté frontend.
Image PNG envoyée à Claude mais c'est la page 4 du mauvais livret
- Cause : groupage par
pageau lieu de(livret, page)dansanswer.ts. - Fix : cf.
pipeline-rag/vision-inline.md— granularité composite obligatoire.
Claude cite un livret en [EXT p.4] au lieu de [Heavy Rain, p.4]
- Cause : ancien format de citation pré-2026-04-11.
- Fix : c'est rétrocompatible côté frontend (regex avec fallback). Mais à terme migrer le SYSTEM_PROMPT pour imposer le nouveau format.
Reranker retourne ~0 sur du contenu technique abstrait → enterre le bon chunk
- Cause : le reranker bge-v2-m3 est peu confiant sur du jargon technique.
- Fix : c'est précisément la motivation du blending position-aware (RAG Fusion v2). Cf. ADR-005. Si tu vois encore des cas, considérer ajuster les ratios (
75/25→85/15sur top-3).
Frontend
display: flex dans une CSS scoped écrase le hidden lg:flex Tailwind
- Cause : la spécificité CSS scoped batt utilities.
- Fix : préférer Tailwind utilities partout. Si tu dois absolument scoped, utiliser
!importantou@apply.
Card autocomplete @ ne matche pas un nom japonais / accentué
- Cause :
\bregex Unicode-incompatible (cf. backend ci-dessus, même piège). - Fix :
useArbiterMarkdown.ts:74-81— lookahead(?=\\s|$|[^\\p{L}\\p{N}_])avec flagu.
CardZoomModal ne s'ouvre pas après reload
- Cause :
cardLookupn'a pas été reconstruit depuis les messages persistés en localStorage. - Fix :
usePlaySessionappellerebuildFromMessages()on mount. Vérifier que ce path n'a pas été cassé.
SSE casse à 60s pile et UI affiche network error
- Cause : NPM
proxy_read_timeout 60s+ heartbeat backend pas émis. - Fix : vérifier que
setIntervalheartbeat 8s est bien armé dans le service streamSSE. Pattern dansroutes/ask.ts.
Infra
TEI répond 000 (curl no response)
- Cause : port custom Unraid (8099 au lieu de 80, 8990 au lieu de 8090).
- Fix : suspecter d'abord le port. Cf. mémoire
reference_unraid_ports.md.
Container oracle SSH timeout
- Cause : VM oracle down ou auth ed25519 cassée.
- Fix :
ssh -i /app/ssh/id_ed25519 oracle@<host> "cd /home/oracle/work && claude -p" <<< "test"- Si timeout : SSH server down, restart la VM
- Si
Permission denied: vérifierauthorized_keyscôté VM, vérifier que la clé privée n'a pas été régénérée
Wrapper oracle rejette toutes les commandes
- Cause : préfixe strict du wrapper modifié sans aligner les autres endroits.
- Fix : 3 endroits doivent matcher (
/home/oracle/bin/oracle-claude.shPREFIX,CLAUDE_SSH_WORKDIRenv,permissions.allow/additionalDirectoriesdu settings.json oracle). Cf.securite/ssh-oracle.md.
Logs spam "EADDRINUSE :::3000"
- Cause : container restart trop rapide, l'ancien process pas encore mort.
- Fix :
docker compose down && docker compose up -d(au lieu de restart).
BDD
Migration Drizzle qui ne s'applique pas
- Cause :
drizzle-kit generaten'a pas détecté le diff (rare). Ou la table__drizzle_migrations__est désynchronisée. - Fix : vérifier le contenu de
migrations/vssrc/schema.ts. Si nécessaire,npm run db:migratemanuel pour forcer.
database.db is locked
- Cause : un autre process / container tient le fichier.
- Fix :
lsof /app/data/database.db(depuis Unraid CLI) pour identifier. Souvent un ancien container qui n'est pas vraiment mort.