Files
ObsiGate/docs/features/file-chat-169.md
T
bruno c94f065f80
CI / lint (push) Successful in 2m46s
CI / security (push) Successful in 1m35s
CI / test (push) Successful in 4m28s
CI / build (push) Successful in 1m27s
CI / e2e (push) Successful in 17m11s
feat: chat intégré par fichier — panneau latéral, historique, temps réel SSE #169
- Store backend `backend/file_chat.py` : messages JSON par (vault, path)
  sous `data/chats/` (nom hashé SHA-256 → traversal impossible), plafond
  500 messages, texte tronqué à 4000 caractères, écriture atomique.
- Routes `GET/POST /api/file/{vault}/chat` : auth + accès vault +
  `resolve_safe_path`, schémas Pydantic (`response_model`), broadcast SSE
  `chat_message` sur le transport existant (#62) — pas de second WebSocket.
- Panneau latéral `frontend/js/filechat.js` : bouton 💬 dans la toolbar
  fichier, historique chronologique, envoi optimiste + dédoublonnage par id,
  toast « Nouveau message » si le panneau est fermé/autre fichier.
- Relais SSE dans `sync.js` (import dynamique), CSS bloc #169 (plein écran
  ≤ 768 px, input 16 px anti-zoom), i18n FR/EN (10 clés `chat.*`).
- Tests : `tests/test_file_chat.py` (15) + `tests/frontend/filechat.test.mjs`
  (6, ajouté au pipeline CI), regex toolbar-order mise à jour.
- Docs : CHANGELOG [Unreleased], ROADMAP #169 → livré + index, fiche
  `docs/features/file-chat-169.md`, guide « Discuter d'un fichier ».
2026-10-08 17:03:29 -04:00

3.4 KiB

Chat intégré par fichier — #169

  • Statut : ✅ livré le 2026-10-08
  • Effort : 3-4 j (réalisé en 1 session) | Impact : 🟢
  • Transport : SSE existant (#62) — pas de second WebSocket

Ce qui a été livré

Backend

  • Store backend/file_chat.py — historique JSON par paire (vault, path) sous data/chats/ :
    • nom de fichier = SHA-256 de vault\0path (32 premiers hex) : aucun séparateur de chemin utilisateur dans le nom → traversal impossible ;
    • plafond 500 messages par fichier (les plus vieux supprimés) ;
    • texte tronqué à 4000 caractères ;
    • écriture atomique (tmp + move) ; fichier corrompu → historique vide (fallback, pas de crash).
  • Routes backend/routers/file_chat.py (tag OpenAPI « Files » dérivé) :
    • GET /api/file/{vault}/chat?path=… → historique chronologique ;
    • POST /api/file/{vault}/chat {path, text} → ajout + broadcast SSE ;
    • garde commune : require_auth + check_vault_access + resolve_safe_path (traversal → 403/500 via le handler ServiceError) ;
    • text ou path vides → 400 ; vault inconnu → 404.
  • Schémas ChatMessageItem / ChatHistoryResponse / ChatMessageResponse dans backend/schemas.py (response_model sur les deux routes).
  • Broadcast : sse_manager.broadcast("chat_message", {vault, path, message}).

Frontend

  • frontend/js/filechat.js — panneau latéral :
    • bouton 💬 (.btn-chat) dans le groupe nav de la toolbar fichier (viewer.js), toggle ouvre/ferme ;
    • historique rendu chronologiquement, bulles « mine » à droite, textContent pour le corps (pas d'injection HTML) ;
    • envoi POST optimiste + dédoublonnage par id à la réception SSE ;
    • panneau fermé / autre fichier → toast « Nouveau message de … » (silencier si c'est son propre message).
  • frontend/js/sync.js — listener SSE chat_message avec import dynamique de filechat.js (pas de cycle d'imports).
  • CSS bloc #169 : panneau fixe 340 px, plein écran ≤ 768 px, variables CSS avec fallbacks, input 16 px (anti-zoom iOS).
  • i18n : 10 clés chat.* dans fr.json et en.json.

Tests

  • tests/test_file_chat.py — 15 tests : roundtrip store, isolation par fichier, plafond de rétention, troncature, traversal hashée, fichier corrompu, routes (200/400/404/403), fixture autouse isolant CHAT_DIR.
  • tests/frontend/filechat.test.mjs — 6 tests JSDOM : empty state, rendu chronologique + classe mine, anti-injection HTML, append live + dédup par id, isolation inter-fichiers, toggle. Locales réellement chargées (fetch simulé vers frontend/locales/).
  • Les deux sont ajoutés au pipeline CI (.gitea/workflows/ci.yml).

Décisions

  • SSE plutôt qu'un second WebSocket : le transport de #62 (EventSource /api/events, auth par cookie) existe déjà côté client dans sync.js ; un WS par fichier aurait dupliqué reconnexion/heartbeat/auth pour zéro bénéfice sur du messages-postés.
  • Pas d'événement dans le panneau de sync (_addEvent) : les messages de chat gonfleraient l'historique d'événements d'index sans valeur.
  • Tag OpenAPI : pas de tag maison (file-chat) — non déclaré dans TAGS_METADATA, ce qui cassait test_used_tags_are_declared. Les routes /api/file/* sont dérivées automatiquement sous « Files ».