feat: chat — les posts affichent le markdown rendu comme un document, code coloré #193
CI / lint (push) Successful in 2m50s
CI / security (push) Successful in 1m37s
CI / test (push) Successful in 4m34s
CI / build (push) Successful in 1m31s
CI / e2e (push) Successful in 17m36s

This commit is contained in:
2026-10-09 15:48:04 -04:00
parent 4fb7c43e06
commit f4c8504c8d
17 changed files with 308 additions and 19 deletions
+2
View File
@@ -337,6 +337,8 @@ Avant de corriger quoi que ce soit, un agent IA doit :
| 2026-10-08 | #192 + BUG-109 | Fonctionnalité + correction | `backend/file_chat.py`, `backend/routers/file_chat.py`, `backend/schemas.py`, `frontend/js/filechat.js`, `frontend/js/sync.js`, `frontend/index.html`, `frontend/style.css`, `frontend/locales/{fr,en}.json`, `tests/test_file_chat.py`, `tests/frontend/filechat.test.mjs`, `docs/ROADMAP.md`, `docs/features/file-chat-169.md`, `CHANGELOG.md` | **#192 — chat : saisie auto-agrandissante + accusé de réception.** La boîte d'envoi (panneau document **et** sidebar) devient un `textarea` dont la hauteur suit le contenu (plafond 160 px puis défilement, repasse à 1 ligne après l'envoi), `Entrée` envoie / `Maj+Entrée` saute une ligne ; placeholder sidebar désormais traduit à l'init. **Accusé de réception** : chaque document de conversation porte `read = {utilisateur: ts}` (verrou global sur le read-modify-write — une lecture ne peut plus faire disparaître un message), `GET` d'historique renvoie la carte, nouveau `POST /api/chat/read` (400 sans vault/path, 403 DM hors pair, ACL vault pour un chat de fichier) marque la lecture et diffuse `chat_read` en SSE ; côté client `✓` envoyé / `✓✓` lu sur **mes** messages (exclut ma propre carte de lecture), bascule en direct via `onChatRead()` + toast « {user} a lu votre message », accusé émis à l'ouverture d'une conversation et à la réception d'un message déjà affiché. **BUG-109** : la vignette des tuiles de lien ne s'affichait jamais (CSP `img-src 'self'` + `og:image` relative résolue contre ObsiGate) → `_proxy_image()` téléverse l'image dans `chat_uploads` (same-origin, `urljoin` contre la page, garde SSRF, 2 Mo max, allow-list) et la carte reste affichée si le téléchargement échoue. Tests : pytest `test_file_chat.py` 45→**54** (`TestReadReceipts` 7, proxy image 2, 2 assertions de réponse adaptées), JSDOM `filechat.test.mjs` 16→**19** ; ruff 0, mypy 0 (113 fichiers), validate-imports 42/371, unit 13/13 | ✅ livré (en attente vérif utilisateur) |
| 2026-10-09 | BUG-110 | Correction + refonte UI chat | `frontend/js/filechat.js`, `frontend/js/ui.js`, `frontend/js/viewer.js`, `frontend/style.css`, `frontend/locales/{fr,en}.json`, `tests/frontend/filechat.test.mjs`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-110 — refonte visuelle du chat** : (1) les boutons de pièce jointe deviennent des **icônes seules** (24 px, `aria-label`) rangées **sous l'image, alignées à droite** — plus de gros boutons à libellé ; (2) le clic sur une image ouvre le **même visualiseur que pour une image de vault** (zoom/pan/plein écran) mais dans un **onglet de l'app** (`TabManager.openChatImage()`, branche `chatImage` de `activate()`, `renderImageViewer()` accepte désormais une `url` directe : une pièce jointe n'est pas dans un vault) au lieu d'un onglet du navigateur ; (3) **point rouge pulsant sur l'icône chat** de la sidebar tant qu'il reste des non-lus (classe `chat-has-unread`, en plus du badge chiffré) ; (4) **notification OS** (`Notification`) quand l'app n'est pas au premier plan (`document.hidden`/hors focus), permission demandée au premier clic d'ouverture du chat ; (5) la suppression n'est plus mensongère : le `DELETE` est **différé de 10 s**, la ligne passe à « Ce message a été supprimé » avec « Annuler » — annuler réarme le message (l'ancien code restaurait une copie DOM d'un message déjà détruit côté serveur en affichant « Message restauré ») ; échec réseau → le message revient. Vérifié : `filechat.test.mjs` 25/25 (dont 6 tests #110 : icônes seules, clic image → `openChatImage`, aucun `DELETE` pendant la fenêtre d'annulation, notification OS), `validate-imports` 42 modules / 372 exports 0 erreur, `unit.test.mjs` 13/13 | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-10-09 | #193 | Fonctionnalité | `backend/file_chat.py`, `backend/schemas.py`, `frontend/js/filechat.js`, `frontend/style.css`, `tests/test_file_chat.py`, `tests/frontend/filechat.test.mjs`, `docs/features/file-chat-169.md`, `CHANGELOG.md` | **#193 — un post du chat s'affiche comme un document markdown** : le serveur rend le texte de chaque message avec le **pipeline des documents** (`backend/render.py::_render_markdown` : mistune — tableaux, listes de tâches, notes —, wikilinks, masquage des secrets #188, **sanitizer BUG-021**) et le renvoie dans un champ `html` (ajouté à `ChatMessageItem`, sinon `response_model` le filtrait). Le rendu est calculé **à la lecture** (`get_messages`) et à l'ajout (`_append`) — il n'est donc **jamais persisté** dans `data/chats/*.json` — et l'écho SSE part avec : les autres clients voient le rendu sans refresh. Côté front, `_fillBody()` injecte le HTML dans un `<div class="md-content">` puis appelle `safeHighlight()` sur chaque `pre code` (highlight.js + alias de langages, exactement comme le viewer ; mermaid non déclenché). Repli intact si `html` manque (écho optimiste → texte brut + URL cliquable) et un échec de rendu n'emporte jamais le message. CSS : `.file-chat-md` annule le `pre-wrap` hérité de la bulle (les retours à la ligne du HTML source produisaient des lignes vides) et compacte `.md-content` (`pre`/`table` en `overflow-x: auto`). Vérifié : `test_file_chat.py` 54→**61** (`TestMarkdownRendering` ×7 : html à l'ajout et à la lecture, **absent du JSON**, classe `language-xxx`, listes, `<script>` neutralisé, routes POST/GET, broadcast SSE), `filechat.test.mjs` 25→**27**, suite 1689 passed, ruff 0, mypy 0 (113 fichiers), validate-imports 42 modules / 372 exports | ✅ livré (en attente vérif utilisateur) |
---
## 📜 Historique des bugs résolus
+1 -1
View File
@@ -1,6 +1,6 @@
# ObsiGate — Roadmap
> **Version :** 2.62.0 | **Dernière mise à jour :** 2026-10-09
> **Version :** 2.63.0 | **Dernière mise à jour :** 2026-10-09
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
> vers les fonctionnalités livrées.
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
+48
View File
@@ -211,6 +211,54 @@ carte reste (jamais de message perdu).
direct + notification + pas d'écho, textarea (tagName/rows, hauteur
pilotée, `Entrée` intercepté / `Maj+Entrée` non).
## #193 — Rendu markdown des posts (2026-10-09)
Un post du chat s'affiche désormais **comme un document** : titres, listes,
tableaux, citations, blocs de code colorés — au lieu du texte brut avec liens
cliquables.
### A. Le rendu vient du serveur (pas de second moteur markdown)
- `backend/file_chat.py::_decorate()` ajoute un champ `html` à chaque message
en réutilisant `backend/render.py::_render_markdown()` — **le pipeline exact
des documents** : mistune (tableaux, strikethrough, notes, listes de tâches),
résolution des wikilinks, normalisation des sauts de ligne, masquage des
secrets (#188) et **sanitizer BUG-021** en sortie. Aucun markdown côté client,
donc aucun XSS nouveau à traiter : la sanitisation est déjà éprouvée.
- `ChatMessageItem.html` (schéma de réponse) : sans ce champ, `response_model`
filtrait la clé.
- Le `html` est calculé **à la lecture** (`get_messages()`) et à l'ajout
(`_append()`), donc il n'est **jamais persisté** dans `data/chats/*.json` :
un message reste du texte, le rendu suit les évolutions du pipeline.
- L'écho SSE part avec le `html` : les autres clients voient le rendu sans
attendre un refresh.
- Un échec de rendu n'emporte pas le message : repli silencieux (`html = ""`),
le client affiche le texte brut.
### B. Frontend : `.md-content` + highlight.js
- `_fillBody()` injecte le `html` dans un `<div class="md-content">` (la
typographie des documents) puis appelle `safeHighlight()` sur chaque
`pre code` — le même helper (et les mêmes alias de langages) que le viewer.
Mermaid n'est pas déclenché dans le chat.
- Repli intact : un message sans `html` (écho optimiste d'un autre client)
repasse par l'ancien chemin texte + URL cliquable.
- CSS : `.file-chat-body.file-chat-md` annule le `white-space: pre-wrap` hérité
de la bulle (sinon les retours à la ligne du HTML source créaient des lignes
vides) et compacte `.md-content` (tailles, marges, `pre`/`table` en
`overflow-x: auto`) pour tenir dans une bulle de 92 % de large.
### Tests #193
- pytest `TestMarkdownRendering` (7) : `html` présent à l'ajout **et** à la
lecture, `html` **absent** du JSON stocké, classe `language-xxx` conservée
(ce sur quoi hljs se branche), tableaux/listes, `<script>` neutralisé,
routes `POST/GET` (chat de fichier + chat général) et écho SSE porteur du
`html`. Suite : 1689 passed.
- JSDOM `filechat.test.mjs` +2 (27) : post avec `html` → `.md-content`,
`<strong>`, `pre code.language-python` ; post sans `html` → texte brut et URL
cliquable conservés.
## Décisions
- **SSE plutôt qu'un second WebSocket** : le transport de #62 (EventSource