- 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 ».
3.4 KiB
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)sousdata/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).
- nom de fichier = SHA-256 de
- 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 handlerServiceError) ; textoupathvides → 400 ; vault inconnu → 404.
- Schémas
ChatMessageItem/ChatHistoryResponse/ChatMessageResponsedansbackend/schemas.py(response_modelsur 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,
textContentpour 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).
- bouton 💬 (
frontend/js/sync.js— listener SSEchat_messageavec import dynamique defilechat.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.*dansfr.jsoneten.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 isolantCHAT_DIR.tests/frontend/filechat.test.mjs— 6 tests JSDOM : empty state, rendu chronologique + classemine, anti-injection HTML, append live + dédup par id, isolation inter-fichiers, toggle. Locales réellement chargées (fetch simulé versfrontend/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 danssync.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é dansTAGS_METADATA, ce qui cassaittest_used_tags_are_declared. Les routes/api/file/*sont dérivées automatiquement sous « Files ».