- **A — Suivi du document** : l'en-tête du panneau chat affiche le document ciblé (titre + chemin) et `followFileChat()` appelé par `renderFile()` re-cible le panneau ouvert à chaque changement de document. - **B — Chat général** : onglet « Chat » (dernier) dans la sidebar, conversation centrale stockée dans le store #169 via les sentinelles `__global__/general` (`GET/POST /api/chat`) ; pastille de messages non lus sur l'onglet ; pièces jointes image/vidéo (`POST /api/chat/upload` : allow-list d'extensions, 25 MB, nom UUID ; `GET /api/chat/attachment/{name}` résolu contre l'allow-list) ; URL cliquables au rendu ; date/heure d'envoi. - **C — Ordre des onglets** : la barre de filtre passe sous les onglets et sert de recherche dans le chat (texte + auteur) quand l'onglet Chat est actif (`switchSidebarTab` + `routeFilter`). - Transport : broadcast SSE `chat_message` réutilisé (`vault __global__` route vers la sidebar, sinon le panneau fichier) — pas de 2ᵉ WebSocket. - i18n FR/EN (6 clés), CSS bloc #190, `initSidebarChat()` dans l'orchestrateur. - Tests : pytest 27 chat (12 nouveaux) ; JSDOM `filechat.test.mjs` 11 (5 nouveaux) ; suite 1655 passed, ruff/mypy 0, validate-imports 42 modules. - Docs : CHANGELOG [Unreleased], ROADMAP #190 → index, fiche `file-chat-169.md` §#190, guide « Chat général », journal des interventions.
5.2 KiB
5.2 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).
#190 — Suivi du document, chat général, ordre des onglets (2026-10-08)
A. Le panneau suit le document
- En-tête du panneau :
.file-chat-targetaffiche le titre du document (l'attributtitleporte le chemin complet). followFileChat(vault, path, title)est appelé en tête derenderFile(): panneau ouvert + autre document → re-cible (reload de l'historique) ; panneau fermé → no-op. Le bouton 💬 transmetdata.title.
B. Chat général en onglet sidebar
- Onglet
#sidebar-tab-chat(dernier,message-circle) + panneau#sidebar-panel-chat(liste + 📎 + saisie + Envoyer, Entrée = envoyer). - Store : rien de nouveau — les sentinelles
GLOBAL_VAULT="__global__"/GLOBAL_PATH="general"réutilisentadd_message/get_messages. - Routes :
GET/POST /api/chat,POST /api/chat/upload,GET /api/chat/attachment/{name}. - Pièces jointes : allow-list d'extensions (11 : images + vidéos), 25 MB, nom de stockage = UUID (le nom client n'est jamais utilisé), résolution par comparaison au nom + allow-list (aucun chemin utilisateur).
- Messages non lus : timestamp de dernière visite dans
localStorage['obsigate-chat-unread'], comptage des messages plus récents d'un autre auteur, pastille rouge99+sur l'onglet, effacée à l'ouverture. - URL : linkification
http(s)://au rendu (texte autour enTextNode, donc pas d'injection). - SSE :
vault === "__global__"route vers la liste sidebar (append + clear) ou vers badge + toast selon que l'onglet est actif.
C. Ordre des onglets + filtre = recherche chat
index.html: le bloc.sidebar-filterest déplacé après.sidebar-tabs.switchSidebarTab: placeholdersidebar.filter_chat, dispatchchat.initSidebarFilter(sidebar.js) :routeFilter/routeClearroutent versfilterChatMessages()(texte et auteur, insensible à la casse).
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 ».