Files
ObsiGate/docs/features/file-chat-169.md
T
bruno ae06436f91
CI / lint (push) Successful in 2m48s
CI / security (push) Successful in 1m34s
CI / test (push) Successful in 4m33s
CI / build (push) Successful in 1m30s
CI / e2e (push) Successful in 17m5s
feat: chat — suivi du document, chat général en onglet sidebar, onglets au-dessus du filtre #190
- **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.
2026-10-08 20:40:54 -04:00

104 lines
5.2 KiB
Markdown

# 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`).
## #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-target` affiche le titre du document
(l'attribut `title` porte le chemin complet).
- `followFileChat(vault, path, title)` est appelé en tête de `renderFile()` :
panneau ouvert + autre document → re-cible (reload de l'historique) ;
panneau fermé → no-op. Le bouton 💬 transmet `data.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éutilisent `add_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 rouge `99+` sur l'onglet, effacée à l'ouverture.
- **URL** : linkification `http(s)://` au rendu (texte autour en `TextNode`,
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-filter` est **déplacé après** `.sidebar-tabs`.
- `switchSidebarTab` : placeholder `sidebar.filter_chat`, dispatch `chat`.
- `initSidebarFilter` (`sidebar.js`) : `routeFilter`/`routeClear` routent
vers `filterChatMessages()` (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 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 ».