diff --git a/CHANGELOG.md b/CHANGELOG.md index 21ee944..b32c69a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/), et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). > **En cours de développement** : les changements à venir sont listés dans la section -> [Unreleased](#unreleased). La dernière version livrée est **2.62.0**. +> [Unreleased](#unreleased). La dernière version livrée est **2.63.0**. --- @@ -14,6 +14,32 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). --- +## [2.63.0] — 2026-10-09 + +### Ajouté + +- **#193 — Chat : un post s'affiche comme un document markdown** + - Le rendu vient du **serveur** : `backend/file_chat.py` ajoute un champ + `html` à chaque message en réutilisant le pipeline des documents + (`backend/render.py::_render_markdown` → mistune + wikilinks + masquage des + secrets + **sanitizer BUG-021**). Pas de second moteur markdown côté client, + donc pas de nouvelle surface XSS. + - Titres, listes, tableaux, citations, notes, listes de tâches et **blocs de + code colorés** : le front injecte le HTML dans un `.md-content` puis passe + `safeHighlight()` (highlight.js + alias de langages) sur chaque `pre code`, + exactement comme la visionneuse de documents. + - Le `html` est calculé **à la lecture** (et à l'ajout, pour l'écho SSE) : + rien n'est persisté dans `data/chats/*.json`, un message reste du texte. + - Repli conservé : sans `html`, le texte brut + liens cliquables s'affichent + comme avant ; un échec de rendu n'emporte jamais le message. + - CSS : `.file-chat-md` annule le `pre-wrap` de la bulle (sinon les retours à + la ligne du HTML source devenaient des lignes vides) et compacte + `.md-content` (tailles, `pre`/`table` défilants) pour une bulle de chat. + - Tests : pytest `TestMarkdownRendering` +7 (54 → **61** dans + `test_file_chat.py`), JSDOM `filechat.test.mjs` +2 (25 → **27**). + +--- + ## [2.62.0] — 2026-10-09 ### Modifié diff --git a/README.fr.md b/README.fr.md index 8563c8e..0227ef5 100644 --- a/README.fr.md +++ b/README.fr.md @@ -4,7 +4,7 @@ **Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive. -[]() +[]() [](https://opensource.org/licenses/MIT) [](https://www.docker.com/) [](https://www.python.org/) @@ -976,8 +976,8 @@ Ce projet est sous licence **MIT** — voir le fichier [LICENSE](LICENSE) pour l ## 📝 Changelog -Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.62.0). +Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.63.0). --- -*Projet : ObsiGate | Version : 2.62.0 | Dernière mise à jour : Septembre 2026* +*Projet : ObsiGate | Version : 2.63.0 | Dernière mise à jour : Septembre 2026* diff --git a/README.md b/README.md index 0eb56ed..b014562 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **Ultra-light web gateway for your Obsidian vaults** — Access, browse, and search all your Obsidian notes from any device via a modern, responsive web interface. -[]() +[]() [](https://opensource.org/licenses/MIT) [](https://www.docker.com/) [](https://www.python.org/) @@ -1151,8 +1151,8 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) ## 📝 Changelog -See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.62.0). +See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.63.0). --- -*Project: ObsiGate | Version: 2.62.0 | Last updated: September 2026* +*Project: ObsiGate | Version: 2.63.0 | Last updated: September 2026* diff --git a/VERSION b/VERSION index 995366c..e604dbd 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.62.0 +2.63.0 diff --git a/backend/file_chat.py b/backend/file_chat.py index c69cfd4..78a4022 100644 --- a/backend/file_chat.py +++ b/backend/file_chat.py @@ -29,6 +29,8 @@ from urllib.parse import urljoin, urlparse import httpx +from backend.render import _render_markdown + logger = logging.getLogger("obsigate.file_chat") CHAT_DIR = Path("data/chats") @@ -96,8 +98,28 @@ def _write(file: Path, doc: dict[str, Any]) -> None: def get_messages(vault: str, path: str) -> list[dict[str, Any]]: - """Return the chat history for *(vault, path)* (chronological).""" - return list(_read(vault, path).get("messages", [])) + """Return the chat history for *(vault, path)* (chronological). + + Every message carries its rendered ``html`` (#193): same markdown pipeline + as a document (mistune + sanitizer), computed on read so a template change + applies to the whole history without rewriting the JSON store. + """ + return [_decorate(m, vault) for m in _read(vault, path).get("messages", [])] + + +def _decorate(msg: dict[str, Any], vault: str) -> dict[str, Any]: + """Return a copy of *msg* with its sanitized markdown ``html`` (#193). + + The stored message is left untouched (``html`` is never persisted). A + rendering failure must never break the chat: the message goes out with an + empty ``html`` and the client falls back to plain text. + """ + try: + html = _render_markdown(msg.get("text", ""), vault) + except Exception as e: # pragma: no cover - defensive + logger.warning("chat markdown rendering failed: %s", e) + html = "" + return {**msg, "html": html} def add_message( @@ -147,7 +169,7 @@ def _append( messages = messages[-MAX_MESSAGES:] doc["messages"] = messages _write(_chat_file(vault, path), doc) - return msg + return _decorate(msg, vault) # #193 — le html part avec l'écho SSE # --- #192 : accusé de réception --------------------------------------------- diff --git a/backend/schemas.py b/backend/schemas.py index 455d1a2..1233857 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -166,6 +166,13 @@ class ChatMessageItem(BaseModel): default=None, description="Link preview card {url, title, description, image, site} (#191)", ) + html: str = Field( + default="", + description=( + "Rendered (mistune + sanitized) HTML of ``text``, like a document " + "preview — tables, lists, fenced code blocks (#193)" + ), + ) class ChatHistoryResponse(BaseModel): diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock index 2efe040..2e34d66 100644 --- a/desktop/Cargo.lock +++ b/desktop/Cargo.lock @@ -2626,7 +2626,7 @@ dependencies = [ [[package]] name = "obsigate-desktop" -version = "2.62.0" +version = "2.63.0" dependencies = [ "chrono", "env_logger", diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index c17a813..e3d47b5 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "obsigate-desktop" -version = "2.62.0" +version = "2.63.0" description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian" authors = ["Bruno Charest"] edition = "2021" diff --git a/desktop/tauri.conf.json b/desktop/tauri.conf.json index faa273e..f10c8ee 100644 --- a/desktop/tauri.conf.json +++ b/desktop/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json", "productName": "ObsiGate", - "version": "2.62.0", + "version": "2.63.0", "identifier": "com.obsigate.desktop", "build": { "frontendDist": "../frontend", diff --git a/docs/ISSUES_TODOLIST.md b/docs/ISSUES_TODOLIST.md index cf8d610..6a62a24 100644 --- a/docs/ISSUES_TODOLIST.md +++ b/docs/ISSUES_TODOLIST.md @@ -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 `