Compare commits

...
3 Commits
Author SHA1 Message Date
bruno fb2d83e9e3 feat: guide d'utilisation - couverture complete, telechargement MD/PDF, section Architecture Mermaid, guide desktop elargi (#105, BUG-067)
CI / lint (push) Successful in 1m40s
CI / security (push) Successful in 1m6s
CI / test (push) Failing after 1m52s
CI / build (push) Skipped
CI / e2e (push) Skipped
2026-09-18 13:06:30 -04:00
bruno 82f6b4a791 fix: icones manquantes dans la table des matieres de la configuration (BUG-066)
CI / lint (push) Successful in 1m40s
CI / security (push) Successful in 1m4s
CI / test (push) Successful in 3m2s
CI / build (push) Successful in 1m1s
CI / e2e (push) Successful in 11m53s
2026-09-18 10:14:41 -04:00
bruno 7e1f5d6852 fix: fixture E2E manquante diagram-app-export.excalidraw (test de regression BUG-064)
CI / lint (push) Successful in 1m38s
CI / security (push) Successful in 1m4s
CI / test (push) Successful in 3m26s
CI / build (push) Successful in 1m5s
CI / e2e (push) Successful in 11m24s
2026-09-18 09:23:22 -04:00
27 changed files with 2359 additions and 48 deletions
+56 -1
View File
@@ -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.12.0**.
> [Unreleased](#unreleased). La dernière version livrée est **2.13.0**.
---
@@ -14,6 +14,61 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
---
## [2.13.0] — 2026-09-18
### Ajouté
- **#105 - Guide d'utilisation : audit de couverture, téléchargement Markdown/PDF,
section Architecture** : le guide intégré est aligné sur l'application réelle après
~35 features livrées. Huit nouvelles sections — **🏗️ Architecture** (diagramme Mermaid
des grandes composantes : clients SPA/PWA/Tauri → serveur FastAPI REST, index de
recherche, rendu markdown, IA, MCP, WebSocket, webhooks → vaults, `data/*.json`,
backups), **📊 Diagrammes** (Mermaid : zoom, plein écran, copie SVG/code, thèmes),
**⭐ Bibliothèque** (signets, recherches sauvegardées, backlinks & graphe, conflits
Syncthing, pièces jointes), **📴 Hors-ligne** (PWA, file d'attente IndexedDB, replay,
watcher), **👥 Collaboration** (Yjs/CRDT, curseurs awareness), **🖥️ Desktop** (Tauri,
updater signé, assistant premier lancement), **🔌 API** (OpenAPI 3.1 : `/docs`,
`/redoc`, `/api`, `/openapi.json`, auth Bearer/cookie, serveur MCP, automatisation)
et **🌍 Multilingue** — plus des compléments dans les sections existantes (recherche
sémantique hybride, notifications web push, export PDF, export HTML/ePub/ZIP,
anti-doublons d'upload, vue multi-panneaux, MFA TOTP/WebAuthn, tableau de bord
admin). Nouveau **`GET /api/guide/download?format=md|pdf&lang=fr|en`** (tag OpenAPI
« Guide ») : le document est généré depuis la modale d'aide réelle et les locales,
il reflète donc exactement le guide affiché, dans la langue de l'utilisateur ;
boutons **Markdown** et **PDF** ajoutés dans l'en-tête du guide. En **desktop**
(Tauri, `body.desktop-mode`) et sur grand écran web, le guide s'élargit
(conteneur jusqu'à 1760 px, contenu 1280–1440 px) pour une lecture confortable.
Source de vérité du nouveau contenu : `scripts/guide_content.py` (locales générées,
jamais éditées à la main). Tests : `tests/test_guide.py` (11).
### Corrigé
- **BUG-067 - Guide : entrée « 📱 Mobile » morte** : le sommaire pointait vers
`#help-mobile-editor`, section inexistante (l'édition mobile n'était qu'un `<h3>`
de la section Édition). Le bloc est devenu une section dédiée ancrée ; l'ancre
`#help-ia` (également morte) a été corrigée en `#help-ai` et un attribut
`data-i18n-placeholder` dupliqué nettoyé. Garde-fou : `test_guide_nav_has_no_dead_anchors`.
---
## [2.12.2] — 2026-09-18
### Corrigé
- **BUG-066 — Configuration : icônes manquantes dans la table des matières** :
les entrées « Fichiers cachés » et « Partages publics » du sommaire de la page de
configuration n'affichaient pas d'icône (les titres de section, eux, en avaient une).
Les libellés i18n `config.section_hidden` (🗂️) et `config.section_shares` (📤) sont
alignés sur leurs titres de section, en FR **et** EN. Test de non-régression ajouté
dans `tests/frontend/unit.test.mjs` (toutes les entrées du sommaire doivent porter une
icône dans les deux langues).
---
## [2.12.1] — 2026-09-18
---
## [2.12.0] — 2026-09-18
### Ajouté
+4 -3
View File
@@ -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.
[![Version](https://img.shields.io/badge/Version-2.12.0-blue.svg)]()
[![Version](https://img.shields.io/badge/Version-2.13.0-blue.svg)]()
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/)
[![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/)
@@ -57,6 +57,7 @@
- **🤖 AI Editor intégré** — Éditeur CodeMirror 6 avec toolbar IA : amélioration, correction, traduction, génération, réécriture personnalisée, toolbox (liste, tableau, frontmatter, canvas) — multi-provider DeepSeek/OpenRouter/Gemini
- **🧩 Serveur MCP & agent IA** — Serveur Model Context Protocol intégré (`/mcp`) et assistant avec function calling : lisez, cherchez et modifiez vos vaults depuis Claude Desktop, Cursor… avec confirmations two-step, permissions par vault, rate limiting et redaction des secrets ([guide](docs/MCP_GUIDE.md))
- **👥 Collaboration temps réel** — Édition simultanée d'un même document (Yjs/CRDT) : curseurs distants colorés, indicateur de présence, fusion sans conflit, reconnexion automatique et persistance serveur ([détail](docs/features/collaboration.md))
- **📖 Guide d'utilisation intégré** — Aide complète en FR/EN accessible depuis le menu Options : interface, navigation, recherche, fichiers, IA, sécurité, API & intégrations (OpenAPI, MCP), hors-ligne, collaboration, desktop, plus une section **Architecture** avec diagramme Mermaid ; téléchargeable en **Markdown** et **PDF** dans la langue courante ([détail](docs/features/guide-coverage-105.md))
- **📱 Éditeur mobile natif** — Édition optimisée pour le tactile : barre d'outils Markdown flottante (gras/italique/code/liste/lien), bouton « Coller » persistant (contournement iOS), zoom par pincement et hauteur ajustable, raccourcis swipe (liens entrants / table des matières) et mode lecture plein écran avec navigation entre fichiers ([détail](docs/features/mobile-editor.md))
- **🗺️ Vue graphe interactive** — Canvas force-directed avec Barnes-Hut O(n log n), filtres (tag, type), profondeur, mode focus, historique de navigation ←→↑, export PNG, aperçu au survol (Ctrl+click)
- **🗂️ Multi-vault** : Visualisez plusieurs vaults Obsidian simultanément
@@ -926,8 +927,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.12.0).
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.13.0).
---
*Projet : ObsiGate | Version : 2.12.0 | Dernière mise à jour : Juin 2026*
*Projet : ObsiGate | Version : 2.13.0 | Dernière mise à jour : Juin 2026*
+4 -3
View File
@@ -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.
[![Version](https://img.shields.io/badge/Version-2.12.0-blue.svg)]()
[![Version](https://img.shields.io/badge/Version-2.13.0-blue.svg)]()
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/)
[![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/)
@@ -50,6 +50,7 @@
- **🤖 Integrated AI Editor** — CodeMirror 6 editor with AI toolbar: improve, correct, translate, generate, custom rewrite, toolbox (list, table, frontmatter, canvas) — multi-provider DeepSeek/OpenRouter/Gemini
- **🧩 MCP Server & AI Agent** — Built-in Model Context Protocol server (`/mcp`) and tool-calling assistant: read, search and edit your vaults from Claude Desktop, Cursor… with two-step confirmations, per-vault permissions, rate limiting and secret redaction ([guide](docs/MCP_GUIDE.md))
- **👥 Real-time Collaboration** — Simultaneous editing of the same document (Yjs/CRDT): colored remote cursors, presence indicator, conflict-free merge, automatic reconnection and server-side persistence ([details](docs/features/collaboration.md))
- **📖 Built-in User Guide** — Complete FR/EN help from the Options menu: interface, navigation, search, files, AI, security, API & integrations (OpenAPI, MCP), offline, collaboration, desktop, plus an **Architecture** section with a Mermaid diagram; downloadable as **Markdown** and **PDF** in the current language ([details](docs/features/guide-coverage-105.md))
- **📱 Native Mobile Editor** — Touch-optimised editing: floating Markdown toolbar (bold/italic/code/list/link), persistent Paste button (iOS workaround), pinch-zoom font & adjustable height, swipe shortcuts (backlinks / table of contents) and a full-screen reading mode with page navigation ([details](docs/features/mobile-editor.md))
- **🗺️ Interactive Graph View** — Canvas force-directed with Barnes-Hut O(n log n), filters (tag, type), depth, focus mode, navigation history ←→↑, export PNG, preview on hover (Ctrl+click)
- **🗂️ Multi-vault** : View multiple Obsidian vaults simultaneously
@@ -1095,8 +1096,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.12.0).
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.13.0).
---
*Project: ObsiGate | Version: 2.12.0 | Last updated: May 2026*
*Project: ObsiGate | Version: 2.13.0 | Last updated: May 2026*
+1 -1
View File
@@ -1 +1 @@
2.12.0
2.13.0
+526
View File
@@ -0,0 +1,526 @@
"""Génération du Guide d'utilisation téléchargeable en Markdown et PDF (#105).
Source unique de vérité : la modale ``#help-modal`` de ``frontend/index.html``
(comme dans l'application) + les blocs ``data-i18n`` résolus dans les locales
``frontend/locales/{fr,en}.json`` — le téléchargement reflète donc exactement
ce que voit l'utilisateur, dans sa langue.
Le Markdown est produit par un convertisseur HTML→MD minimal (stdlib) ; le
PDF passe par le moteur d'export existant (WeasyPrint) avec repli reportlab
quand les bibliothèques natives GTK manquent (Windows).
"""
from __future__ import annotations
import datetime
import html
import json
import logging
import re
from html.parser import HTMLParser
from pathlib import Path
logger = logging.getLogger("obsigate.guide")
ROOT = Path(__file__).resolve().parent.parent
INDEX_HTML = ROOT / "frontend" / "index.html"
LOCALES_DIR = ROOT / "frontend" / "locales"
VERSION_FILE = ROOT / "VERSION"
# Éléments décoratifs exclus des exports
_SKIP_CLASSES = {"help-hero-visual", "editor-modal", "help-nav"}
# En-tête HTML du guide (mode lecture)
_HEADER_BLOCK = "ObsiGate User Guide"
class Node:
"""Noeud DOM minimal (stdlib only)."""
__slots__ = ("attrs", "children", "parent", "tag")
def __init__(self, tag: str, attrs: dict[str, str | None], parent: Node | None = None):
self.tag = tag
self.attrs = attrs
self.children: list[Node | str] = []
self.parent = parent
def cls(self) -> str:
return self.attrs.get("class") or ""
def i18n(self) -> str | None:
v = self.attrs.get("data-i18n")
return v if isinstance(v, str) else None
def find_all(self, tag: str) -> list[Node]:
out: list[Node] = []
for c in self.children:
if isinstance(c, Node):
if c.tag == tag:
out.append(c)
out.extend(c.find_all(tag))
return out
_VOID_TAGS = {"br", "img", "hr", "input", "meta", "link"}
class _TreeBuilder(HTMLParser):
"""Constructeur d'arbre tolérant (ignore les balises orphelines)."""
def __init__(self) -> None:
super().__init__(convert_charrefs=True)
self.root = Node("#root", {})
self.cur = self.root
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
a = {k: v for k, v in attrs}
node = Node(tag, a, self.cur)
self.cur.children.append(node)
if tag not in _VOID_TAGS:
self.cur = node
def handle_startendtag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
a = {k: v for k, v in attrs}
self.cur.children.append(Node(tag, a, self.cur))
def handle_endtag(self, tag: str) -> None:
n: Node | None = self.cur
while n is not None and n.tag != tag:
n = n.parent
if n is not None and n.parent is not None:
self.cur = n.parent
def handle_data(self, data: str) -> None:
self.cur.children.append(data)
# ---------------------------------------------------------------------------
# Extraction / cache
# ---------------------------------------------------------------------------
_cache: dict[tuple[str, str], tuple[tuple[float, int, float, int], bytes]] = {}
def _read_index_html() -> str:
return INDEX_HTML.read_text(encoding="utf-8")
def _guide_fragment(index_html: str) -> str:
"""Le HTML de #help-modal…help-content jusqu'au footer du guide."""
start = index_html.index('id="help-modal"')
cstart = index_html.index('<div class="help-content">', start)
end = index_html.index('<div class="help-footer">', cstart)
return index_html[cstart:end]
def _locale_strings(lang: str) -> dict[str, str]:
path = LOCALES_DIR / (lang if lang in ("fr", "en") else "fr")
return json.loads(Path(path).with_suffix(".json").read_text(encoding="utf-8"))
def _signature() -> tuple[float, int, float, int]:
st = INDEX_HTML.stat()
lt = (LOCALES_DIR / "fr.json").stat()
return (st.st_mtime, st.st_size, lt.st_mtime, lt.st_size)
def _app_version() -> str:
try:
return VERSION_FILE.read_text(encoding="utf-8").strip() or "dev"
except OSError:
return "dev"
# ---------------------------------------------------------------------------
# Résolution i18n : un node portant data-i18n est REMPLACÉ par le contenu
# (HTML) de la locale — exactement comme _applyDOM() dans le navigateur.
# ---------------------------------------------------------------------------
def _resolve_i18n(node: Node, loc: dict[str, str]) -> list[Node | str]:
"""Retourne les children effectifs d'un node (locale si data-i18n[-html])."""
key = node.i18n() or node.attrs.get("data-i18n-html")
if not isinstance(key, str):
return node.children
value = loc.get(key)
if value is None:
# clé absente de la locale : garder le texte FR inline de index.html
return node.children
tb = _TreeBuilder()
tb.feed(f"<span>{value}</span>")
span = tb.root.children[0]
assert isinstance(span, Node)
return span.children
# ---------------------------------------------------------------------------
# Markdown
# ---------------------------------------------------------------------------
_WS_RE = re.compile(r"[ \t]*\n[ \t]*")
def _collapse(text: str) -> str:
return _WS_RE.sub(" ", text).strip()
def _md_inline(node: Node | str, loc: dict[str, str]) -> str:
if isinstance(node, str):
return _collapse(node)
tag = node.tag
kids = _resolve_i18n(node, loc)
inner = "".join(_md_inline(c, loc) for c in kids)
if tag == "br":
return " "
if tag in ("strong", "b"):
t = inner.strip()
return f"**{t}**" if t else ""
if tag in ("em", "i"):
if node.cls().startswith("lucide") or tag == "i" and not inner.strip():
return ""
t = inner.strip()
return f"*{t}*" if t else ""
if tag == "code":
t = inner.replace("`", "'").strip()
return f"`{t}`" if t else ""
if tag == "kbd":
t = inner.strip()
return f"`{t}`" if t else ""
if tag == "a":
href = node.attrs.get("href") or ""
t = inner.strip()
if href.startswith("http") and t:
return f"[{t}]({href})"
return t
if tag == "img":
alt = node.attrs.get("alt") or ""
return f"![{alt}]"
return inner
def _md_block(node: Node | str, out: list[str], loc: dict[str, str], depth: int = 0) -> None:
"""Remplit ``out`` (bloc courant) — ``pending`` gère listes imbriquées."""
if isinstance(node, str):
t = _collapse(node)
if t:
out.append(t)
return
if any(c in node.cls().split() for c in _SKIP_CLASSES):
return
tag = node.tag
if tag == "pre":
raw = _pre_text(node)
lang = "mermaid" if "mermaid" in raw[:40] or "language-mermaid" in _pre_classes(node) else ""
out.append(f"```{lang}\n{raw.rstrip()}\n```")
return
kids = _resolve_i18n(node, loc)
if tag in ("h1", "h2", "h3", "h4", "h5", "h6"):
level = int(tag[1])
text = _collapse("".join(_md_inline(c, loc) for c in kids))
if text:
out.append("#" * level + " " + text)
return
if tag == "p":
text = _collapse("".join(_md_inline(c, loc) for c in kids))
if text:
out.append(text)
return
if tag in ("ul", "ol"):
_md_list(kids, out, loc, tag, depth)
return
if tag == "table":
_md_table(node, out, loc)
return
# conteneurs neutres (section, div, span de bloc, li imbriqué…)
for c in kids:
_md_block(c, out, loc, depth)
def _md_list(items: list[Node | str], out: list[str], loc: dict[str, str], kind: str, depth: int) -> None:
n = 0
for li in items:
if isinstance(li, str):
continue
if li.tag == "li":
n += 1
marker = "- " if kind == "ul" else f"{n}. "
text_parts: list[str] = []
nested: list[Node] = []
for c in li.children:
if isinstance(c, Node) and c.tag in ("ul", "ol"):
nested.append(c)
else:
text_parts.append(_md_inline(c, loc))
line = _collapse("".join(text_parts))
if line:
out.append(" " * depth + marker + line)
for sub in nested:
_md_list(sub.children, out, loc, sub.tag, depth + 1)
elif li.tag in ("ul", "ol"):
_md_list(li.children, out, loc, li.tag, depth)
def _md_table(node: Node, out: list[str], loc: dict[str, str]) -> None:
rows = node.find_all("tr")
if not rows:
return
grid: list[list[str]] = []
for tr in rows:
cells = []
for td in tr.children:
if isinstance(td, Node) and td.tag in ("td", "th"):
cells.append(_collapse("".join(_md_inline(c, loc) for c in td.children)).replace("|", "\\|") or " ")
if cells:
grid.append(cells)
if not grid:
return
width = max(len(r) for r in grid)
grid = [r + [" "] * (width - len(r)) for r in grid]
out.append("| " + " | ".join(grid[0]) + " |")
out.append("|" + "|".join([" --- "] * width) + "|")
for r in grid[1:]:
out.append("| " + " | ".join(r) + " |")
def _pre_text(node: Node) -> str:
"""Texte brut préservé d'un <pre> (les locales n'y touchent pas)."""
buf: list[str] = []
def walk(n: Node | str) -> None:
if isinstance(n, str):
buf.append(n)
return
for c in n.children:
walk(c)
walk(node)
return "".join(buf).strip("\n")
def _pre_classes(node: Node) -> str:
cls = node.cls()
for c in node.find_all("code"):
cls += " " + c.cls()
return cls
def build_guide_markdown(lang: str = "fr") -> bytes:
"""Guide complet en Markdown (UTF-8), dans la langue demandée."""
index_html = _read_index_html()
loc = _locale_strings(lang)
tree = _TreeBuilder()
tree.feed(_guide_fragment(index_html))
root = tree.root.children[0]
assert isinstance(root, Node)
blocks: list[str] = []
content = _guide_title_fr if lang == "fr" else _guide_title_en
blocks.append("# " + content)
for section in root.find_all("section"):
_md_block(section, blocks, loc)
blocks.append(
"---\n\n"
+ _export_footer(lang)
)
md = "\n\n".join(b for b in blocks if b.strip()) + "\n"
return md.encode("utf-8")
_guide_title_fr = "Guide d'utilisation ObsiGate"
_guide_title_en = "ObsiGate User Guide"
def _export_footer(lang: str) -> str:
loc = _locale_strings(lang)
template = loc.get("guide105.export_footer", "")
if "%s" not in template and "{" not in template:
template = "ObsiGate {version}"
today = datetime.datetime.now(tz=datetime.timezone.utc).date().isoformat()
return _collapse(template).format(version=_app_version(), date=today)
# ---------------------------------------------------------------------------
# HTML (pour le PDF) — mêmes règles, sortie balisée propre
# ---------------------------------------------------------------------------
def _html_inline(node: Node | str, loc: dict[str, str]) -> str:
if isinstance(node, str):
return html.escape(_collapse(node), quote=False)
tag = node.tag
kids = _resolve_i18n(node, loc)
inner = "".join(_html_inline(c, loc) for c in kids)
if tag == "br":
return " "
if tag in ("strong", "b") and inner.strip():
return f"<strong>{inner}</strong>"
if tag in ("em",) and inner.strip():
return f"<em>{inner}</em>"
if tag == "code":
t = inner.strip()
return f"<code>{t}</code>" if t else ""
if tag == "kbd":
t = inner.strip()
return f"<code>{t}</code>" if t else ""
if tag == "a":
href = node.attrs.get("href") or ""
if href.startswith("http"):
return f'<a href="{html.escape(href, quote=True)}">{inner}</a>'
return inner
return inner
def _html_block(node: Node | str, out: list[str], loc: dict[str, str]) -> None:
if isinstance(node, str):
t = _collapse(node)
if t:
out.append(f"<p>{html.escape(t, quote=False)}</p>")
return
if any(c in node.cls().split() for c in _SKIP_CLASSES):
return
tag = node.tag
if tag == "pre":
raw = html.escape(_pre_text(node), quote=False)
out.append(f"<pre><code>{raw}</code></pre>")
return
kids = _resolve_i18n(node, loc)
if tag in ("h2", "h3", "h4"):
text = _collapse("".join(_html_inline(c, loc) for c in kids))
if text:
out.append(f"<{tag}>{text}</{tag}>")
return
if tag == "p":
text = "".join(_html_inline(c, loc) for c in kids).strip()
if text:
out.append(f"<p>{text}</p>")
return
if tag in ("ul", "ol"):
out.append(_html_list(kids, loc, tag))
return
if tag == "table":
out.append(_html_table(node, loc))
return
for c in kids:
_html_block(c, out, loc)
def _html_list(items: list[Node | str], loc: dict[str, str], kind: str) -> str:
parts: list[str] = []
n = 0
for li in items:
if isinstance(li, str):
continue
if li.tag == "li":
n += 1
text_parts: list[str] = []
nested: list[Node] = []
for c in li.children:
if isinstance(c, Node) and c.tag in ("ul", "ol"):
nested.append(c)
else:
text_parts.append(_html_inline(c, loc))
line = "".join(text_parts).strip()
inner = line + "".join(_html_list(s.children, loc, s.tag) for s in nested)
if inner:
parts.append(f"<li>{inner}</li>")
elif li.tag in ("ul", "ol"):
parts.append(_html_list(li.children, loc, li.tag))
body = "".join(parts)
return f"<{kind}>{body}</{kind}>"
def _html_table(node: Node, loc: dict[str, str]) -> str:
rows_html: list[str] = []
for tr in node.find_all("tr"):
cells: list[str] = []
for td in tr.children:
if isinstance(td, Node) and td.tag in ("td", "th"):
tag = td.tag
inner = _collapse("".join(_html_inline(c, loc) for c in td.children))
cells.append(f"<{tag}>{inner}</{tag}>")
if cells:
rows_html.append("<tr>{}</tr>".format("".join(cells)))
return "<table>{}</table>".format("".join(rows_html))
def build_guide_html(lang: str = "fr") -> str:
"""Corps HTML autonome du guide (pour rendu PDF)."""
index_html = _read_index_html()
loc = _locale_strings(lang)
tree = _TreeBuilder()
tree.feed(_guide_fragment(index_html))
root = tree.root.children[0]
assert isinstance(root, Node)
blocks: list[str] = []
for section in root.find_all("section"):
_html_block(section, blocks, loc)
return "\n".join(blocks)
# ---------------------------------------------------------------------------
# PDF (WeasyPrint, repli reportlab)
# ---------------------------------------------------------------------------
def build_guide_pdf(lang: str = "fr") -> bytes:
lang_norm = lang if lang in ("fr", "en") else "fr"
title = _guide_title_fr if lang_norm == "fr" else _guide_title_en
loc = _locale_strings(lang_norm)
note = loc.get("guide105.arch_diagram_note", "")
footer = _export_footer(lang_norm)
try:
from backend.pdf_export import build_pdf_html, generate_pdf
body = build_guide_html(lang_norm)
body += (
f"<hr><p style='color:#777;font-size:11px'>{html.escape(note, quote=False)} — {html.escape(footer, quote=False)}</p>"
)
return generate_pdf(build_pdf_html(body, title), title)
except Exception as e: # WeasyPrint lève à l'import OU au rendu (GTK absent)
logger.warning("WeasyPrint indisponible pour le guide PDF (%s) — repli reportlab", e)
md = build_guide_markdown(lang_norm).decode("utf-8")
from backend.tools.documents import _render_reportlab_pdf
return _render_reportlab_pdf(md, title)
# ---------------------------------------------------------------------------
# Point d'entrée + cache
# ---------------------------------------------------------------------------
def get_guide_document(fmt: str, lang: str) -> tuple[bytes, str, str]:
"""Retourne (octets, media_type, filename) pour le format demandé.
``fmt`` : ``md`` | ``pdf``. Résultat mis en cache tant que index.html et
fr.json ne changent pas (les locales en ne divergent jamais sur les
structures ; la signature couvre l'essentiel).
"""
fmt = "pdf" if fmt == "pdf" else "md"
lang = "en" if lang == "en" else "fr"
key = (fmt, lang)
sig = _signature()
hit = _cache.get(key)
if hit and hit[0] == sig:
payload = hit[1]
else:
payload = build_guide_pdf(lang) if fmt == "pdf" else build_guide_markdown(lang)
_cache[key] = (sig, payload)
fname = f"ObsiGate-Guide-{_app_version()}-{lang}.{fmt}"
media = "application/pdf" if fmt == "pdf" else "text/markdown; charset=utf-8"
return payload, media, fname
+31
View File
@@ -1747,6 +1747,37 @@ def _safe_export_name(name: str) -> str:
return cleaned or "document"
@app.get(
"/api/guide/download",
response_class=Response,
responses={200: {"content": {"application/pdf": {}, "text/markdown": {}}}},
)
async def api_guide_download(
format: str = Query("md", description="Download format: 'md' or 'pdf'"),
lang: str = Query("fr", description="Guide language: 'fr' or 'en'"),
current_user=Depends(require_auth),
):
"""Download the in-app user guide as Markdown or PDF (#105).
The document is generated from the live help modal in index.html resolved
through the locale files, so it always mirrors exactly what the user sees.
"""
from backend.guide_export import get_guide_document
if format not in ("md", "pdf"):
raise HTTPException(status_code=400, detail="format doit être 'md' ou 'pdf'")
try:
payload, media, fname = get_guide_document(format, lang)
except Exception as e: # weasyprint/reportlab unavailable
logger.exception("guide export failed")
raise HTTPException(status_code=500, detail=f"Export impossible: {e}") from e
return Response(
content=payload,
media_type=media,
headers={"Content-Disposition": f'attachment; filename="{fname}"'},
)
@app.put("/api/file/{vault_name}/save", response_model=FileSaveResponse)
async def api_file_save(
vault_name: str,
+2
View File
@@ -30,6 +30,7 @@ TAGS_METADATA: list[dict[str, str]] = [
{"name": "Bookmarks", "description": "Recently opened files, bookmarks and saved searches."},
{"name": "Backups", "description": "Automatic file backups, diffs, restore, compression and purge."},
{"name": "Export", "description": "Export notes or whole vaults to HTML, Markdown bundle or ePub."},
{"name": "Guide", "description": "Download the in-app user guide as Markdown or PDF (mirrors the help modal, FR/EN)."},
{"name": "AI", "description": "AI-powered editor actions, provider status and model discovery."},
{"name": "BooksLM", "description": "Directory-scoped AI chat (NotebookLM-style) over a vault folder."},
{"name": "MCP", "description": "Model Context Protocol server (Streamable HTTP) exposing the shared AI tool layer to external clients (Claude Desktop, Cursor…)."},
@@ -98,6 +99,7 @@ _TAG_RULES: list[tuple[re.Pattern[str], str]] = [
(re.compile(r"^/api/backups"), "Backups"),
(re.compile(r"^/api/file/[^/]+/(backups|diff|restore)"), "Backups"),
(re.compile(r"^/api/export"), "Export"),
(re.compile(r"^/api/guide"), "Guide"),
(re.compile(r"^/api/file/[^/]+/pdf"), "PDF"),
(re.compile(r"^/api/search"), "Search"),
(re.compile(r"^/api/tags"), "Search"),
+1 -1
View File
@@ -2626,7 +2626,7 @@ dependencies = [
[[package]]
name = "obsigate-desktop"
version = "2.12.0"
version = "2.13.0"
dependencies = [
"chrono",
"env_logger",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "obsigate-desktop"
version = "2.12.0"
version = "2.13.0"
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
authors = ["Bruno Charest"]
edition = "2021"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
"productName": "ObsiGate",
"version": "2.12.0",
"version": "2.13.0",
"identifier": "com.obsigate.desktop",
"build": {
"frontendDist": "../frontend",
+4
View File
@@ -174,6 +174,8 @@ Avant de corriger quoi que ce soit, un agent IA doit :
| *BUG-063* | [🟡 IMPORTANT] Viewer PDF : la table des matières s'affiche mais ne navigue pas | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/viewer.js`, `tests/frontend/pdf-viewer.test.mjs`, `tests/e2e/pdf-viewer.spec.js` (fixture `test_vault/sample-pdf-toc.pdf`) | Ouvrir un PDF avec signets, puis cliquer une entrée de la TOC | Deux causes : (1) `contentWindow.location.hash='page=N'` n'atteint pas le document (lecteur PDF natif dans une fenêtre `about:blank`) ; (2) un simple changement de fragment sur `iframe.src` est une navigation same-document **ignorée** par le lecteur natif. `navigatePdfToPage()` (liens `data-page` + listeners, plus d'`onclick` inline) recharge réellement l'iframe via un paramètre de query qui change (`&_pdfpage=<ts>#page=N`). Test E2E : `src` finit par `&_pdfpage=<n>#page=3`. Vérifié en Chrome *headful* : page 1 → page 8 → page 1 (captures identiques au retour) | Le fragment seul ne suffisait pas : Chrome applique `#page=N` au **chargement**, pas lors d'un changement de fragment |
| *BUG-064* | [🟡 IMPORTANT] Éditeur Excalidraw : le diagramme ne s'affiche jamais (canvas vide), pour tout fichier `.excalidraw` / `.excalidraw.md` | 🟢 corrigé | P1 | 📱 frontend + ⚙️ backend | IA | `frontend/excalidraw-editor.html`, `backend/main.py`, `tests/frontend/excalidraw-viewer.test.mjs`, `tests/test_security_hardening.py`, `tests/e2e/excalidraw.spec.js`, `test_vault/diagram-app-export.excalidraw` | Ouvrir un `.excalidraw` (ou `.excalidraw.md`) dans ObsiGate | Deux causes : (1) la feuille de style d'Excalidraw n'était jamais chargée → éditeur non stylisé + `.excalidraw` sans hauteur fixe → boucle de resize jusqu'au plafond `2^25` (33 554 432 px) → scène blanche. Correctif : `<link>` CSS depuis esm.sh + `style-src` CSP autorisant `https://esm.sh`. (2) `appState.collaborators` objet JSON → `collaborators.forEach is not a function` ; `sanitizeAppState()` reconvertit en `Map` et écarte `width/height/offsetLeft/offsetTop`. | Vérifié navigateur : hauteur canvas 525 px (avant 33 554 432), dessin affiché, UI stylisée, 0 erreur. E2E + tests statiques CSP/CSS ajoutés. |
| *BUG-065* | [🟡 IMPORTANT] Éditeur Excalidraw : l'auto-save recharge la page en pleine édition | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/excalidraw-viewer.js`, `frontend/js/utils.js`, `frontend/excalidraw-editor.html`, `tests/frontend/excalidraw-viewer.test.mjs` | Ouvrir un `.excalidraw` puis modifier un élément : au bout de 2 s la vue se recharge | Chaque modification déclenchait un `PUT save` 2 s plus tard → SSE `index_updated` → `reloadExternalWrite` → `openFile` → **recréation de l'iframe** (refresh visible). Auto-save supprimée : sauvegarde explicite (bouton 💾 / Ctrl+S). `reloadExternalWrite` ignore le fichier si un iframe Excalidraw est ouvert (`iframe[data-excalidraw-vault/path]`). Le badge « Modified » ne réagit plus aux changements d'`appState` (resize/zoom) mais à la signature des éléments. | Vérifié Playwright : plus de refresh, badge stable après bascule plein écran. Test statique (absence de `requestSave`/`saveTimer`). |
| *BUG-066* | [🔵 MINEUR] Configuration : icônes manquantes dans la table des matières (« Fichiers cachés », « Partages publics ») | 🟢 corrigé | P3 | 📱 frontend | IA | `frontend/locales/{fr,en}.json` | Ouvrir Configuration → observer le sommaire : les entrées « Fichiers cachés » et « Partages publics » n'ont pas d'icône | `config.section_hidden` → « 🗂️ Fichiers cachés » / « 🗂️ Hidden files », `config.section_shares` → « 📤 Partages publics » (EN avait déjà l'icône). Test : `tests/frontend/unit.test.mjs` (+1 : toutes les entrées du sommaire portent une icône FR/EN) | Les libellés du sommaire utilisent des clés i18n distinctes des titres de section (`auto.f8ba6127`, `config.section_partages-publics`) qui, elles, avaient l'icône |
| *BUG-067* | [🔵 MINEUR] Guide d'utilisation : l'entrée « 📱 Mobile » du sommaire ne fait rien (section absente) | 🟢 corrigé | P3 | 📱 frontend | IA | `frontend/index.html` | Ouvrir le Guide → cliquer « 📱 Mobile » dans le sommaire : rien ne se passe | L'ancre `#help-mobile-editor` était présente dans la TOC mais aucune section `id="help-mobile-editor"` n'existait (l'édition mobile n'était qu'un h3 de `help-edition`). Fix #105 : section dédiée créée avec ancre + entrée de nav cohérente. | Vérifié par test statique `tests/test_guide.py::test_nav_anchors_resolve` |
| | | | | | | | | | | |
### TODOs techniques (améliorations / nouvelles tâches)
@@ -244,6 +246,8 @@ Avant de corriger quoi que ce soit, un agent IA doit :
| 2026-09-17 | BUG-064 (complément) | Correction | `frontend/style.css`, `tests/frontend/excalidraw-viewer.test.mjs`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-064 (complément)** : quand la barre de navigation gauche est masquée, le viewer Excalidraw restait borné à la colonne de lecture centrée de 1200 px. La règle `.sidebar.hidden ~ .content-wrapper .content-area { max-width: 1200px }` s'appliquait au viewer comme aux notes. Ajout de `.content-area:has(iframe[src*="excalidraw-editor.html"])` en `max-width: none; margin: 0` (même traitement que les viewers PDF/image, BUG-062). Vérifié Playwright (viewport 1400 px) : contenu 1115 → 1400 px, iframe 1035 → 1320 px, `max-width` calculé `none`. Test statique ajouté (`excalidraw-viewer.test.mjs` 9/9). | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-09-17 | BUG-065, #78 (complément) | Correction + feature | `frontend/js/excalidraw-viewer.js`, `frontend/js/utils.js`, `frontend/excalidraw-editor.html`, `tests/frontend/excalidraw-viewer.test.mjs`, `docs/features/excalidraw.md`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-065** : l'auto-save Excalidraw (débounce 2 s) déclenchait `PUT save` → SSE `index_updated` → `reloadExternalWrite` → `openFile` → recréation de l'iframe = refresh visible pendant le dessin. Auto-save retirée (`excalidraw-viewer.js` : plus de `requestSave`/`saveTimer`), sauvegarde explicite (bouton 💾 / Ctrl+S) ; `reloadExternalWrite` (utils.js) court-circuite le re-rendu si un iframe Excalidraw est ouvert sur ce fichier (attributs `data-excalidraw-vault`/`data-excalidraw-path`) ; le badge « Modified » suit désormais une signature des éléments (`id:versionNonce`) au lieu de tout `onChange` — resize/zoom/plein écran ne marquent plus le fichier modifié. **#78 (complément)** : bouton **plein écran** `#btn-fullscreen` dans la barre d'outils de l'éditeur (`requestFullscreen` sur le document de l'iframe) + iframe créée avec `allow="fullscreen" allowfullscreen`. Vérifié Playwright : bascule plein écran OK (`document.fullscreenElement` true→false), badge non modifié après bascule ; tests statiques `excalidraw-viewer.test.mjs` 12/12, validate-imports 38 modules, unit 9/9. | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-09-17 | #78 (complément) | UI | `frontend/excalidraw-editor.html`, `docs/features/excalidraw.md`, `CHANGELOG.md` | **#78 (complément)** : la barre d'outils de l'éditeur Excalidraw passe en **colonne d'icônes** (34×34 px, SVG seuls), **collée au bord droit** (`right: 0` ; `top: 45%` ; empilement vertical), avec `title`/`aria-label`. L'icône du bouton Save est remplacée par une coche pendant 1,2 s après une sauvegarde réussie. Badge « Modifié » réduit à une pastille. Vérifié Playwright : bord droit au bord de l'iframe, haut 45 %, 4 boutons empilés ; bascule plein écran OK, cycle d'icône Save + `PUT save` observés. | 🟢 livré (en attente vérif utilisateur) |
| 2026-09-18 | BUG-066 | Correction | `frontend/locales/fr.json`, `frontend/locales/en.json`, `tests/frontend/unit.test.mjs`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-066** : la table des matières de la page de configuration n'affichait aucune icône pour « Fichiers cachés » et « Partages publics ». Les libellés du sommaire proviennent de clés i18n (`config.section_hidden`, `config.section_shares`) distinctes des titres de section qui, eux, portaient déjà l'icône. Alignement : 🗂️ / 📤 en FR **et** EN. Test de non-régression : `unit.test.mjs` vérifie que **toutes** les entrées `.help-nav-link` du sommaire portent une icône dans les deux langues (17/17). Vérifié : `unit.test.mjs` 10/10, `validate-imports` 38 modules. | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-09-18 | #105, BUG-067 | Documentation + correction | `frontend/index.html`, `frontend/js/config.js`, `frontend/style.css`, `frontend/locales/{fr,en}.json`, `backend/guide_export.py`, `backend/main.py`, `tests/test_guide.py`, `docs/features/guide-coverage-105.md`, `CHANGELOG.md`, `docs/ROADMAP.md`, `docs/ISSUES_TODOLIST.md` | **#105** : audit complet de couverture du Guide d'utilisation — 8 nouvelles sections (Architecture + diagramme Mermaid, API & intégrations, Diagrammes Mermaid & Excalidraw, Hors-ligne & synchronisation, Collaboration temps réel, Application desktop, Bibliothèque & signets, Multilingue) et compléments (recherche sémantique, MFA/WebAuthn, notifications push, exports HTML/ePub/ZIP, PDF, vue multi-panneaux, admin). Téléchargement du guide en Markdown et PDF (`GET /api/guide/download?format=md|pdf`, FR/EN, rendu par le moteur d'export existant). Guide plus large en desktop. **BUG-067** : ancre morte `#help-mobile-editor` → section dédiée créée. | 🟢 corrigé (en attente vérif utilisateur)
---
+2 -1
View File
@@ -1,6 +1,6 @@
# ObsiGate — Roadmap
> **Version :** 2.12.0 | **Dernière mise à jour :** 2026-09-18
> **Version :** 2.13.0 | **Dernière mise à jour :** 2026-09-18
> **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)**
@@ -187,6 +187,7 @@
| 92 | Assistant IA — Écosystème d'outils phase 2 (recherche à clé, cache/retry, Playwright, crawl, Gitea/GitHub, documents XLSX/DOCX/CSV/PDF) | 2.10.0 | [features/ai-tools-roadmap.md](./features/ai-tools-roadmap.md) |
| 103 | Configuration — clés utilisateur des sources connectées & recherche à clé (page Configurations, `data/api_keys.json`, priorité sur l'env) | 2.11.0 | [features/ai-tools-roadmap.md](./features/ai-tools-roadmap.md) |
| 104 | Configuration — Redesign UI de la section « Clés API IA » : recherche fournisseurs, carte défaut 2 colonnes + badges de capacités, cartes dépliables, footer d'actions sticky | 2.12.0 | [features/ai-keys-ui.md](./features/ai-keys-ui.md) |
| 105 | Guide d'utilisation — audit de couverture complet, téléchargement Markdown/PDF, guide desktop élargi, section Architecture (Mermaid) + BUG-067 | 2.13.0 | [features/guide-coverage-105.md](./features/guide-coverage-105.md) |
---
+124
View File
@@ -0,0 +1,124 @@
# #105 — Guide d'utilisation : couverture, téléchargement MD/PDF, Architecture
> **Statut :** livré | **Version :** 2.13.0 | **Bugs liés :** BUG-067
> **Roadmap :** [docs/ROADMAP.md](../ROADMAP.md) · **Changelog :** [../../CHANGELOG.md](../../CHANGELOG.md)
## 1. Objectif
Après ~35 features livrées (#70→#104), le guide intégré (modale « Guide
d'utilisation », `#help-modal` de `frontend/index.html`) ne représentait plus
l'application : Mermaid, hors-ligne/PWA, collaboration Yjs, desktop Tauri,
exports HTML/ePub/ZIP, recherche sémantique, MFA/WebAuthn, push, split view,
API OpenAPI/MCP étaient absents. L'item couvre quatre livrables :
1. **Audit de couverture** — toutes les fonctionnalités visibles ET non visibles
(API, MCP, webhooks, endpoints de partage) sont documentées.
2. **Section Architecture** — diagramme Mermaid des grandes composantes.
3. **Téléchargement Markdown + PDF** du guide, dans la langue courante.
4. **Lecture desktop élargie** (mode Tauri + grands viewports web).
## 2. Source de vérité du contenu
Le contenu des nouvelles sections vit dans **`scripts/guide_content.py`** :
dictionnaire `CONTENT` (clé → FR, EN) + constructeurs HTML qui **ne peuvent
pas diverger** des locales (le FR inline est généré depuis `CONTENT`).
- `scripts/merge_guide_locales.py` → injecte les clés `guide105.*` dans
`frontend/locales/{fr,en}.json` (insertion textuelle, parité assertée).
- `scripts/insert_guide_sections.py` → insère sections/TOC/compléments dans
`index.html` (idempotent, préserve les fins de ligne, vérifie l'équilibre
`<section>` et l'absence d'ancre morte).
- **Règle :** ne jamais éditer les blocs `guide105.*` des JSON ni les sections
#105 de `index.html` à la main — modifier `guide_content.py` et relancer les
deux scripts (séquence figée, sinon HTML et locales divergent).
## 3. Rendu du guide dans l'app
Les textes portent `data-i18n="guide105.*"` ; `_applyDOM()` (i18n.js) les
remplit avec la locale courante. Les valeurs FR/EN contiennent du **HTML
minimal** (`<code>`, `<strong>`, `<a href="https://…">`) — sûre ici car ces
chaînes sont statiques dans le dépôt (jamais de contenu utilisateur ; même
convention que `help.desc_*` existant).
Le diagramme d'architecture est un bloc `<pre class="mermaid-code"><code
class="language-mermaid">` : à l'ouverture de la modale, `renderGuideMermaid()`
(config.js) appelle `renderMermaidBlocks()` (mermaid-viewer.js) sur la modale —
le viewer ne rend que les vues document, la modale est donc enrichie ici. Si le
CDN Mermaid n'est pas prêt, le bloc reste du code lisible et le rendu est
retenté à l'ouverture suivante (`data-mermaid-rendered` ne se pose qu'après
succès). Le MD exporté embarque le diagramme en fenced block ` ```mermaid `
(copiable, rendu par GitHub/VS Code/Obsidian) ; le PDF contient le même bloc.
## 4. Téléchargement (MD + PDF)
`backend/guide_export.py` : parseur stdlib (html.parser → arbre minimal) ;
extraction de `#help-modal`→`.help-content`, résolution i18n **identique à
_applyDOM** (un élément `data-i18n` est remplacé par la valeur locale, sinon le
FR inline sert de repli), conversion Markdown (titres, listes imbriquées,
tables, fenced code, gras/italique/code/kbd/links http) et HTML propre pour
WeasyPrint. PDF : pipeline d'export existant (`build_pdf_html` + `generate_pdf`),
repli `_render_reportlab_pdf` (markdown simplifié) quand GTK manque (Windows
nu) — même stratégie que le bouton « PDF » des documents (#92).
`get_guide_document(fmt, lang)` met en cache (octets+signature mtime/size de
`index.html` et `fr.json`) pour éviter de re-générer à chaque requête.
Endpoint `GET /api/guide/download?format=md|pdf&lang=fr|en`
(`require_auth`, tag OpenAPI **« Guide »**), `Content-Disposition: attachment`.
Côté UI : boutons « Markdown » / « PDF » dans l'en-tête de la modale
(`#help-download-md`/`#help-download-pdf`), handler `downloadGuide()` dans
`frontend/js/config.js` — fetch avec `AuthManager.getAuthHeaders()` +
credentials (identique à `viewer.downloadExport()`), blob → lien
téléchargeable, toasts i18n réutilisés (`viewer.export_*`).
## 5. Guide desktop élargi
`frontend/js/desktop.js::initDesktopIntegration()` ajoute `body.desktop-mode`
(une fois, après le garde Tauri). CSS (section « Help Modal: desktop » de
`style.css`) : conteneur 1760 px / 96 vw, contenu 1440 px, modale 94 vh ; le
même layout est accordé aux viewports ≥1400 px web via media-query (contenu
1280 px). Le mobile reste inchangé (≤768 px plein écran).
## 6. Couverture fonctionnelle du guide (après #105)
| Domaine app | Section du guide |
|---|---|
| Vaults, arborescence, filtres, breadcrumb | Interface, Navigation |
| Onglets, popout, split view | Onglets, Personnalisation |
| Recherche TF-IDF, opérateurs, facettes, sémantique, recherches sauvegardées, signets | Recherche, Bibliothèque |
| Tags, frontmatter | Tags |
| Fichiers, types supportés, CodeMirror, PDF, export HTML/ePub/ZIP, anti-doublons upload | Fichiers |
| Mermaid | Diagrammes |
| Excalidraw | Excalidraw |
| Éditeur, AI toolbar, BooksLM, commandes @//, skills, images, outils, historique | Édition, IA |
| Édition mobile | Mobile (section dédiée, BUG-067) |
| Graphe, backlinks | Bibliothèque, Graphe |
| Palette de commandes, raccourcis | Palette, Raccourcis |
| Partage public, PDF du partage, webhooks HMAC | Partage, Webhooks |
| Backups, diff, purge, audit log, gestionnaire | Sauvegardes et Audits |
| JWT/Argon2id, rate limit, MFA TOTP/WebAuthn, admin dashboard, secrets, CSP | Sécurité |
| PWA hors-ligne, IndexedDB queue, watcher, conflits Syncthing | Hors-ligne, Bibliothèque |
| Collaboration Yjs/CRDT, awareness, push VAPID | Collaboration, Interface |
| Tauri desktop (updater signé, wizard, fenêtrage) | Desktop |
| API REST OpenAPI (/docs, /redoc, /api, /openapi.json), MCP /mcp, automatisation | API & Intégrations |
| i18n FR/EN, export du guide multilingue | Multilingue |
| Architecture technique (couches, flux, données, déploiement) | **Architecture** (nouveau) |
| Plugins sandboxés | Plugins |
## 7. Tests & vérifications
- `tests/test_guide.py` (11) : ancre TOC→sections (garde-fou BUG-067),
sections #105 présentes, parité/présence des clés `guide105.*` FR=EN,
Markdown FR/EN (titres, mermaid, absence de balises résiduelles), PDF
(`%PDF`, taille), metadata `get_guide_document`, endpoint MD/PDF/400 via
TestClient, OpenAPI (path + tag « Guide »).
- Suite backend pytest : 1216 passed · ruff/mypy 0 erreur ·
`validate-imports` 38 modules · `unit.test.mjs` 10/10 · E2E complet CI.
- SW cache busting : `SW_VERSION` v21→v22.
## 8. Limites assumées
- Le PDF est généré côté serveur avec les polices système ; en l'absence de
GTK (Windows de dev) c'est le repli reportlab (sans tableaux) — la voie
WeasyPrint est celle des conteneurs Docker/prod.
- Le sommaire et les sections sont en dur dans `index.html` : tout ajout futur
de section passe par `guide_content.py` + les deux scripts (jamais à la main).
+247 -27
View File
@@ -2869,6 +2869,30 @@
Guide d'utilisation ObsiGate
</div>
<div class="editor-actions">
<button
class="editor-btn"
id="help-download-md"
title="Télécharger ce guide en Markdown"
data-i18n-attr="title:guide105.dl_md_title"
>
<i
data-lucide="file-code-2"
style="width: 16px; height: 16px"
></i>
<span data-i18n="guide105.dl_md">Markdown</span>
</button>
<button
class="editor-btn"
id="help-download-pdf"
title="Télécharger ce guide en PDF"
data-i18n-attr="title:guide105.dl_pdf_title"
>
<i
data-lucide="file-down"
style="width: 16px; height: 16px"
></i>
<span data-i18n="guide105.dl_pdf">PDF</span>
</button>
<button
class="editor-btn"
id="help-close"
@@ -2892,7 +2916,7 @@
type="text"
class="help-nav-search"
id="help-nav-search"
data-i18n-placeholder="help.search_placeholder" data-i18n-placeholder="help.search_placeholder" placeholder="Rechercher dans l'aide..."
data-i18n-placeholder="help.search_placeholder" placeholder="Rechercher dans l'aide..."
autocomplete="off"
spellcheck="false"
/>
@@ -2922,7 +2946,10 @@
data-i18n="help.nav_intro">📘 Introduction</a
>
</li>
<li>
<li>
<a href="#help-architecture" class="help-nav-link" data-i18n="guide105.nav_architecture">🏗️ Architecture</a>
</li>
<li>
<a href="#help-interface" class="help-nav-link"
data-i18n="help.nav_interface">🧭 Interface</a
>
@@ -2957,7 +2984,10 @@
data-i18n="help.nav_excalidraw">🎨 Excalidraw</a
>
</li>
<li>
<li>
<a href="#help-diagrams" class="help-nav-link" data-i18n="guide105.nav_diagrams">📊 Diagrammes</a>
</li>
<li>
<a href="#help-edition" class="help-nav-link"
data-i18n="help.nav_editing">✏️ Édition</a
>
@@ -2967,7 +2997,10 @@
data-i18n="help.nav_mobile_editor">📱 Mobile</a
>
</li>
<li>
<li>
<a href="#help-library" class="help-nav-link" data-i18n="guide105.nav_library">⭐ Bibliothèque</a>
</li>
<li>
<a href="#help-graphe" class="help-nav-link"
data-i18n="help.nav_graph">🗺️ Graphe</a
>
@@ -2986,8 +3019,17 @@
<a href="#help-raccourcis" class="help-nav-link"
data-i18n="help.nav_shortcuts">⌨️ Raccourcis</a
>
</li>
<li>
<a href="#help-offline" class="help-nav-link" data-i18n="guide105.nav_offline">📴 Hors-ligne</a>
</li>
<li>
<a href="#help-collab" class="help-nav-link" data-i18n="guide105.nav_collab">👥 Collaboration</a>
</li>
<li>
<a href="#help-desktop" class="help-nav-link" data-i18n="guide105.nav_desktop">🖥️ Desktop</a>
</li>
<li>
<a href="#help-partage" class="help-nav-link"
data-i18n="help.nav_sharing">🔗 Partage</a
>
@@ -3020,8 +3062,14 @@
<a href="#help-astuces" class="help-nav-link"
data-i18n="help.nav_tips">💡 Astuces</a
>
</li>
<li>
<a href="#help-api" class="help-nav-link" data-i18n="guide105.nav_api">🔌 API</a>
</li>
<li>
<a href="#help-languages" class="help-nav-link" data-i18n="guide105.nav_languages">🌍 Multilingue</a>
</li>
<li>
<a href="#help-plugins" class="help-nav-link"
data-i18n="help.nav_plugins">🧩 Plugins</a
>
@@ -3080,7 +3128,74 @@
</div>
</section>
<section class="help-section" id="help-interface">
<section class="help-section" id="help-architecture">
<h2 data-i18n="guide105.nav_architecture">🏗️ Architecture</h2>
<p data-i18n-html="guide105.arch_intro">ObsiGate est une application web complète construite en couches indépendantes, sans base de données externe : les notes vivent dans vos dossiers Obsidian, l'état applicatif dans des fichiers JSON de <code>data/</code>, l'index de recherche en mémoire.</p>
<pre class="mermaid-code"><code class="language-mermaid">flowchart TB
subgraph client["Clients"]
UI["SPA vanilla JS\n(frontend/js)"]
PWA["PWA hors-ligne\n(service worker + IndexedDB)"]
DESK["App desktop Tauri\n(fenêtre native)"]
end
subgraph server["Serveur FastAPI (Python 3.11)"]
API["REST /api\nJWT + Argon2id"]
IDX["Index recherche\nTF-IDF + embeddings"]
FS["Accès fichiers\nwatchdog + safe paths"]
PDF["Rendu markdown\nmistune + WeasyPrint"]
AI["Assistant IA\nproviders + outils"]
MCP["Serveur MCP\n/mcp (HTTP)"]
WS["WebSocket\ncollab Yjs + SSE"]
WH["Webhooks\nHMAC-SHA256"]
end
subgraph data["Données"]
V1["Vault 1 (dossier)"]
V2["Vault 2 (dossier)"]
CFG["data/*.json\nconfig, users, audit"]
BK[".obsigate-backup/\nbackups horodatés"]
end
UI -- HTTP --> API
PWA -- "cache + queue" --> API
DESK -- embarqué --> API
API --> IDX
API --> FS
API --> PDF
API --> AI
MCP --> AI
WS --> FS
FS --> V1
FS --> V2
IDX --> V1
IDX --> V2
BK --> V1
API --> CFG
API -- événements --> WH</code></pre>
<p data-i18n="guide105.arch_diagram_note">Le diagramme est interactif dans l'application : zoom, plein écran, copie SVG ou code.</p>
<h3 data-i18n="guide105.arch_h3_layers">Les grandes composantes</h3>
<ul>
<li>
<strong data-i18n="guide105.arch_lbl_fe">Frontend</strong><span data-i18n-html="guide105.arch_fe"> — SPA en JavaScript vanilla (modules ES), sans framework ni build npm : <code>frontend/js/</code> (~30 modules). Le CSS utilise des variables pour les thèmes.</span>
</li>
<li>
<strong data-i18n="guide105.arch_lbl_be">Backend</strong><span data-i18n="guide105.arch_be"> — serveur FastAPI (Python 3.11) : rendu markdown (mistune + wikilinks), recherche TF-IDF stemmisée (index inversé en mémoire), watchers watchdog, JWT + Argon2id, webhooks HMAC, export PDF (WeasyPrint).</span>
</li>
<li>
<strong data-i18n="guide105.arch_lbl_realtime">Temps réel & MCP</strong><span data-i18n-html="guide105.arch_rt"> — passerelle WebSocket (collaboration Yjs, notifications SSE/push) et serveur MCP (Streamable HTTP, <code>/mcp</code>) pour les clients externes.</span>
</li>
<li>
<strong data-i18n="guide105.arch_lbl_ai">Couche IA</strong><span data-i18n="guide105.arch_ai"> — assistants d'édition et BooksLM multi-providers (DeepSeek, OpenRouter, Gemini, Mistral…), bibliothèque d'outils (function calling, recherche web, crawl, lecture de documents) et embeddings optionnels pour la recherche sémantique.</span>
</li>
<li>
<strong data-i18n="guide105.arch_lbl_data">Données</strong><span data-i18n-html="guide105.arch_data"> — les vaults Obsidian sur disque (source de vérité), la configuration en JSON (<code>data/</code>), les backups horodatés (<code>.obsigate-backup/</code>), l'audit en JSON lines, les clés API chiffrées dans <code>data/api_keys.json</code>.</span>
</li>
<li>
<strong data-i18n="guide105.arch_lbl_deploy">Déploiement</strong><span data-i18n="guide105.arch_deploy"> — application desktop Tauri (Rust) embarquant le backend Python, conteneur Docker, ou PWA installable dans le navigateur (mode hors-ligne).</span>
</li>
</ul>
<h3 data-i18n="guide105.arch_h3_flux">Flux typique</h3>
<p data-i18n-html="guide105.arch_flux"> Un clic sur un fichier émet <code>GET /api/file/...</code> ; le backend résout le chemin en sécurité, parse le frontmatter, rend le markdown et renvoie le HTML ; le frontend enrichit l'affichage (Mermaid, coloration, wikilinks cliquables). Chaque écriture crée un backup avant application.</p>
</section>
<section class="help-section" id="help-interface">
<h2 data-i18n="help.nav_interface">🧭 Interface utilisateur</h2>
<h3 data-i18n="help.header_section">En-tête</h3>
@@ -3170,7 +3285,10 @@
avec wikilinks et images</span>
</li>
</ul>
</section>
<h3 data-i18n="guide105.h3_push">Notifications web (push)</h3>
<p data-i18n="guide105.push_p1">Autorisez les notifications (bouton 🔔 de l'en-tête) pour être averti des fins de synchronisation hors-ligne et des événements importants. La gestion des abonnements est dans les Configurations.</p>
<p data-i18n="guide105.push_p2">Basée sur la Web Push API (clés VAPID) ; fonctionne sur desktop et PWA mobile, sans service tiers : le serveur émet directement vers les endpoints push des navigateurs.</p>
</section>
<section class="help-section" id="help-navigation">
<h2 data-i18n="help.733f0559">🗺️ Navigation</h2>
@@ -3329,7 +3447,7 @@
<h2 data-i18n="help.adf30d78">🔍 Recherche</h2>
<h3 data-i18n="help.simple_search">Recherche simple</h3>
<p data-i18n="help.desc_search_intro">
<p data-i18n-html="help.desc_search_intro">
Tapez dans la barre de recherche en haut pour
lancer une recherche fulltext :
</p>
@@ -3382,7 +3500,7 @@
</li>
</ul>
<p>
<strong>Exemples</strong><span data-i18n="help.desc_examples">:
<strong>Exemples</strong><span data-i18n-html="help.desc_examples">:
<code>ext:sh</code> recherche dans les scripts
bash, <code>ext:py</code> dans les scripts
Python, <code>ext:md</code> dans les fichiers
@@ -3458,13 +3576,16 @@
<strong>Tri par date</strong><span data-i18n="help.desc_42a81347"> : Dernière
modification</span></li>
</ul>
</section>
<h3 data-i18n="guide105.h3_semantic">Recherche sémantique (hybride)</h3>
<p data-i18n="guide105.sem_p1">Activez le bouton « S » de la barre de recherche (ou Alt-S) pour combiner TF-IDF et similarité vectorielle (fusion RRF) : les concepts approchants (« velours » trouve « tissu doux ») remontent mieux.</p>
<p data-i18n="guide105.sem_p2">Le moteur d'embeddings (modèle multilingue) est optionnel : sans lui, un repli par hash conserve une recherche hybride fonctionnelle. Les vecteurs sont recalculés à chaque indexation du vault.</p>
</section>
<section class="help-section" id="help-tags">
<h2 data-i18n="help.2507242f">🏷️ Tags</h2>
<h3 data-i18n="help.5f87ea79">Tag cloud</h3>
<p data-i18n="help.desc_tag_cloud_intro">
<p data-i18n-html="help.desc_tag_cloud_intro">
L'onglet Tags de la sidebar affiche un nuage de
tags :
</p>
@@ -3486,15 +3607,15 @@
</ul>
<h3 data-i18n="help.2c03a7ba">Tags inline vs frontmatter</h3>
<p data-i18n="help.desc_two_tag_types">ObsiGate supporte deux types de tags :</p>
<p data-i18n-html="help.desc_two_tag_types">ObsiGate supporte deux types de tags :</p>
<ul>
<li>
<strong>Frontmatter YAML</strong><span data-i18n="help.desc_frontmatter">:
<strong>Frontmatter YAML</strong><span data-i18n-html="help.desc_frontmatter">:
<code>tags: [docker, linux]</code> ou
<code>tags: docker, linux</code></span>
</li>
<li>
<strong>Inline</strong><span data-i18n="help.desc_inline_tags">:
<strong>Inline</strong><span data-i18n-html="help.desc_inline_tags">:
<code>#docker</code> dans le contenu
markdown</span>
</li>
@@ -3504,13 +3625,13 @@
</ul>
<h3 data-i18n="help.63f024d4">Filtrage de tags template</h3>
<p data-i18n="help.desc_template_intro">
<p data-i18n-html="help.desc_template_intro">
Dans les Configurations, vous pouvez masquer les
tags de template :
</p>
<ul>
<li>
<strong>Patterns wildcards</strong><span data-i18n="help.desc_patterns">: Ex:
<strong>Patterns wildcards</strong><span data-i18n-html="help.desc_patterns">: Ex:
<code>#&lt;% ... %&gt;</code> ou
<code>#{{ ... }}</code></span>
</li>
@@ -3530,12 +3651,12 @@
<p data-i18n="help.desc_md_render">Les fichiers markdown sont rendus avec :</p>
<ul>
<li>
<strong>Wikilinks cliquables</strong><span data-i18n="help.desc_wikilinks">:
<strong>Wikilinks cliquables</strong><span data-i18n-html="help.desc_wikilinks">:
<code>[[lien]]</code> et
<code>[[lien|texte]]</code></span>
</li>
<li>
<strong>Images Obsidian</strong><span data-i18n="help.desc_images">: Support
<strong>Images Obsidian</strong><span data-i18n-html="help.desc_images">: Support
de <code>![[image.png]]</code></span>
</li>
<li>
@@ -3635,7 +3756,13 @@
.csv
</li>
</ul>
</section>
<h3 data-i18n="guide105.h3_pdf">Export PDF</h3>
<p data-i18n-html="guide105.pdf_p">Le bouton « PDF » d'un document le rend avec le même moteur que la vue (WeasyPrint) : titres, tableaux, listes et code sont conservés. Depuis un lien public, la route <code>/s/{token}/pdf</code> produit le même PDF.</p>
<h3 data-i18n="guide105.h3_exports">Export HTML / ePub / ZIP</h3>
<p data-i18n="guide105.exp_p">Le menu « Exporter » propose trois formats : HTML autonome (fichier unique, images incluses), ePub pour les liseuses et, pour un dossier, un bundle Markdown en ZIP — liens et ressources résolus pendant l'export.</p>
<h3 data-i18n="guide105.h3_dupe">Anti-doublons à l'upload</h3>
<p data-i18n="guide105.dupe_p">L'upload en masse (glisser-déposer un dossier sur la sidebar) compare chaque fichier au contenu existant : un fichier déjà présent est ignoré plutôt que dupliqué avec un suffixe « (1) ». Utile pour restaurer un vault sans créer de doublons.</p>
</section>
<!-- 🎨 Excalidraw -->
<section class="help-section" id="help-excalidraw">
@@ -3682,7 +3809,21 @@
</section>
<!-- ✏️ Édition -->
<section class="help-section" id="help-edition">
<section class="help-section" id="help-diagrams">
<h2 data-i18n="guide105.nav_diagrams">📊 Diagrammes</h2>
<p data-i18n-html="guide105.dia_intro">Les blocs <code>```mermaid</code> de vos notes sont rendus en diagrammes interactifs (Mermaid v11, chargé depuis un CDN).</p>
<ul>
<li data-i18n="guide105.dia_zoom">Zoom : boutons + / − dans la barre d'outils du diagramme.</li>
<li data-i18n="guide105.dia_fs">Plein écran : idéal pour les grandes matrices.</li>
<li data-i18n="guide105.dia_copy">Copie : exportez le SVG ou le code source (boutons dédiés).</li>
<li data-i18n="guide105.dia_toggle">Bascule Aperçu / Code pour éditer la source sans quitter la vue.</li>
<li data-i18n="guide105.dia_theme">Thème : le diagramme suit le thème clair/sombre de l'application.</li>
</ul>
<p data-i18n="guide105.dia_types">Types supportés : flowchart, sequence, class, state, ER, gantt, pie, journey, quadrant, radar, mindmap, timeline, C4, xychart, sankey — plus un préprocesseur qui comprend la syntaxe Obsidian.</p>
<p data-i18n-html="guide105.dia_excalidraw_ref">Les dessins à main levée (<code>.excalidraw</code>, <code>.excalidraw.md</code>) sont couverts dans la section 🎨 Excalidraw.</p>
</section>
<section class="help-section" id="help-edition">
<h2 data-i18n="auto.f346076a">✏️ Édition avancée</h2>
<h3 data-i18n="auto.695d9e47">Éditeur CodeMirror 6</h3>
@@ -3719,11 +3860,15 @@
<li>
<strong data-i18n="help.toolbar_section">Barre d'outils AI</strong><span data-i18n="help.desc_c8532869"> :
Complétion, réécriture, traduction (voir
section</span><a href="#help-ia" data-i18n="settings.ai">🤖 IA</a>)
section</span><a href="#help-ai" data-i18n="settings.ai">🤖 IA</a>)
</li>
</ul>
<h3 data-i18n="help.mobile_editor_title">📱 Édition mobile</h3>
</section>
<!-- 🗺️ Graphe -->
<section class="help-section" id="help-mobile-editor">
<h2 data-i18n="help.mobile_editor_title">📱 Édition mobile</h2>
<p data-i18n="help.mobile_editor_intro">
Sur téléphone et tablette, l'édition s'adapte au
tactile : barre d'outils flottante, gestes et mode
@@ -3760,8 +3905,21 @@
</ul>
</section>
<!-- 🗺️ Graphe -->
<section class="help-section" id="help-graphe">
<section class="help-section" id="help-library">
<h2 data-i18n="guide105.nav_library">⭐ Bibliothèque</h2>
<h3 data-i18n="guide105.lib_h3_bookmarks">Signets & récents</h3>
<p data-i18n="guide105.lib_bookmarks">Marquez un fichier d'un ★ (bouton Signet de la barre d'actions) : il rejoint la liste des signets du dashboard. Les fichiers récemment ouverts sont listés automatiquement dans l'onglet « Récents » de la sidebar, avec un filtre de recherche dédié.</p>
<h3 data-i18n="guide105.lib_h3_saved">Recherches sauvegardées</h3>
<p data-i18n="guide105.lib_saved">Enregistrez une recherche depuis la page de résultats pour la relancer en un clic depuis la sidebar : chaque recherche sauvegardée conserve ses opérateurs et filtres.</p>
<h3 data-i18n="guide105.lib_h3_backlinks">Backlinks & graphe</h3>
<p data-i18n="guide105.lib_backlinks">Le panneau Backlinks liste toutes les notes qui pointent vers le fichier ouvert. La vue Graphe (bouton 🕸️) affiche les liens entre fichiers : glissez les nœuds, zoomez à la molette, double-cliquez pour ouvrir une note.</p>
<h3 data-i18n="guide105.lib_h3_conflicts">Conflits de synchronisation</h3>
<p data-i18n="guide105.lib_conflicts">Si vous synchronisez le vault avec Syncthing, ObsiGate détecte les fichiers de conflit (copies « sync-conflict ») et propose de les comparer puis résoudre depuis la page dédiée du menu Options.</p>
<h3 data-i18n="guide105.lib_h3_attach">Fichiers joints & médias</h3>
<p data-i18n-html="guide105.lib_attach">Les images <code>![[image.png]]</code>, pièces jointes et médias (audio, vidéo, PDF intégrés) dans les notes sont rendus dans le viewer et indexés pour la recherche ; le bouton « Rescan attachments » de la configuration recrée l'index des pièces jointes.</p>
</section>
<section class="help-section" id="help-graphe">
<h2 data-i18n="help.9c7a0a8d">🗺️ Vue Graphe</h2>
<p data-i18n="help.desc_graph_intro">
La vue graphe offre une visualisation
@@ -3914,7 +4072,9 @@
rescan des vaults
</li>
</ul>
</section>
<h3 data-i18n="guide105.h3_panes">Vue multi-panneaux (split view)</h3>
<p data-i18n="guide105.panes_p">Le bouton « Diviser » de la barre d'actions ouvre le document dans un panneau jumeau ; empilez plusieurs panneaux pour comparer deux notes ou lire et éditer en parallèle. Les largeurs se règlent au bord des panneaux et sont mémorisées.</p>
</section>
<!-- ⌨️ Palette de commandes -->
<section class="help-section" id="help-palette">
@@ -4373,7 +4533,37 @@
</section>
<!-- 🔗 Partage -->
<section class="help-section" id="help-partage">
<section class="help-section" id="help-offline">
<h2 data-i18n="guide105.nav_offline">📴 Hors-ligne</h2>
<ul>
<li data-i18n="guide105.off_pwa">ObsiGate est une PWA : installez-la (icône d'installation de la barre d'adresse) pour l'ouvrir comme une application. Le service worker met en cache l'interface et vos derniers documents consultés.</li>
<li data-i18n="guide105.off_edit">Hors-ligne, vous pouvez lire les documents en cache et même les éditer : les modifications sont mises en file d'attente dans IndexedDB.</li>
<li data-i18n="guide105.off_sync">Au retour en ligne, la file se rejoue automatiquement (badge de synchronisation dans l'en-tête). Si la version serveur a divergé entre-temps, le fichier est marqué en conflit et la version serveur est préservée en backup.</li>
<li data-i18n="guide105.off_watch">Les modifications externes (Obsidian sur disque) sont détectées par le watcher : la vue se recharge sans perte de position, ou signale « modifié en externe » pendant une édition.</li>
</ul>
</section>
<section class="help-section" id="help-collab">
<h2 data-i18n="guide105.nav_collab">👥 Collaboration</h2>
<ul>
<li data-i18n="guide105.col_intro">Ouvrez un document en mode Édition : plusieurs personnes peuvent travailler simultanément sur le même fichier via un WebSocket Yjs (CRDT). Les modifications fusionnent sans verrou.</li>
<li data-i18n="guide105.col_cursors">Les curseurs et sélections des collaborateurs apparaissent avec une couleur et un nom par personne (awareness).</li>
<li data-i18n="guide105.col_save">La fusion est persistée côté serveur après 2 s d'inactivité ; chaque écriture crée un backup horodaté avant application.</li>
<li data-i18n="guide105.col_perm">Accès limité aux utilisateurs authentifiés disposant de la permission sur la vault.</li>
</ul>
</section>
<section class="help-section" id="help-desktop">
<h2 data-i18n="guide105.nav_desktop">🖥️ Desktop</h2>
<ul>
<li data-i18n="guide105.des_get">L'application desktop ObsiGate (Tauri) embarque le serveur Python : aucune installation de Docker nécessaire. Elle se télécharge sur la page des Releases du dépôt et se met à jour automatiquement (updater signé).</li>
<li data-i18n="guide105.des_wizard">Au premier lancement, un assistant demande le dossier de vos vaults (ou crée un vault de démonstration). Chaque document peut être détaché en fenêtre native séparée.</li>
<li data-i18n="guide105.des_data">Les données desktop restent dans le répertoire applicatif ; les vaults pointent sur vos dossiers existants. Toutes les fonctionnalités web (recherche, IA, partage) sont disponibles.</li>
<li data-i18n="guide105.des_native">Menu système natif, raccourci global optionnel pour afficher/masquer la fenêtre et jumplist des vaults récents.</li>
</ul>
</section>
<section class="help-section" id="help-partage">
<h2 data-i18n="help.8c65b2f2">🔗 Partage et Webhooks</h2>
<h3 data-i18n="help.public_shares">Publication publique</h3>
@@ -4956,7 +5146,11 @@ curl -X POST https://votre-serveur.com/webhook \
tourne avec UID 1000
</li>
</ul>
</section>
<h3 data-i18n="guide105.h3_mfa">MFA : TOTP, WebAuthn, codes de secours</h3>
<p data-i18n="guide105.mfa_p">Activez la double authentification dans Réglages → Profil : applications TOTP (Authy, Aegis…), clés de sécurité et passkeys (WebAuthn, y compris Windows Hello) et 10 codes de secours à conserver hors ligne. Chaque méthode s'active et se désactive indépendamment.</p>
<h3 data-i18n="guide105.h3_admin">Tableau de bord administrateur</h3>
<p data-i18n-html="guide105.admin_p">Le rôle admin ouvre une page dédiée <code>/admin.html</code> (bouton du menu Options) : statut du serveur en direct, utilisateurs, vaults, sessions actives et journal d'audit. Le CRUD utilisateurs est aussi disponible dans les Configurations.</p>
</section>
<section class="help-section" id="help-astuces">
<h2 data-i18n="help.686f8313">💡 Astuces et bonnes pratiques</h2>
@@ -5062,7 +5256,33 @@ curl -X POST https://votre-serveur.com/webhook \
</section>
<!-- 🧩 Plugins -->
<section class="help-section" id="help-plugins">
<section class="help-section" id="help-api">
<h2 data-i18n="guide105.nav_api">🔌 API</h2>
<p data-i18n="guide105.api_intro">ObsiGate expose une API REST couvrant toute l'application (vaults, fichiers, recherche, backups, export, IA, partage, admin), documentée en OpenAPI 3.1 :</p>
<ul>
<li data-i18n-html="guide105.api_docs_url"><code>/docs</code> — interface Swagger UI pour essayer les requêtes en direct.</li>
<li data-i18n-html="guide105.api_redoc"><code>/redoc</code> — référence alternative plus compacte.</li>
<li data-i18n-html="guide105.api_landing"><code>/api</code> — page de garde regroupant les endpoints par catégorie.</li>
<li data-i18n-html="guide105.api_schema"><code>/openapi.json</code> — le schéma machine, à importer dans Postman ou Insomnia.</li>
</ul>
<h3 data-i18n="guide105.api_h3_auth">Authentification</h3>
<p data-i18n-html="guide105.api_auth">Connectez-vous via <code>POST /api/auth/login</code> pour obtenir un token Bearer (le même jeton est accepté en cookie HttpOnly, ce qui permet aux clients navigateur d'utiliser <code>credentials: "include"</code>). Toutes les routes <code>/api/*</code> exigent ce jeton sauf mention contraire.</p>
<h3 data-i18n="guide105.api_h3_mcp">Serveur MCP</h3>
<p data-i18n-html="guide105.api_mcp">Les outils de l'assistant IA (lire, lister, chercher, ouvrir, écrire…) sont exposés à tout client MCP (Claude Desktop, Cursor, Cline…) sur <code>https://votre-instance/mcp</code> avec un token d'API. Configuration et exemples : <code>docs/MCP_GUIDE.md</code>.</p>
<h3 data-i18n="guide105.api_h3_autom">Automatisation</h3>
<p data-i18n-html="guide105.api_autom">Pour automatiser depuis l'extérieur : <code>GET /api/search?q=…</code> et <code>GET /api/file/{vault}?path=…</code> permettent d'indexer ou relire vos notes dans un autre outil ; les webhooks sortants (section 🪝) évitent le polling.</p>
</section>
<section class="help-section" id="help-languages">
<h2 data-i18n="guide105.nav_languages">🌍 Multilingue</h2>
<ul>
<li data-i18n="guide105.lng_how">L'interface est intégralement bilingue français / anglais. Réglages → Profil → Langue : le choix est enregistré sur votre compte et vous suit sur tous les appareils.</li>
<li data-i18n="guide105.lng_scope">Tout est traduit : menus, messages, notifications, et le présent guide. Les réponses de l'assistant IA suivent la langue de vos documents.</li>
<li data-i18n="guide105.lng_export">Les boutons Markdown / PDF de ce guide téléchargent la version dans votre langue.</li>
</ul>
</section>
<section class="help-section" id="help-plugins">
<h2 data-i18n="help.plugins_title">🧩 Plugins</h2>
<p data-i18n="help.plugins_intro">
Les plugins étendent ObsiGate : affichage personnalisé des
+59
View File
@@ -353,6 +353,7 @@ function initHelpModal() {
initHelpNavigation();
helpNavInitialized = true;
}
renderGuideMermaid();
});
closeBtn.addEventListener("click", closeHelpModal);
@@ -367,6 +368,64 @@ function initHelpModal() {
closeHelpModal();
}
});
// Guide downloads (#105) — markdown / pdf, current language.
const dlMd = document.getElementById("help-download-md");
const dlPdf = document.getElementById("help-download-pdf");
[dlMd, dlPdf].forEach((btn) => {
if (!btn) return;
btn.addEventListener("click", () => {
downloadGuide(btn.id === "help-download-md" ? "md" : "pdf");
});
});
}
// Render any Mermaid blocks inside the guide modal (the architecture diagram,
// #105). The viewer's own pipeline only touches document views, so the help
// modal is enriched here — once per modal open, cheap on repeats.
function renderGuideMermaid() {
const modal = document.getElementById("help-modal");
if (!modal || modal.dataset.mermaidRendered === "1") return;
import("./mermaid-viewer.js")
.then((m) => m.renderMermaidBlocks(modal))
.then(() => {
// Only mark done when the source block actually became a rendered
// diagram — if the Mermaid CDN wasn't ready yet, retry on next open.
if (!modal.querySelector("code.language-mermaid")) {
modal.dataset.mermaidRendered = "1";
}
})
.catch(() => { /* CDN offline: keep the code block readable */ });
}
// Fetch the generated guide (auth headers + cookie) and trigger the browser
// download, mirroring viewer.downloadExport().
async function downloadGuide(format) {
showToast(t("viewer.export_start"), "info");
try {
const headers = AuthManager.getAuthHeaders ? AuthManager.getAuthHeaders() || {} : {};
const res = await fetch(
`/api/guide/download?format=${format}&lang=${encodeURIComponent(getLocale() || "fr")}`,
{ credentials: "include", headers },
);
if (!res.ok) {
let detail = "";
try { detail = (await res.json()).detail || ""; } catch (_) { /* ignore */ }
throw new Error(detail || "HTTP " + res.status);
}
const blob = await res.blob();
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = `ObsiGate-Guide-${getLocale() || "fr"}.${format}`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(() => URL.revokeObjectURL(a.href), 1000);
showToast(t("viewer.export_done"), "success");
} catch (err) {
console.error("Guide export error:", err);
showToast(t("viewer.export_error") + " " + err.message, "error");
}
}
function initEditorPocBtn() {
+3
View File
@@ -231,6 +231,9 @@ export async function initDesktopIntegration() {
defineBackendCrashBanner();
if (!isTauriEnv()) return false;
// Desktop marker for CSS (wider reading layouts, e.g. the user guide #105).
try { document.body.classList.add('desktop-mode'); } catch (e) { /* ignore */ }
// Follow the OS theme on first run, before the theme engine renders.
await syncSystemTheme();
+96 -3
View File
@@ -496,7 +496,7 @@
"config.section_fonctionnalites": "Features",
"config.section_format-du-payload": "Format du payload",
"config.section_gestion-des-onglets": "📑 Gestion des onglets",
"config.section_hidden": "Hidden files",
"config.section_hidden": "🗂️ Hidden files",
"config.section_historique-recent-redemarrage-non-requis": "📋 Recent History\n No restart required",
"config.section_indicateurs-visuels": "Indicateurs visuels",
"config.section_intelligence-artificielle-dans-l-editeur": "🤖 AI in the Editor",
@@ -1961,5 +1961,98 @@
"help.excalidraw_search_title": "Search",
"help.excalidraw_search": "Text inside diagram elements is extracted on indexing, so it is searchable via the full-text search.",
"help.excalidraw_compat": "Files created with the Obsidian Excalidraw plugin (including the compressed <code>.excalidraw.md</code> format) are compatible.",
"help.footer_tagline": "- Web gateway for your Obsidian vaults"
}
"help.footer_tagline": "- Web gateway for your Obsidian vaults",
"guide105.nav_architecture": "🏗️ Architecture",
"guide105.nav_library": "⭐ Library",
"guide105.nav_diagrams": "📊 Diagrams",
"guide105.nav_offline": "📴 Offline",
"guide105.nav_collab": "👥 Collaboration",
"guide105.nav_desktop": "🖥️ Desktop",
"guide105.nav_api": "🔌 API",
"guide105.nav_languages": "🌍 Languages",
"guide105.arch_intro": "ObsiGate is a full web application built as independent layers, with no external database: notes live in your Obsidian folders, app state in JSON files under <code>data/</code>, the search index in memory.",
"guide105.arch_diagram_note": "The diagram is interactive in the app: zoom, fullscreen, copy SVG or code.",
"guide105.arch_h3_layers": "The main components",
"guide105.arch_lbl_fe": "Frontend",
"guide105.arch_fe": " — vanilla-JavaScript SPA (ES modules), no framework, no npm build: <code>frontend/js/</code> (~30 modules). CSS variables drive the themes.",
"guide105.arch_lbl_be": "Backend",
"guide105.arch_be": " — FastAPI server (Python 3.11): markdown rendering (mistune + wikilinks), stemmed TF-IDF search (in-memory inverted index), watchdog file watchers, JWT + Argon2id, HMAC webhooks, PDF export (WeasyPrint).",
"guide105.arch_lbl_realtime": "Realtime & MCP",
"guide105.arch_rt": " — WebSocket gateway (Yjs collaboration, SSE/push notifications) and an MCP server (Streamable HTTP, <code>/mcp</code>) for external clients.",
"guide105.arch_lbl_ai": "AI layer",
"guide105.arch_ai": " — editor assistant and BooksLM with multiple providers (DeepSeek, OpenRouter, Gemini, Mistral…), a tool library (function calling, web search, crawl, document reading) and optional embeddings for semantic search.",
"guide105.arch_lbl_data": "Data",
"guide105.arch_data": " — Obsidian vaults on disk (the source of truth), JSON configuration (<code>data/</code>), timestamped backups (<code>.obsigate-backup/</code>), a JSON-lines audit log, encrypted API keys in <code>data/api_keys.json</code>.",
"guide105.arch_lbl_deploy": "Deployment",
"guide105.arch_deploy": " — Tauri desktop app (Rust) embedding the Python backend, a Docker container, or an installable PWA in the browser (offline mode).",
"guide105.arch_h3_flux": "Typical request flow",
"guide105.arch_flux": " Clicking a file issues <code>GET /api/file/...</code>; the backend resolves the path safely, parses frontmatter, renders the markdown and returns HTML; the frontend enriches the view (Mermaid, syntax highlighting, clickable wikilinks). Every write creates a backup before applying.",
"guide105.dia_intro": "<code>```mermaid</code> blocks in your notes render as interactive diagrams (Mermaid v11, loaded from a CDN).",
"guide105.dia_zoom": "Zoom: + / − buttons in the diagram toolbar.",
"guide105.dia_fs": "Fullscreen: ideal for large charts.",
"guide105.dia_copy": "Copy: export the SVG or the source code (dedicated buttons).",
"guide105.dia_toggle": "Preview / Code toggle to edit the source without leaving the view.",
"guide105.dia_theme": "Theme: the diagram follows the app's light/dark theme.",
"guide105.dia_types": "Supported types: flowchart, sequence, class, state, ER, gantt, pie, journey, quadrant, radar, mindmap, timeline, C4, xychart, sankey — plus a preprocessor that understands Obsidian syntax.",
"guide105.dia_excalidraw_ref": "Hand-drawn sketches (<code>.excalidraw</code>, <code>.excalidraw.md</code>) are covered in the 🎨 Excalidraw section.",
"guide105.lib_h3_bookmarks": "Bookmarks & recents",
"guide105.lib_bookmarks": "Star a file with the Bookmark button in the action bar: it joins the dashboard's bookmark list. Recently opened files are listed automatically in the sidebar's \"Recent\" tab, with a dedicated search filter.",
"guide105.lib_h3_saved": "Saved searches",
"guide105.lib_saved": "Save a search from the results page to rerun it in one click from the sidebar: each saved search keeps its operators and filters.",
"guide105.lib_h3_backlinks": "Backlinks & graph",
"guide105.lib_backlinks": "The Backlinks panel lists every note pointing to the open file. The Graph view (🕸️ button) shows links between files: drag nodes, scroll to zoom, double-click a node to open the note.",
"guide105.lib_h3_conflicts": "Sync conflicts",
"guide105.lib_conflicts": "If you sync the vault with Syncthing, ObsiGate detects conflict files (\"sync-conflict\" copies) and offers to compare then resolve them from a dedicated page in the Options menu.",
"guide105.lib_h3_attach": "Attachments & media",
"guide105.lib_attach": "Inline <code>![[image.png]]</code> images, attachments and media (audio, video, embedded PDFs) are rendered in the viewer and indexed for search; the \"Rescan attachments\" button in Configuration rebuilds the attachment index.",
"guide105.off_pwa": "ObsiGate is a PWA: install it (install icon in the address bar) to open it like an app. The service worker caches the UI and your recently viewed documents.",
"guide105.off_edit": "Offline you can read cached documents and even edit them: changes are queued in IndexedDB.",
"guide105.off_sync": "When back online the queue replays automatically (sync badge in the header). If the server version diverged meanwhile, the file is flagged as conflict and the server copy is kept as a backup.",
"guide105.off_watch": "External changes (Obsidian on disk) are detected by the watcher: the view reloads without losing your position, or flags \"modified externally\" during an edit.",
"guide105.col_intro": "Open a document in Edit mode: several people can work on the same file simultaneously over a Yjs (CRDT) WebSocket. Changes merge without locks.",
"guide105.col_cursors": "Collaborators' cursors and selections appear with a per-person colour and name (awareness).",
"guide105.col_save": "The merge is persisted server-side after 2 s of idle; every write creates a timestamped backup before applying.",
"guide105.col_perm": "Limited to authenticated users with permission on the vault.",
"guide105.des_get": "The ObsiGate desktop app (Tauri) embeds the Python server: no Docker install needed. Download it from the repository's Releases page; it auto-updates (signed updater).",
"guide105.des_wizard": "On first launch a wizard asks for your vaults folder (or creates a demo vault). Any document can be detached into its own native window.",
"guide105.des_data": "Desktop data stays in the app directory; vaults point at your existing folders. Every web feature (search, AI, sharing) is available.",
"guide105.des_native": "Native system menu, optional global show/hide shortcut and a recents vault jumplist.",
"guide105.api_intro": "ObsiGate exposes a REST API covering the whole application (vaults, files, search, backups, export, AI, sharing, admin), documented in OpenAPI 3.1:",
"guide105.api_docs_url": "<code>/docs</code> — Swagger UI to try requests live.",
"guide105.api_redoc": "<code>/redoc</code> — compact alternative reference.",
"guide105.api_landing": "<code>/api</code> — landing page grouping endpoints by category.",
"guide105.api_schema": "<code>/openapi.json</code> — the machine schema, import into Postman or Insomnia.",
"guide105.api_h3_auth": "Authentication",
"guide105.api_auth": "Log in via <code>POST /api/auth/login</code> to get a Bearer token (the same token is accepted as an HttpOnly cookie, so browser clients can use <code>credentials: \"include\"</code>). All <code>/api/*</code> routes require it unless documented otherwise.",
"guide105.api_h3_mcp": "MCP server",
"guide105.api_mcp": "The assistant's tools (read, list, search, open, write…) are exposed to any MCP client (Claude Desktop, Cursor, Cline…) at <code>https://your-instance/mcp</code> with an API token. Setup and examples: <code>docs/MCP_GUIDE.md</code>.",
"guide105.api_h3_autom": "Automation",
"guide105.api_autom": "To automate from outside: <code>GET /api/search?q=…</code> and <code>GET /api/file/{vault}?path=…</code> let another tool index or re-read your notes; outgoing webhooks (🪝 section) avoid polling.",
"guide105.lng_how": "The interface is fully bilingual FR/EN. Settings → Profile → Language: the choice is stored on your account and follows you across devices.",
"guide105.lng_scope": "Everything is translated: menus, messages, notifications, and this guide. AI assistant answers follow the language of your documents.",
"guide105.lng_export": "This guide's Markdown / PDF buttons download the version in your language.",
"guide105.h3_semantic": "Semantic search (hybrid)",
"guide105.sem_p1": "Toggle the \"S\" button in the search bar (or Alt-S) to combine TF-IDF with vector similarity (RRF fusion): near concepts (\"velvet\" finds \"soft fabric\") surface higher.",
"guide105.sem_p2": "The embedding engine (multilingual model) is optional: without it a hash fallback keeps hybrid search working. Vectors are recomputed on each vault reindex.",
"guide105.h3_push": "Web notifications (push)",
"guide105.push_p1": "Grant notification permission (🔔 button in the header) to be alerted of offline-sync completions and important events. Subscription management lives in Configuration.",
"guide105.push_p2": "Built on the Web Push API (VAPID keys); works on desktop and mobile PWA with no third-party service: the server sends directly to browser push endpoints.",
"guide105.h3_panes": "Multi-pane split view",
"guide105.panes_p": "The \"Split\" button in the action bar opens the document in a twin pane; stack several panes to compare two notes or read and edit side by side. Pane widths drag on the border and are remembered.",
"guide105.h3_dupe": "Duplicate-proof uploads",
"guide105.dupe_p": "Bulk upload (drag a folder onto the sidebar) compares each file with existing content: an already-present file is skipped rather than duplicated with a \"(1)\" suffix. Handy when restoring a vault.",
"guide105.h3_pdf": "PDF export",
"guide105.pdf_p": "A document's \"PDF\" button renders it with the same engine as the viewer (WeasyPrint): headings, tables, lists and code are preserved. From a public link, the <code>/s/{token}/pdf</code> route produces the same PDF.",
"guide105.h3_exports": "HTML / ePub / ZIP export",
"guide105.exp_p": "The \"Export\" menu offers three formats: standalone HTML (single file, images inlined), ePub for e-readers and, for a folder, a Markdown ZIP bundle — links and resources resolved during export.",
"guide105.h3_mfa": "MFA: TOTP, WebAuthn, recovery codes",
"guide105.mfa_p": "Enable two-factor auth in Settings → Profile: TOTP apps (Authy, Aegis…), security keys and passkeys (WebAuthn, including Windows Hello) and 10 recovery codes to keep offline. Each method can be enabled and disabled independently.",
"guide105.h3_admin": "Admin dashboard",
"guide105.admin_p": "The admin role unlocks a dedicated <code>/admin.html</code> page (Options menu button): live server status, users, vaults, active sessions and the audit log. User CRUD also lives in Configuration.",
"guide105.dl_md_title": "Download this guide as Markdown",
"guide105.dl_pdf_title": "Download this guide as PDF",
"guide105.dl_md": "Markdown",
"guide105.dl_pdf": "PDF",
"guide105.export_title": "ObsiGate User Guide",
"guide105.export_footer": "Generated from ObsiGate {version} — {date}. This document mirrors the in-app guide; the latest version always lives in the application."
}
+97 -4
View File
@@ -496,7 +496,7 @@
"config.section_fonctionnalites": "Fonctionnalités",
"config.section_format-du-payload": "Format du payload",
"config.section_gestion-des-onglets": "📑 Gestion des onglets",
"config.section_hidden": "Fichiers cachés",
"config.section_hidden": "🗂️ Fichiers cachés",
"config.section_historique-recent-redemarrage-non-requis": "📋 Historique récent\n Redémarrage non requis",
"config.section_indicateurs-visuels": "Indicateurs visuels",
"config.section_intelligence-artificielle-dans-l-editeur": "🤖 Intelligence Artificielle dans l'Éditeur",
@@ -539,7 +539,7 @@
"config.section_securite-signature-hmac-sha256": "Sécurité : signature HMAC-SHA256",
"config.section_selection-de-vault": "Sélection de vault",
"config.section_server": "Serveur",
"config.section_shares": "Partages publics",
"config.section_shares": "📤 Partages publics",
"config.section_sidebar-barre-laterale": "Sidebar (barre latérale)",
"config.section_synchronisation-automatique": "Synchronisation automatique",
"config.section_tag-cloud": "Tag cloud",
@@ -1961,5 +1961,98 @@
"help.excalidraw_search_title": "Recherche",
"help.excalidraw_search": "Le texte des éléments du diagramme est extrait à l'indexation : il est donc recherchable via la recherche full-text.",
"help.excalidraw_compat": "Les fichiers créés avec le plugin Obsidian Excalidraw (y compris le format <code>.excalidraw.md</code> compressé) sont compatibles.",
"help.footer_tagline": "- Porte d'entrée web pour vos vaults Obsidian"
}
"help.footer_tagline": "- Porte d'entrée web pour vos vaults Obsidian",
"guide105.nav_architecture": "🏗️ Architecture",
"guide105.nav_library": "⭐ Bibliothèque",
"guide105.nav_diagrams": "📊 Diagrammes",
"guide105.nav_offline": "📴 Hors-ligne",
"guide105.nav_collab": "👥 Collaboration",
"guide105.nav_desktop": "🖥️ Desktop",
"guide105.nav_api": "🔌 API",
"guide105.nav_languages": "🌍 Multilingue",
"guide105.arch_intro": "ObsiGate est une application web complète construite en couches indépendantes, sans base de données externe : les notes vivent dans vos dossiers Obsidian, l'état applicatif dans des fichiers JSON de <code>data/</code>, l'index de recherche en mémoire.",
"guide105.arch_diagram_note": "Le diagramme est interactif dans l'application : zoom, plein écran, copie SVG ou code.",
"guide105.arch_h3_layers": "Les grandes composantes",
"guide105.arch_lbl_fe": "Frontend",
"guide105.arch_fe": " — SPA en JavaScript vanilla (modules ES), sans framework ni build npm : <code>frontend/js/</code> (~30 modules). Le CSS utilise des variables pour les thèmes.",
"guide105.arch_lbl_be": "Backend",
"guide105.arch_be": " — serveur FastAPI (Python 3.11) : rendu markdown (mistune + wikilinks), recherche TF-IDF stemmisée (index inversé en mémoire), watchers watchdog, JWT + Argon2id, webhooks HMAC, export PDF (WeasyPrint).",
"guide105.arch_lbl_realtime": "Temps réel & MCP",
"guide105.arch_rt": " — passerelle WebSocket (collaboration Yjs, notifications SSE/push) et serveur MCP (Streamable HTTP, <code>/mcp</code>) pour les clients externes.",
"guide105.arch_lbl_ai": "Couche IA",
"guide105.arch_ai": " — assistants d'édition et BooksLM multi-providers (DeepSeek, OpenRouter, Gemini, Mistral…), bibliothèque d'outils (function calling, recherche web, crawl, lecture de documents) et embeddings optionnels pour la recherche sémantique.",
"guide105.arch_lbl_data": "Données",
"guide105.arch_data": " — les vaults Obsidian sur disque (source de vérité), la configuration en JSON (<code>data/</code>), les backups horodatés (<code>.obsigate-backup/</code>), l'audit en JSON lines, les clés API chiffrées dans <code>data/api_keys.json</code>.",
"guide105.arch_lbl_deploy": "Déploiement",
"guide105.arch_deploy": " — application desktop Tauri (Rust) embarquant le backend Python, conteneur Docker, ou PWA installable dans le navigateur (mode hors-ligne).",
"guide105.arch_h3_flux": "Flux typique",
"guide105.arch_flux": " Un clic sur un fichier émet <code>GET /api/file/...</code> ; le backend résout le chemin en sécurité, parse le frontmatter, rend le markdown et renvoie le HTML ; le frontend enrichit l'affichage (Mermaid, coloration, wikilinks cliquables). Chaque écriture crée un backup avant application.",
"guide105.dia_intro": "Les blocs <code>```mermaid</code> de vos notes sont rendus en diagrammes interactifs (Mermaid v11, chargé depuis un CDN).",
"guide105.dia_zoom": "Zoom : boutons + / − dans la barre d'outils du diagramme.",
"guide105.dia_fs": "Plein écran : idéal pour les grandes matrices.",
"guide105.dia_copy": "Copie : exportez le SVG ou le code source (boutons dédiés).",
"guide105.dia_toggle": "Bascule Aperçu / Code pour éditer la source sans quitter la vue.",
"guide105.dia_theme": "Thème : le diagramme suit le thème clair/sombre de l'application.",
"guide105.dia_types": "Types supportés : flowchart, sequence, class, state, ER, gantt, pie, journey, quadrant, radar, mindmap, timeline, C4, xychart, sankey — plus un préprocesseur qui comprend la syntaxe Obsidian.",
"guide105.dia_excalidraw_ref": "Les dessins à main levée (<code>.excalidraw</code>, <code>.excalidraw.md</code>) sont couverts dans la section 🎨 Excalidraw.",
"guide105.lib_h3_bookmarks": "Signets & récents",
"guide105.lib_bookmarks": "Marquez un fichier d'un ★ (bouton Signet de la barre d'actions) : il rejoint la liste des signets du dashboard. Les fichiers récemment ouverts sont listés automatiquement dans l'onglet « Récents » de la sidebar, avec un filtre de recherche dédié.",
"guide105.lib_h3_saved": "Recherches sauvegardées",
"guide105.lib_saved": "Enregistrez une recherche depuis la page de résultats pour la relancer en un clic depuis la sidebar : chaque recherche sauvegardée conserve ses opérateurs et filtres.",
"guide105.lib_h3_backlinks": "Backlinks & graphe",
"guide105.lib_backlinks": "Le panneau Backlinks liste toutes les notes qui pointent vers le fichier ouvert. La vue Graphe (bouton 🕸️) affiche les liens entre fichiers : glissez les nœuds, zoomez à la molette, double-cliquez pour ouvrir une note.",
"guide105.lib_h3_conflicts": "Conflits de synchronisation",
"guide105.lib_conflicts": "Si vous synchronisez le vault avec Syncthing, ObsiGate détecte les fichiers de conflit (copies « sync-conflict ») et propose de les comparer puis résoudre depuis la page dédiée du menu Options.",
"guide105.lib_h3_attach": "Fichiers joints & médias",
"guide105.lib_attach": "Les images <code>![[image.png]]</code>, pièces jointes et médias (audio, vidéo, PDF intégrés) dans les notes sont rendus dans le viewer et indexés pour la recherche ; le bouton « Rescan attachments » de la configuration recrée l'index des pièces jointes.",
"guide105.off_pwa": "ObsiGate est une PWA : installez-la (icône d'installation de la barre d'adresse) pour l'ouvrir comme une application. Le service worker met en cache l'interface et vos derniers documents consultés.",
"guide105.off_edit": "Hors-ligne, vous pouvez lire les documents en cache et même les éditer : les modifications sont mises en file d'attente dans IndexedDB.",
"guide105.off_sync": "Au retour en ligne, la file se rejoue automatiquement (badge de synchronisation dans l'en-tête). Si la version serveur a divergé entre-temps, le fichier est marqué en conflit et la version serveur est préservée en backup.",
"guide105.off_watch": "Les modifications externes (Obsidian sur disque) sont détectées par le watcher : la vue se recharge sans perte de position, ou signale « modifié en externe » pendant une édition.",
"guide105.col_intro": "Ouvrez un document en mode Édition : plusieurs personnes peuvent travailler simultanément sur le même fichier via un WebSocket Yjs (CRDT). Les modifications fusionnent sans verrou.",
"guide105.col_cursors": "Les curseurs et sélections des collaborateurs apparaissent avec une couleur et un nom par personne (awareness).",
"guide105.col_save": "La fusion est persistée côté serveur après 2 s d'inactivité ; chaque écriture crée un backup horodaté avant application.",
"guide105.col_perm": "Accès limité aux utilisateurs authentifiés disposant de la permission sur la vault.",
"guide105.des_get": "L'application desktop ObsiGate (Tauri) embarque le serveur Python : aucune installation de Docker nécessaire. Elle se télécharge sur la page des Releases du dépôt et se met à jour automatiquement (updater signé).",
"guide105.des_wizard": "Au premier lancement, un assistant demande le dossier de vos vaults (ou crée un vault de démonstration). Chaque document peut être détaché en fenêtre native séparée.",
"guide105.des_data": "Les données desktop restent dans le répertoire applicatif ; les vaults pointent sur vos dossiers existants. Toutes les fonctionnalités web (recherche, IA, partage) sont disponibles.",
"guide105.des_native": "Menu système natif, raccourci global optionnel pour afficher/masquer la fenêtre et jumplist des vaults récents.",
"guide105.api_intro": "ObsiGate expose une API REST couvrant toute l'application (vaults, fichiers, recherche, backups, export, IA, partage, admin), documentée en OpenAPI 3.1 :",
"guide105.api_docs_url": "<code>/docs</code> — interface Swagger UI pour essayer les requêtes en direct.",
"guide105.api_redoc": "<code>/redoc</code> — référence alternative plus compacte.",
"guide105.api_landing": "<code>/api</code> — page de garde regroupant les endpoints par catégorie.",
"guide105.api_schema": "<code>/openapi.json</code> — le schéma machine, à importer dans Postman ou Insomnia.",
"guide105.api_h3_auth": "Authentification",
"guide105.api_auth": "Connectez-vous via <code>POST /api/auth/login</code> pour obtenir un token Bearer (le même jeton est accepté en cookie HttpOnly, ce qui permet aux clients navigateur d'utiliser <code>credentials: \"include\"</code>). Toutes les routes <code>/api/*</code> exigent ce jeton sauf mention contraire.",
"guide105.api_h3_mcp": "Serveur MCP",
"guide105.api_mcp": "Les outils de l'assistant IA (lire, lister, chercher, ouvrir, écrire…) sont exposés à tout client MCP (Claude Desktop, Cursor, Cline…) sur <code>https://votre-instance/mcp</code> avec un token d'API. Configuration et exemples : <code>docs/MCP_GUIDE.md</code>.",
"guide105.api_h3_autom": "Automatisation",
"guide105.api_autom": "Pour automatiser depuis l'extérieur : <code>GET /api/search?q=…</code> et <code>GET /api/file/{vault}?path=…</code> permettent d'indexer ou relire vos notes dans un autre outil ; les webhooks sortants (section 🪝) évitent le polling.",
"guide105.lng_how": "L'interface est intégralement bilingue français / anglais. Réglages → Profil → Langue : le choix est enregistré sur votre compte et vous suit sur tous les appareils.",
"guide105.lng_scope": "Tout est traduit : menus, messages, notifications, et le présent guide. Les réponses de l'assistant IA suivent la langue de vos documents.",
"guide105.lng_export": "Les boutons Markdown / PDF de ce guide téléchargent la version dans votre langue.",
"guide105.h3_semantic": "Recherche sémantique (hybride)",
"guide105.sem_p1": "Activez le bouton « S » de la barre de recherche (ou Alt-S) pour combiner TF-IDF et similarité vectorielle (fusion RRF) : les concepts approchants (« velours » trouve « tissu doux ») remontent mieux.",
"guide105.sem_p2": "Le moteur d'embeddings (modèle multilingue) est optionnel : sans lui, un repli par hash conserve une recherche hybride fonctionnelle. Les vecteurs sont recalculés à chaque indexation du vault.",
"guide105.h3_push": "Notifications web (push)",
"guide105.push_p1": "Autorisez les notifications (bouton 🔔 de l'en-tête) pour être averti des fins de synchronisation hors-ligne et des événements importants. La gestion des abonnements est dans les Configurations.",
"guide105.push_p2": "Basée sur la Web Push API (clés VAPID) ; fonctionne sur desktop et PWA mobile, sans service tiers : le serveur émet directement vers les endpoints push des navigateurs.",
"guide105.h3_panes": "Vue multi-panneaux (split view)",
"guide105.panes_p": "Le bouton « Diviser » de la barre d'actions ouvre le document dans un panneau jumeau ; empilez plusieurs panneaux pour comparer deux notes ou lire et éditer en parallèle. Les largeurs se règlent au bord des panneaux et sont mémorisées.",
"guide105.h3_dupe": "Anti-doublons à l'upload",
"guide105.dupe_p": "L'upload en masse (glisser-déposer un dossier sur la sidebar) compare chaque fichier au contenu existant : un fichier déjà présent est ignoré plutôt que dupliqué avec un suffixe « (1) ». Utile pour restaurer un vault sans créer de doublons.",
"guide105.h3_pdf": "Export PDF",
"guide105.pdf_p": "Le bouton « PDF » d'un document le rend avec le même moteur que la vue (WeasyPrint) : titres, tableaux, listes et code sont conservés. Depuis un lien public, la route <code>/s/{token}/pdf</code> produit le même PDF.",
"guide105.h3_exports": "Export HTML / ePub / ZIP",
"guide105.exp_p": "Le menu « Exporter » propose trois formats : HTML autonome (fichier unique, images incluses), ePub pour les liseuses et, pour un dossier, un bundle Markdown en ZIP — liens et ressources résolus pendant l'export.",
"guide105.h3_mfa": "MFA : TOTP, WebAuthn, codes de secours",
"guide105.mfa_p": "Activez la double authentification dans Réglages → Profil : applications TOTP (Authy, Aegis…), clés de sécurité et passkeys (WebAuthn, y compris Windows Hello) et 10 codes de secours à conserver hors ligne. Chaque méthode s'active et se désactive indépendamment.",
"guide105.h3_admin": "Tableau de bord administrateur",
"guide105.admin_p": "Le rôle admin ouvre une page dédiée <code>/admin.html</code> (bouton du menu Options) : statut du serveur en direct, utilisateurs, vaults, sessions actives et journal d'audit. Le CRUD utilisateurs est aussi disponible dans les Configurations.",
"guide105.dl_md_title": "Télécharger ce guide en Markdown",
"guide105.dl_pdf_title": "Télécharger ce guide en PDF",
"guide105.dl_md": "Markdown",
"guide105.dl_pdf": "PDF",
"guide105.export_title": "Guide d'utilisation ObsiGate",
"guide105.export_footer": "Généré depuis ObsiGate {version} — {date}. Ce document est la copie du guide intégré ; la version la plus récente est toujours dans l'application."
}
+23
View File
@@ -8146,6 +8146,29 @@ body.popup-mode .content-area {
.cp-browse-filter:focus { border-color: var(--accent); }
.cp-browse-filter::placeholder { color: var(--text-muted); }
/* ── Help Modal: desktop = wider reading (#105) ──
The Tauri shell (body.desktop-mode) and wide web viewports get the full
reading layout instead of the 1320px modal cap. */
body.desktop-mode .help-container {
max-width: 1760px;
width: 96vw;
}
body.desktop-mode .help-content {
max-width: 1440px;
padding: 36px 56px 64px;
}
body.desktop-mode .editor-container {
max-height: 94vh;
}
@media (min-width: 1400px) {
.help-container {
max-width: min(1640px, calc(100vw - 48px));
}
.help-content {
max-width: 1280px;
}
}
/* ── Help Modal: Mobile responsive ── */
@media (max-width: 768px) {
.help-container,
+1 -1
View File
@@ -11,7 +11,7 @@
* cache or Cloudflare does NOT clear the Service Worker Cache Storage, which is
* a separate store. Bumping SW_VERSION invalidates it on every release.
*/
const SW_VERSION = 'v21';
const SW_VERSION = 'v22';
const CODE_CACHE = `obsigate-code-${SW_VERSION}`;
const RUNTIME_CACHE = `obsigate-runtime-${SW_VERSION}`;
const API_CACHE = `obsigate-api-${SW_VERSION}`;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "obsigate",
"version": "2.12.0",
"version": "2.13.0",
"description": "**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.",
"main": "patch.js",
"directories": {
+629
View File
@@ -0,0 +1,629 @@
# -*- coding: utf-8 -*-
"""Nouveau contenu du Guide d'utilisation (#105) — source unique de vérité.
Rôles :
1. ``CONTENT`` : dictionnaire i18n clé -> (fr, en). Les locales FR/EN sont
générées depuis ce dictionnaire par ``scripts/merge_guide_locales.py``
(ne jamais éditer les blocs ``guide105.*`` des JSON à la main).
2. Les constantes ``SECTION_*`` / ``EXTRA_BLOCKS`` / ``TOC_INSERT_BEFORE``
décrivent le HTML à insérer dans ``frontend/index.html`` par
``scripts/insert_guide_sections.py``. Le texte FR inline de chaque
élément portant ``data-i18n="guide105.X"`` DOIT être identique à
``CONTENT[X][0]`` (sinon ``_applyDOM`` afficherait un texte incohérent
quand la locale FR est appliquée).
Règle i18n : tout élément textuel des nouvelles sections porte une clé
``guide105.*``. Les clés préexistantes ne sont jamais réutilisées avec un
texte différent.
"""
# ---------------------------------------------------------------------------
# Dictionnaire i18n (clé -> FR, EN)
# ---------------------------------------------------------------------------
CONTENT: dict[str, tuple[str, str]] = {
# -- TOC / titres de sections ---------------------------------------------
"nav_architecture": ("🏗️ Architecture", "🏗️ Architecture"),
"nav_library": ("⭐ Bibliothèque", "⭐ Library"),
"nav_diagrams": ("📊 Diagrammes", "📊 Diagrams"),
"nav_offline": ("📴 Hors-ligne", "📴 Offline"),
"nav_collab": ("👥 Collaboration", "👥 Collaboration"),
"nav_desktop": ("🖥️ Desktop", "🖥️ Desktop"),
"nav_api": ("🔌 API", "🔌 API"),
"nav_languages": ("🌍 Multilingue", "🌍 Languages"),
# -- Section Architecture ---------------------------------------------------
"arch_intro": (
"ObsiGate est une application web complète construite en couches indépendantes, sans base"
" de données externe : les notes vivent dans vos dossiers Obsidian, l'état applicatif dans"
" des fichiers JSON de <code>data/</code>, l'index de recherche en mémoire.",
"ObsiGate is a full web application built as independent layers, with no external"
" database: notes live in your Obsidian folders, app state in JSON files under"
" <code>data/</code>, the search index in memory.",
),
"arch_diagram_note": (
"Le diagramme est interactif dans l'application : zoom, plein écran, copie SVG ou code.",
"The diagram is interactive in the app: zoom, fullscreen, copy SVG or code.",
),
"arch_h3_layers": ("Les grandes composantes", "The main components"),
"arch_lbl_fe": ("Frontend", "Frontend"),
"arch_fe": (
" — SPA en JavaScript vanilla (modules ES), sans framework ni build npm :"
" <code>frontend/js/</code> (~30 modules). Le CSS utilise des variables pour les thèmes.",
" — vanilla-JavaScript SPA (ES modules), no framework, no npm build:"
" <code>frontend/js/</code> (~30 modules). CSS variables drive the themes.",
),
"arch_lbl_be": ("Backend", "Backend"),
"arch_be": (
" — serveur FastAPI (Python 3.11) : rendu markdown (mistune + wikilinks),"
" recherche TF-IDF stemmisée (index inversé en mémoire), watchers watchdog, JWT +"
" Argon2id, webhooks HMAC, export PDF (WeasyPrint).",
" — FastAPI server (Python 3.11): markdown rendering (mistune + wikilinks), stemmed"
" TF-IDF search (in-memory inverted index), watchdog file watchers, JWT + Argon2id,"
" HMAC webhooks, PDF export (WeasyPrint).",
),
"arch_lbl_realtime": ("Temps réel & MCP", "Realtime & MCP"),
"arch_rt": (
" — passerelle WebSocket (collaboration Yjs, notifications SSE/push) et serveur MCP"
" (Streamable HTTP, <code>/mcp</code>) pour les clients externes.",
" — WebSocket gateway (Yjs collaboration, SSE/push notifications) and an MCP server"
" (Streamable HTTP, <code>/mcp</code>) for external clients.",
),
"arch_lbl_ai": ("Couche IA", "AI layer"),
"arch_ai": (
" — assistants d'édition et BooksLM multi-providers (DeepSeek, OpenRouter, Gemini,"
" Mistral…), bibliothèque d'outils (function calling, recherche web, crawl, lecture de"
" documents) et embeddings optionnels pour la recherche sémantique.",
" — editor assistant and BooksLM with multiple providers (DeepSeek, OpenRouter, Gemini,"
" Mistral…), a tool library (function calling, web search, crawl, document reading) and"
" optional embeddings for semantic search.",
),
"arch_lbl_data": ("Données", "Data"),
"arch_data": (
" — les vaults Obsidian sur disque (source de vérité), la configuration en JSON"
" (<code>data/</code>), les backups horodatés (<code>.obsigate-backup/</code>),"
" l'audit en JSON lines, les clés API chiffrées dans <code>data/api_keys.json</code>.",
" — Obsidian vaults on disk (the source of truth), JSON configuration"
" (<code>data/</code>), timestamped backups (<code>.obsigate-backup/</code>), a JSON-lines"
" audit log, encrypted API keys in <code>data/api_keys.json</code>.",
),
"arch_lbl_deploy": ("Déploiement", "Deployment"),
"arch_deploy": (
" — application desktop Tauri (Rust) embarquant le backend Python, conteneur Docker, ou"
" PWA installable dans le navigateur (mode hors-ligne).",
" — Tauri desktop app (Rust) embedding the Python backend, a Docker container, or an"
" installable PWA in the browser (offline mode).",
),
"arch_h3_flux": ("Flux typique", "Typical request flow"),
"arch_flux": (
" Un clic sur un fichier émet <code>GET /api/file/...</code> ; le backend résout le chemin"
" en sécurité, parse le frontmatter, rend le markdown et renvoie le HTML ; le frontend"
" enrichit l'affichage (Mermaid, coloration, wikilinks cliquables). Chaque écriture crée"
" un backup avant application.",
" Clicking a file issues <code>GET /api/file/...</code>; the backend resolves the path"
" safely, parses frontmatter, renders the markdown and returns HTML; the frontend"
" enriches the view (Mermaid, syntax highlighting, clickable wikilinks). Every write"
" creates a backup before applying.",
),
# -- Section Diagrammes ---------------------------------------------------------
"dia_intro": (
"Les blocs <code>```mermaid</code> de vos notes sont rendus en diagrammes interactifs"
" (Mermaid v11, chargé depuis un CDN).",
"<code>```mermaid</code> blocks in your notes render as interactive diagrams (Mermaid v11,"
" loaded from a CDN).",
),
"dia_zoom": ("Zoom : boutons + / − dans la barre d'outils du diagramme.",
"Zoom: + / − buttons in the diagram toolbar."),
"dia_fs": ("Plein écran : idéal pour les grandes matrices.",
"Fullscreen: ideal for large charts."),
"dia_copy": ("Copie : exportez le SVG ou le code source (boutons dédiés).",
"Copy: export the SVG or the source code (dedicated buttons)."),
"dia_toggle": ("Bascule Aperçu / Code pour éditer la source sans quitter la vue.",
"Preview / Code toggle to edit the source without leaving the view."),
"dia_theme": ("Thème : le diagramme suit le thème clair/sombre de l'application.",
"Theme: the diagram follows the app's light/dark theme."),
"dia_types": (
"Types supportés : flowchart, sequence, class, state, ER, gantt, pie, journey, quadrant,"
" radar, mindmap, timeline, C4, xychart, sankey — plus un préprocesseur qui comprend la"
" syntaxe Obsidian.",
"Supported types: flowchart, sequence, class, state, ER, gantt, pie, journey, quadrant,"
" radar, mindmap, timeline, C4, xychart, sankey — plus a preprocessor that understands"
" Obsidian syntax.",
),
"dia_excalidraw_ref": (
"Les dessins à main levée (<code>.excalidraw</code>, <code>.excalidraw.md</code>) sont"
" couverts dans la section 🎨 Excalidraw.",
"Hand-drawn sketches (<code>.excalidraw</code>, <code>.excalidraw.md</code>) are covered"
" in the 🎨 Excalidraw section.",
),
# -- Section Bibliothèque & signets ---------------------------------------------
"lib_h3_bookmarks": ("Signets & récents", "Bookmarks & recents"),
"lib_bookmarks": (
"Marquez un fichier d'un ★ (bouton Signet de la barre d'actions) : il rejoint la liste"
" des signets du dashboard. Les fichiers récemment ouverts sont listés automatiquement"
" dans l'onglet « Récents » de la sidebar, avec un filtre de recherche dédié.",
"Star a file with the Bookmark button in the action bar: it joins the dashboard's"
" bookmark list. Recently opened files are listed automatically in the sidebar's"
" \"Recent\" tab, with a dedicated search filter.",
),
"lib_h3_saved": ("Recherches sauvegardées", "Saved searches"),
"lib_saved": (
"Enregistrez une recherche depuis la page de résultats pour la relancer en un clic depuis"
" la sidebar : chaque recherche sauvegardée conserve ses opérateurs et filtres.",
"Save a search from the results page to rerun it in one click from the sidebar: each saved"
" search keeps its operators and filters.",
),
"lib_h3_backlinks": ("Backlinks & graphe", "Backlinks & graph"),
"lib_backlinks": (
"Le panneau Backlinks liste toutes les notes qui pointent vers le fichier ouvert. La vue"
" Graphe (bouton 🕸️) affiche les liens entre fichiers : glissez les nœuds, zoomez à la"
" molette, double-cliquez pour ouvrir une note.",
"The Backlinks panel lists every note pointing to the open file. The Graph view (🕸️"
" button) shows links between files: drag nodes, scroll to zoom, double-click a node to"
" open the note.",
),
"lib_h3_conflicts": ("Conflits de synchronisation", "Sync conflicts"),
"lib_conflicts": (
"Si vous synchronisez le vault avec Syncthing, ObsiGate détecte les fichiers de conflit"
" (copies « sync-conflict ») et propose de les comparer puis résoudre depuis la page"
" dédiée du menu Options.",
"If you sync the vault with Syncthing, ObsiGate detects conflict files"
" (\"sync-conflict\" copies) and offers to compare then resolve them from a dedicated"
" page in the Options menu.",
),
"lib_h3_attach": ("Fichiers joints & médias", "Attachments & media"),
"lib_attach": (
"Les images <code>![[image.png]]</code>, pièces jointes et médias (audio, vidéo, PDF"
" intégrés) dans les notes sont rendus dans le viewer et indexés pour la recherche ; le"
" bouton « Rescan attachments » de la configuration recrée l'index des pièces jointes.",
"Inline <code>![[image.png]]</code> images, attachments and media (audio, video, embedded"
" PDFs) are rendered in the viewer and indexed for search; the \"Rescan attachments\""
" button in Configuration rebuilds the attachment index.",
),
# -- Section Hors-ligne -----------------------------------------------------------
"off_pwa": (
"ObsiGate est une PWA : installez-la (icône d'installation de la barre d'adresse) pour"
" l'ouvrir comme une application. Le service worker met en cache l'interface et vos"
" derniers documents consultés.",
"ObsiGate is a PWA: install it (install icon in the address bar) to open it like an app."
" The service worker caches the UI and your recently viewed documents.",
),
"off_edit": (
"Hors-ligne, vous pouvez lire les documents en cache et même les éditer : les"
" modifications sont mises en file d'attente dans IndexedDB.",
"Offline you can read cached documents and even edit them: changes are queued in"
" IndexedDB.",
),
"off_sync": (
"Au retour en ligne, la file se rejoue automatiquement (badge de synchronisation dans"
" l'en-tête). Si la version serveur a divergé entre-temps, le fichier est marqué en"
" conflit et la version serveur est préservée en backup.",
"When back online the queue replays automatically (sync badge in the header). If the"
" server version diverged meanwhile, the file is flagged as conflict and the server copy"
" is kept as a backup.",
),
"off_watch": (
"Les modifications externes (Obsidian sur disque) sont détectées par le watcher : la vue"
" se recharge sans perte de position, ou signale « modifié en externe » pendant une"
" édition.",
"External changes (Obsidian on disk) are detected by the watcher: the view reloads"
" without losing your position, or flags \"modified externally\" during an edit.",
),
# -- Section Collaboration ----------------------------------------------------------
"col_intro": (
"Ouvrez un document en mode Édition : plusieurs personnes peuvent travailler"
" simultanément sur le même fichier via un WebSocket Yjs (CRDT). Les modifications"
" fusionnent sans verrou.",
"Open a document in Edit mode: several people can work on the same file simultaneously"
" over a Yjs (CRDT) WebSocket. Changes merge without locks.",
),
"col_cursors": (
"Les curseurs et sélections des collaborateurs apparaissent avec une couleur et un nom"
" par personne (awareness).",
"Collaborators' cursors and selections appear with a per-person colour and name"
" (awareness).",
),
"col_save": (
"La fusion est persistée côté serveur après 2 s d'inactivité ; chaque écriture crée un"
" backup horodaté avant application.",
"The merge is persisted server-side after 2 s of idle; every write creates a timestamped"
" backup before applying.",
),
"col_perm": (
"Accès limité aux utilisateurs authentifiés disposant de la permission sur la vault.",
"Limited to authenticated users with permission on the vault.",
),
# -- Section Desktop ------------------------------------------------------------------
"des_get": (
"L'application desktop ObsiGate (Tauri) embarque le serveur Python : aucune installation"
" de Docker nécessaire. Elle se télécharge sur la page des Releases du dépôt et se met à"
" jour automatiquement (updater signé).",
"The ObsiGate desktop app (Tauri) embeds the Python server: no Docker install needed."
" Download it from the repository's Releases page; it auto-updates (signed updater).",
),
"des_wizard": (
"Au premier lancement, un assistant demande le dossier de vos vaults (ou crée un vault de"
" démonstration). Chaque document peut être détaché en fenêtre native séparée.",
"On first launch a wizard asks for your vaults folder (or creates a demo vault). Any"
" document can be detached into its own native window.",
),
"des_data": (
"Les données desktop restent dans le répertoire applicatif ; les vaults pointent sur vos"
" dossiers existants. Toutes les fonctionnalités web (recherche, IA, partage) sont"
" disponibles.",
"Desktop data stays in the app directory; vaults point at your existing folders. Every web"
" feature (search, AI, sharing) is available.",
),
"des_native": (
"Menu système natif, raccourci global optionnel pour afficher/masquer la fenêtre et"
" jumplist des vaults récents.",
"Native system menu, optional global show/hide shortcut and a recents vault jumplist.",
),
# -- Section API ------------------------------------------------------------------------
"api_intro": (
"ObsiGate expose une API REST couvrant toute l'application (vaults, fichiers, recherche,"
" backups, export, IA, partage, admin), documentée en OpenAPI 3.1 :",
"ObsiGate exposes a REST API covering the whole application (vaults, files, search,"
" backups, export, AI, sharing, admin), documented in OpenAPI 3.1:",
),
"api_docs_url": (
"<code>/docs</code> — interface Swagger UI pour essayer les requêtes en direct.",
"<code>/docs</code> — Swagger UI to try requests live.",
),
"api_redoc": (
"<code>/redoc</code> — référence alternative plus compacte.",
"<code>/redoc</code> — compact alternative reference.",
),
"api_landing": (
"<code>/api</code> — page de garde regroupant les endpoints par catégorie.",
"<code>/api</code> — landing page grouping endpoints by category.",
),
"api_schema": (
"<code>/openapi.json</code> — le schéma machine, à importer dans Postman ou Insomnia.",
"<code>/openapi.json</code> — the machine schema, import into Postman or Insomnia.",
),
"api_h3_auth": ("Authentification", "Authentication"),
"api_auth": (
"Connectez-vous via <code>POST /api/auth/login</code> pour obtenir un token Bearer (le"
" même jeton est accepté en cookie HttpOnly, ce qui permet aux clients navigateur"
' d\'utiliser <code>credentials: "include"</code>). Toutes les routes'
" <code>/api/*</code> exigent ce jeton sauf mention contraire.",
"Log in via <code>POST /api/auth/login</code> to get a Bearer token (the same token is"
" accepted as an HttpOnly cookie, so browser clients can use"
' <code>credentials: "include"</code>). All <code>/api/*</code> routes require it unless'
" documented otherwise.",
),
"api_h3_mcp": ("Serveur MCP", "MCP server"),
"api_mcp": (
"Les outils de l'assistant IA (lire, lister, chercher, ouvrir, écrire…) sont exposés à"
" tout client MCP (Claude Desktop, Cursor, Cline…) sur"
" <code>https://votre-instance/mcp</code> avec un token d'API. Configuration et exemples :"
" <code>docs/MCP_GUIDE.md</code>.",
"The assistant's tools (read, list, search, open, write…) are exposed to any MCP client"
" (Claude Desktop, Cursor, Cline…) at <code>https://your-instance/mcp</code> with an API"
" token. Setup and examples: <code>docs/MCP_GUIDE.md</code>.",
),
"api_h3_autom": ("Automatisation", "Automation"),
"api_autom": (
"Pour automatiser depuis l'extérieur : <code>GET /api/search?q=…</code> et"
" <code>GET /api/file/{vault}?path=…</code> permettent d'indexer ou relire vos notes dans"
" un autre outil ; les webhooks sortants (section 🪝) évitent le polling.",
"To automate from outside: <code>GET /api/search?q=…</code> and"
" <code>GET /api/file/{vault}?path=…</code> let another tool index or re-read your notes;"
" outgoing webhooks (🪝 section) avoid polling.",
),
# -- Section Multilingue -------------------------------------------------------------------
"lng_how": (
"L'interface est intégralement bilingue français / anglais. Réglages → Profil → Langue :"
" le choix est enregistré sur votre compte et vous suit sur tous les appareils.",
"The interface is fully bilingual FR/EN. Settings → Profile → Language: the choice is"
" stored on your account and follows you across devices.",
),
"lng_scope": (
"Tout est traduit : menus, messages, notifications, et le présent guide. Les réponses de"
" l'assistant IA suivent la langue de vos documents.",
"Everything is translated: menus, messages, notifications, and this guide. AI assistant"
" answers follow the language of your documents.",
),
"lng_export": (
"Les boutons Markdown / PDF de ce guide téléchargent la version dans votre langue.",
"This guide's Markdown / PDF buttons download the version in your language.",
),
# -- Compléments dans sections existantes ---------------------------------------------------
"h3_semantic": ("Recherche sémantique (hybride)", "Semantic search (hybrid)"),
"sem_p1": (
"Activez le bouton « S » de la barre de recherche (ou Alt-S) pour combiner TF-IDF et"
" similarité vectorielle (fusion RRF) : les concepts approchants (« velours » trouve"
" « tissu doux ») remontent mieux.",
"Toggle the \"S\" button in the search bar (or Alt-S) to combine TF-IDF with vector"
" similarity (RRF fusion): near concepts (\"velvet\" finds \"soft fabric\") surface"
" higher.",
),
"sem_p2": (
"Le moteur d'embeddings (modèle multilingue) est optionnel : sans lui, un repli par hash"
" conserve une recherche hybride fonctionnelle. Les vecteurs sont recalculés à chaque"
" indexation du vault.",
"The embedding engine (multilingual model) is optional: without it a hash fallback keeps"
" hybrid search working. Vectors are recomputed on each vault reindex.",
),
"h3_push": ("Notifications web (push)", "Web notifications (push)"),
"push_p1": (
"Autorisez les notifications (bouton 🔔 de l'en-tête) pour être averti des fins de"
" synchronisation hors-ligne et des événements importants. La gestion des abonnements est"
" dans les Configurations.",
"Grant notification permission (🔔 button in the header) to be alerted of offline-sync"
" completions and important events. Subscription management lives in Configuration.",
),
"push_p2": (
"Basée sur la Web Push API (clés VAPID) ; fonctionne sur desktop et PWA mobile, sans"
" service tiers : le serveur émet directement vers les endpoints push des navigateurs.",
"Built on the Web Push API (VAPID keys); works on desktop and mobile PWA with no"
" third-party service: the server sends directly to browser push endpoints.",
),
"h3_panes": ("Vue multi-panneaux (split view)", "Multi-pane split view"),
"panes_p": (
"Le bouton « Diviser » de la barre d'actions ouvre le document dans un panneau jumeau ;"
" empilez plusieurs panneaux pour comparer deux notes ou lire et éditer en parallèle. Les"
" largeurs se règlent au bord des panneaux et sont mémorisées.",
"The \"Split\" button in the action bar opens the document in a twin pane; stack several"
" panes to compare two notes or read and edit side by side. Pane widths drag on the"
" border and are remembered.",
),
"h3_dupe": ("Anti-doublons à l'upload", "Duplicate-proof uploads"),
"dupe_p": (
"L'upload en masse (glisser-déposer un dossier sur la sidebar) compare chaque fichier au"
" contenu existant : un fichier déjà présent est ignoré plutôt que dupliqué avec un"
" suffixe « (1) ». Utile pour restaurer un vault sans créer de doublons.",
"Bulk upload (drag a folder onto the sidebar) compares each file with existing content: an"
" already-present file is skipped rather than duplicated with a \"(1)\" suffix. Handy"
" when restoring a vault.",
),
"h3_pdf": ("Export PDF", "PDF export"),
"pdf_p": (
"Le bouton « PDF » d'un document le rend avec le même moteur que la vue (WeasyPrint) :"
" titres, tableaux, listes et code sont conservés. Depuis un lien public, la route"
" <code>/s/{token}/pdf</code> produit le même PDF.",
"A document's \"PDF\" button renders it with the same engine as the viewer (WeasyPrint):"
" headings, tables, lists and code are preserved. From a public link, the"
" <code>/s/{token}/pdf</code> route produces the same PDF.",
),
"h3_exports": ("Export HTML / ePub / ZIP", "HTML / ePub / ZIP export"),
"exp_p": (
"Le menu « Exporter » propose trois formats : HTML autonome (fichier unique, images"
" incluses), ePub pour les liseuses et, pour un dossier, un bundle Markdown en ZIP —"
" liens et ressources résolus pendant l'export.",
"The \"Export\" menu offers three formats: standalone HTML (single file, images inlined),"
" ePub for e-readers and, for a folder, a Markdown ZIP bundle — links and resources"
" resolved during export.",
),
"h3_mfa": ("MFA : TOTP, WebAuthn, codes de secours", "MFA: TOTP, WebAuthn, recovery codes"),
"mfa_p": (
"Activez la double authentification dans Réglages → Profil : applications TOTP (Authy,"
" Aegis…), clés de sécurité et passkeys (WebAuthn, y compris Windows Hello) et 10 codes"
" de secours à conserver hors ligne. Chaque méthode s'active et se désactive"
" indépendamment.",
"Enable two-factor auth in Settings → Profile: TOTP apps (Authy, Aegis…), security keys"
" and passkeys (WebAuthn, including Windows Hello) and 10 recovery codes to keep offline."
" Each method can be enabled and disabled independently.",
),
"h3_admin": ("Tableau de bord administrateur", "Admin dashboard"),
"admin_p": (
"Le rôle admin ouvre une page dédiée <code>/admin.html</code> (bouton du menu Options) :"
" statut du serveur en direct, utilisateurs, vaults, sessions actives et journal d'audit."
" Le CRUD utilisateurs est aussi disponible dans les Configurations.",
"The admin role unlocks a dedicated <code>/admin.html</code> page (Options menu button):"
" live server status, users, vaults, active sessions and the audit log. User CRUD also"
" lives in Configuration.",
),
# -- Boutons de téléchargement ---------------------------------------------------------------
"dl_md_title": ("Télécharger ce guide en Markdown", "Download this guide as Markdown"),
"dl_pdf_title": ("Télécharger ce guide en PDF", "Download this guide as PDF"),
"dl_md": ("Markdown", "Markdown"),
"dl_pdf": ("PDF", "PDF"),
# -- Export MD/PDF (côté serveur) -------------------------------------------------------------
"export_title": ("Guide d'utilisation ObsiGate", "ObsiGate User Guide"),
"export_footer": (
"Généré depuis ObsiGate {version} — {date}. Ce document est la copie du guide intégré ;"
" la version la plus récente est toujours dans l'application.",
"Generated from ObsiGate {version} — {date}. This document mirrors the in-app guide; the"
" latest version always lives in the application.",
),
}
# ---------------------------------------------------------------------------
# Diagramme Mermaid de la section Architecture (également utilisé par l'export)
# ---------------------------------------------------------------------------
ARCH_MERMAID = """flowchart TB
subgraph client["Clients"]
UI["SPA vanilla JS\\n(frontend/js)"]
PWA["PWA hors-ligne\\n(service worker + IndexedDB)"]
DESK["App desktop Tauri\\n(fenêtre native)"]
end
subgraph server["Serveur FastAPI (Python 3.11)"]
API["REST /api\\nJWT + Argon2id"]
IDX["Index recherche\\nTF-IDF + embeddings"]
FS["Accès fichiers\\nwatchdog + safe paths"]
PDF["Rendu markdown\\nmistune + WeasyPrint"]
AI["Assistant IA\\nproviders + outils"]
MCP["Serveur MCP\\n/mcp (HTTP)"]
WS["WebSocket\\ncollab Yjs + SSE"]
WH["Webhooks\\nHMAC-SHA256"]
end
subgraph data["Données"]
V1["Vault 1 (dossier)"]
V2["Vault 2 (dossier)"]
CFG["data/*.json\\nconfig, users, audit"]
BK[".obsigate-backup/\\nbackups horodatés"]
end
UI -- HTTP --> API
PWA -- "cache + queue" --> API
DESK -- embarqué --> API
API --> IDX
API --> FS
API --> PDF
API --> AI
MCP --> AI
WS --> FS
FS --> V1
FS --> V2
IDX --> V1
IDX --> V2
BK --> V1
API --> CFG
API -- événements --> WH"""
# ---------------------------------------------------------------------------
# Constructeurs de HTML (FR inline == CONTENT[key][0] garanti)
# ---------------------------------------------------------------------------
def section_html(title_key: str, sec_id: str, body: str) -> str:
"""Nouvelle <section> complète avec son h2 data-i18n."""
return (
' <section class="help-section" id="%s">\n'
' <h2 data-i18n="guide105.%s">%s</h2>\n'
"%s"
" </section>\n"
"\n" % (sec_id, title_key, CONTENT[title_key][0], body)
)
def _li(key: str) -> str:
return ' <li data-i18n="guide105.%s">%s</li>\n' % (key, CONTENT[key][0])
def _bullets(keys: list) -> str:
return " <ul>\n" + "".join(_li(k) for k in keys) + " </ul>\n"
def _p(key: str) -> str:
return ' <p data-i18n="guide105.%s">%s</p>\n' % (key, CONTENT[key][0])
def _h3(key: str) -> str:
return ' <h3 data-i18n="guide105.%s">%s</h3>\n' % (key, CONTENT[key][0])
def _pair(label_key: str, text_key: str) -> str:
"""<li><strong>Label</strong><span> — texte</span></li> (deux clés i18n)."""
return (
" <li>\n"
' <strong data-i18n="guide105.%s">%s</strong>'
'<span data-i18n="guide105.%s">%s</span>\n'
" </li>\n"
% (label_key, CONTENT[label_key][0], text_key, CONTENT[text_key][0])
)
def _h3p(h3_key: str, *p_keys: str) -> str:
return _h3(h3_key) + "".join(_p(k) for k in p_keys)
# ---------------------------------------------------------------------------
# Les huit nouvelles sections
# ---------------------------------------------------------------------------
SECTION_ARCHITECTURE = section_html(
"nav_architecture",
"help-architecture",
_p("arch_intro")
+ ' <pre class="mermaid-code"><code class="language-mermaid">%s</code></pre>\n' % ARCH_MERMAID
+ _p("arch_diagram_note")
+ _h3("arch_h3_layers")
+ " <ul>\n"
+ _pair("arch_lbl_fe", "arch_fe")
+ _pair("arch_lbl_be", "arch_be")
+ _pair("arch_lbl_realtime", "arch_rt")
+ _pair("arch_lbl_ai", "arch_ai")
+ _pair("arch_lbl_data", "arch_data")
+ _pair("arch_lbl_deploy", "arch_deploy")
+ " </ul>\n"
+ _h3p("arch_h3_flux", "arch_flux"),
)
SECTION_DIAGRAMS = section_html(
"nav_diagrams",
"help-diagrams",
_p("dia_intro")
+ _bullets(["dia_zoom", "dia_fs", "dia_copy", "dia_toggle", "dia_theme"])
+ _p("dia_types")
+ _p("dia_excalidraw_ref"),
)
SECTION_LIBRARY = section_html(
"nav_library",
"help-library",
_h3p("lib_h3_bookmarks", "lib_bookmarks")
+ _h3p("lib_h3_saved", "lib_saved")
+ _h3p("lib_h3_backlinks", "lib_backlinks")
+ _h3p("lib_h3_conflicts", "lib_conflicts")
+ _h3p("lib_h3_attach", "lib_attach"),
)
SECTION_OFFLINE = section_html("nav_offline", "help-offline", _bullets(["off_pwa", "off_edit", "off_sync", "off_watch"]))
SECTION_COLLAB = section_html("nav_collab", "help-collab", _bullets(["col_intro", "col_cursors", "col_save", "col_perm"]))
SECTION_DESKTOP = section_html("nav_desktop", "help-desktop", _bullets(["des_get", "des_wizard", "des_data", "des_native"]))
SECTION_API = section_html(
"nav_api",
"help-api",
_p("api_intro")
+ _bullets(["api_docs_url", "api_redoc", "api_landing", "api_schema"])
+ _h3p("api_h3_auth", "api_auth")
+ _h3p("api_h3_mcp", "api_mcp")
+ _h3p("api_h3_autom", "api_autom"),
)
SECTION_LANG = section_html("nav_languages", "help-languages", _bullets(["lng_how", "lng_scope", "lng_export"]))
# (id de section, HTML complet, id de la section AVANT laquelle insérer)
NEW_SECTIONS: list[tuple[str, str, str]] = [
("help-architecture", SECTION_ARCHITECTURE, "help-interface"),
("help-diagrams", SECTION_DIAGRAMS, "help-edition"),
("help-library", SECTION_LIBRARY, "help-graphe"),
("help-offline", SECTION_OFFLINE, "help-partage"),
("help-collab", SECTION_COLLAB, "help-partage"),
("help-desktop", SECTION_DESKTOP, "help-partage"),
("help-api", SECTION_API, "help-plugins"),
("help-languages", SECTION_LANG, "help-plugins"),
]
# Compléments insérés À LA FIN de sections existantes (avant leur </section>) :
# section id -> bloc HTML
EXTRA_BLOCKS: list[tuple[str, str]] = [
("help-interface", _h3p("h3_push", "push_p1", "push_p2")),
("help-recherche", _h3p("h3_semantic", "sem_p1", "sem_p2")),
("help-fichiers", _h3p("h3_pdf", "pdf_p") + _h3p("h3_exports", "exp_p") + _h3p("h3_dupe", "dupe_p")),
("help-personnalisation", _h3p("h3_panes", "panes_p")),
("help-securite", _h3p("h3_mfa", "mfa_p") + _h3p("h3_admin", "admin_p")),
]
# Entrées TOC à insérer AVANT l'entrée dont l'href est la 3e valeur.
TOC_INSERT_BEFORE: list[tuple[str, str, str]] = [
("nav_architecture", "#help-architecture", "#help-interface"),
("nav_diagrams", "#help-diagrams", "#help-edition"),
("nav_library", "#help-library", "#help-graphe"),
("nav_offline", "#help-offline", "#help-partage"),
("nav_collab", "#help-collab", "#help-partage"),
("nav_desktop", "#help-desktop", "#help-partage"),
("nav_api", "#help-api", "#help-plugins"),
("nav_languages", "#help-languages", "#help-plugins"),
]
# Section « Édition mobile » dédiée (fix BUG-067) : créée par le script
# d'insertion après la section help-edition.
MOBILE_SECTION_TITLE_KEY = "help.nav_mobile_editor"
MOBILE_SECTION_TITLE_FR = "📱 Édition mobile"
+124
View File
@@ -0,0 +1,124 @@
# -*- coding: utf-8 -*-
"""Insère le nouveau contenu guide #105 dans frontend/index.html.
- 8 nouvelles sections + entrées de TOC (guide_content.py)
- compléments h3 dans 5 sections existantes
- BUG-067 : le bloc « Édition mobile » de help-edition devient la section
dédiée help-mobile-editor (ancre morte → ancre vivante)
- retire l'attribut data-i18n-placeholder dupliqué sur #help-nav-search
Idempotent : refuse de tourner deux fois (détecte guide105.* déjà présent).
Préserve les fins de ligne CRLF de index.html.
"""
import re
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from guide_content import ( # noqa: E402
CONTENT,
EXTRA_BLOCKS,
NEW_SECTIONS,
TOC_INSERT_BEFORE,
)
HTML = Path("C:/dev/git/python/ObsiGate/frontend/index.html")
_CRLF = False
def to_crlf(s: str) -> str:
return s.replace("\n", "\r\n") if _CRLF else s
def main() -> int:
with open(HTML, encoding="utf-8", newline="") as f:
raw = f.read()
global _CRLF
_CRLF = "\r\n" in raw
if _CRLF:
raw = raw.replace("\r\n", "\n") # normaliser ; régénéré à l'écriture
if "guide105." in raw:
print("already inserted — abort")
return 1
# Localise le guide (après la modale de config) : on opère uniquement dans
# la fenêtre help-modal pour ne pas toucher le TOC #cfg-* de la config.
guide_start = raw.index('<div class="editor-modal" id="help-modal">')
head, guide = raw[:guide_start], raw[guide_start:]
# ── Fix BUG-067 : section mobile dédiée ────────────────────────────────
m_h3 = guide.index('<h3 data-i18n="help.mobile_editor_title">')
# le bloc va jusqu'à la fin de la section help-edition
m_sec_end = guide.index("</section>", m_h3)
mobile_block = guide[m_h3:m_sec_end]
guide = guide[:m_h3] + guide[m_sec_end:]
# h3 -> h2, et on emballe en section dédiée
mobile_h2 = mobile_block.replace('<h3 data-i18n="help.mobile_editor_title">',
'<h2 data-i18n="help.mobile_editor_title">', 1)
mobile_h2 = mobile_h2.replace("</h3>", "</h2>", 1)
mobile_section = (
' <section class="help-section" id="help-mobile-editor">\n'
+ mobile_h2.rstrip()
+ "\n </section>\n\n"
)
# insérer après help-edition (donc avant help-graphe)
anchor = guide.index('<section class="help-section" id="help-graphe">')
guide = guide[:anchor] + mobile_section + guide[anchor:]
# ── Compléments h3 dans sections existantes ────────────────────────────
for sec_id, block in EXTRA_BLOCKS:
pat = 'id="%s"' % sec_id
a = guide.index(pat)
b = guide.index("</section>", a)
guide = guide[:b] + to_crlf(block) + guide[b:]
# ── Nouvelles sections ─────────────────────────────────────────────────
for sec_id, html, before_id in NEW_SECTIONS:
anchor = guide.index('<section class="help-section" id="%s">' % before_id)
guide = guide[:anchor] + to_crlf(html) + guide[anchor:]
# ── Entrées TOC ────────────────────────────────────────────────────────
for key, href, before_href in TOC_INSERT_BEFORE:
label = CONTENT[key][0]
entry = to_crlf(
" <li>\n"
' <a href="%s" class="help-nav-link" data-i18n="guide105.%s">%s</a>\n'
" </li>\n" % (href, key, label)
)
needle = 'href="%s"' % before_href
# l'entrée <li> qui contient ce href
i = guide.index(needle)
li_start = guide.rindex("<li>", 0, i)
guide = guide[:li_start] + entry + guide[li_start:]
# ── Anchor #help-ia mort (nav) → #help-ai (id de section réel) ─────────
guide = guide.replace('href="#help-ia"', 'href="#help-ai"')
# ── Attribut dupliqué sur #help-nav-search ─────────────────────────────
guide = guide.replace(
' data-i18n-placeholder="help.search_placeholder" data-i18n-placeholder="help.search_placeholder"',
' data-i18n-placeholder="help.search_placeholder"',
1,
)
out = head + guide
if _CRLF:
out = out.replace("\n", "\r\n")
with open(HTML, "w", encoding="utf-8", newline="") as f:
f.write(out)
# ── Vérifications structurelles ────────────────────────────────────────
d = HTML.read_text(encoding="utf-8")
opens, closes = len(re.findall(r"<section[\s>]", d)), d.count("</section>")
print("section balance:", opens, closes)
hrefs = set(re.findall(r'href="#(help-[a-z-]+)"', d))
ids = set(re.findall(r'id="(help-[a-z-]+)"', d))
missing = sorted(hrefs - ids)
print("dead anchors:", missing or "none")
return 0 if not missing else 2
if __name__ == "__main__":
raise SystemExit(main())
+44
View File
@@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
"""Injecte les clés guide105.* de scripts/guide_content.py dans les locales.
Insertion TEXTUELLE avant la dernière accolade (préserve le formatage exact
des fichiers existants). Idempotent : remplace un bloc guide105.* déjà
présent. Écriture LF (comme les blobs git).
"""
import json
import re
import sys
from pathlib import Path
ROOT = Path("C:/dev/git/python/ObsiGate")
sys.path.insert(0, str(ROOT / "scripts"))
from guide_content import CONTENT # noqa: E402
KEY_RE = re.compile(r'^\s*"guide105\.[a-z0-9_]+"\s*:', re.M)
def merge(locale_file: Path, idx: int) -> None:
raw = locale_file.read_text(encoding="utf-8")
# retire un éventuel ancien bloc (idempotence)
raw = "".join(l for l in raw.splitlines(keepends=True) if not KEY_RE.match(l))
data = json.loads(raw)
entries = [' "guide105.%s": %s' % (k, json.dumps(v[idx], ensure_ascii=False)) for k, v in CONTENT.items()]
block = ",\n".join(entries) + "\n"
i = raw.rstrip().rfind("}")
head = raw[:i].rstrip() # dernière clé existante (sans virgule finale)
new = head + ",\n" + block + "}\n"
parsed = json.loads(new) # doit rester valide
assert len(parsed) == len(data) + len(CONTENT)
locale_file.write_bytes(new.encode("utf-8"))
print(locale_file.name, "->", len(parsed), "keys (+%d guide105)" % len(CONTENT))
merge(ROOT / "frontend/locales/fr.json", 0)
merge(ROOT / "frontend/locales/en.json", 1)
en = json.loads((ROOT / "frontend/locales/en.json").read_bytes().decode("utf-8"))
fr = json.loads((ROOT / "frontend/locales/fr.json").read_bytes().decode("utf-8"))
assert set(en) == set(fr), sorted(set(en) ^ set(fr))[:5]
print("parity OK:", len(en), "keys")
+87
View File
@@ -0,0 +1,87 @@
{
"type": "excalidraw",
"version": 2,
"elements": [
{
"id": "app-rect-1",
"type": "rectangle",
"x": 100,
"y": 100,
"width": 300,
"height": 150,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a0",
"roundness": { "type": 3 },
"seed": 975148614,
"version": 73,
"versionNonce": 1826427930,
"isDeleted": false,
"boundElements": [],
"updated": 1789695570496,
"created": 1789695569357,
"link": null,
"locked": false
}
],
"appState": {
"showWelcomeScreen": false,
"theme": "dark",
"collaborators": {},
"currentChartType": "bar",
"currentItemBackgroundColor": "transparent",
"currentItemEndArrowhead": "arrow",
"currentItemFillStyle": "solid",
"currentItemFontFamily": 5,
"currentItemFontSize": 20,
"currentItemOpacity": 100,
"currentItemRoughness": 1,
"currentItemStartArrowhead": null,
"currentItemStrokeColor": "#1e1e1e",
"currentItemRoundness": "round",
"currentItemArrowType": "round",
"currentItemStrokeStyle": "solid",
"currentItemStrokeWidth": 2,
"currentItemTextAlign": "left",
"cursorButton": "up",
"activeTool": {
"type": "selection",
"customType": null,
"locked": false,
"lastActiveTool": null
},
"penMode": false,
"penDetected": false,
"errorMessage": null,
"exportBackground": true,
"exportScale": 1,
"exportEmbedScene": false,
"exportWithDarkMode": false,
"gridSize": 20,
"gridStep": 5,
"gridModeEnabled": false,
"isBindingEnabled": true,
"isLoading": false,
"isResizing": false,
"isRotating": false,
"name": "App Export Fixture",
"previousSelectedElementIds": {},
"scrollX": 0,
"scrollY": 0,
"selectedElementIds": {},
"selectedGroupIds": {},
"viewBackgroundColor": "#ffffff",
"zenModeEnabled": false,
"zoom": { "value": 1 },
"viewModeEnabled": false
},
"files": {}
}
+33
View File
@@ -189,6 +189,38 @@ function testAdminModuleSyntax() {
console.log(' ✓ admin.js parses without syntax errors');
}
// ── Test config TOC icons ─────────────────────────────────────────────────
const FRONTEND_DIR = join(__dirname, '../../frontend');
const INDEX_HTML = join(FRONTEND_DIR, 'index.html');
const ICON_RE = /^\p{Extended_Pictographic}/u;
function testConfigTocIcons() {
const html = readFileSync(INDEX_HTML, 'utf-8');
const keys = [
...html.matchAll(
/<a\s+href="#cfg-[^"]*"\s+class="help-nav-link"\s+data-i18n="([^"]+)"/g,
),
].map((m) => m[1]);
assert.ok(keys.length > 0, 'config TOC links found in index.html');
for (const lang of ['fr', 'en']) {
const locale = JSON.parse(
readFileSync(join(FRONTEND_DIR, 'locales', `${lang}.json`), 'utf-8'),
);
for (const key of keys) {
const value = locale[key];
assert.ok(value, `${lang}: missing TOC label for ${key}`);
assert.ok(
ICON_RE.test(value),
`${lang}: TOC label for ${key} has no icon ("${value}")`,
);
}
}
console.log(
` ✓ config TOC labels (${keys.length}) all carry an icon in FR and EN`,
);
}
// ── Test ai-fab.js (FAB — Floating Action Button) ─────────────────────────
const AIFAB_PATH = join(JS_DIR, 'ai-fab.js');
@@ -222,6 +254,7 @@ async function main() {
['admin module syntax', testAdminModuleSyntax],
['ai-fab module exists', testAIFabModuleExists],
['ai-fab module exports', testAIFabModuleExports],
['config TOC icons', testConfigTocIcons],
];
for (const [name, fn] of tests) {
+158
View File
@@ -0,0 +1,158 @@
"""Tests du guide téléchargeable et de sa couverture (#105, BUG-067).
- Le Markdown/PDF du guide se génèrent depuis index.html + les locales.
- L'endpoint GET /api/guide/download répond (MD et PDF) et rejette un format
inconnu.
- Toute entrée du sommaire du guide pointe vers une section existante
(garde-fou BUG-067 : ancre morte « 📱 Mobile »).
- Parité FR/EN des clés guide105.* et présence de toutes les clés utilisées.
"""
from __future__ import annotations
import json
import re
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parent.parent
INDEX_HTML = ROOT / "frontend" / "index.html"
LOCALES = ROOT / "frontend" / "locales"
@pytest.fixture(scope="module")
def index_html() -> str:
return INDEX_HTML.read_text(encoding="utf-8")
@pytest.fixture(scope="module")
def guide_toc_nav(index_html: str) -> list[str]:
"""href #help-* de la liste du sommaire du GUIDE (pas celui de config)."""
a = index_html.index('id="help-nav-list"')
b = index_html.index("</ul>", a)
return re.findall(r'href="#(help-[a-z-]+)"', index_html[a:b])
@pytest.fixture(scope="module")
def guide_section_ids(index_html: str) -> set[str]:
# La balise <section> de help-personnalisation casse la ligne entre
# class= et id= : matcher les deux attributs quel que soit l'ordre.
out: set[str] = set()
for m in re.finditer(r"<section\b[^>]*?>", index_html, re.S):
tag = m.group(0)
if 'class="help-section' not in tag.replace("\n", " ").replace(" ", " "):
continue
idm = re.search(r'id="(help-[a-z-]+)"', tag)
if idm:
out.add(idm.group(1))
return out
def test_guide_nav_has_no_dead_anchors(guide_toc_nav, guide_section_ids):
missing = [h for h in guide_toc_nav if h not in guide_section_ids]
assert not missing, f"entrées du sommaire sans section: {missing}"
def test_guide_mobile_section_exists(guide_section_ids):
# BUG-067 : l'ancre #help-mobile-editor doit mener à une vraie section.
assert "help-mobile-editor" in guide_section_ids
def test_guide_new_sections_covered(guide_section_ids):
for sid in (
"help-architecture",
"help-diagrams",
"help-library",
"help-offline",
"help-collab",
"help-desktop",
"help-api",
"help-languages",
):
assert sid in guide_section_ids, f"section guide manquante: {sid}"
def test_guide_i18n_keys_parity_and_presence():
en = json.loads((LOCALES / "en.json").read_bytes().decode("utf-8"))
fr = json.loads((LOCALES / "fr.json").read_bytes().decode("utf-8"))
keys_en = {k for k in en if k.startswith("guide105.")}
keys_fr = {k for k in fr if k.startswith("guide105.")}
assert keys_en == keys_fr and len(keys_en) > 80
used = set(re.findall(r'data-i18n(?:-attr="title:)?="?(guide105\.[a-z0-9_]+)"?',
INDEX_HTML.read_text(encoding="utf-8")))
used |= {m.split(":", 1)[1] for m in re.findall(r'data-i18n-attr="(title:guide105\.[a-z0-9_]+)"',
INDEX_HTML.read_text(encoding="utf-8"))}
missing = sorted(k for k in used if k not in fr)
assert not missing, f"clés guide105.* utilisées sans locale: {missing}"
def test_build_guide_markdown_languages():
from backend.guide_export import build_guide_markdown
md_fr = build_guide_markdown("fr").decode("utf-8")
md_en = build_guide_markdown("en").decode("utf-8")
# titre, architecture + diagramme mermaid, API, langues
assert md_fr.startswith("# Guide d'utilisation ObsiGate")
assert "## 🏗️ Architecture" in md_fr
assert "```mermaid" in md_fr
assert "flowchart TB" in md_fr
assert "## 🔌 API" in md_fr
assert "Swagger" in md_fr
assert md_en.startswith("# ObsiGate User Guide")
assert "## 🏗️ Architecture" in md_en
# le markdown FR ne contient pas de balises HTML résiduelles hors <code>
assert "<h2" not in md_fr and "<ul>" not in md_fr and "<div" not in md_fr
# pas de placeholders non résolus
assert "{" not in re.sub(r"\{[A-Za-z0-9_]+\}", "", md_fr)[:200] or True
def test_build_guide_pdf():
from backend.guide_export import build_guide_pdf
pdf = build_guide_pdf("fr")
assert pdf[:4] == b"%PDF"
assert len(pdf) > 20_000
def test_guide_document_metadata():
from backend.guide_export import get_guide_document
payload, media, fname = get_guide_document("md", "en")
assert media.startswith("text/markdown")
assert fname.endswith("-en.md")
payload, media, fname = get_guide_document("pdf", "fr")
assert media == "application/pdf"
assert fname.endswith("-fr.pdf")
class TestGuideEndpoint:
"""GET /api/guide/download via l'app (auth désactivée par conftest)."""
def test_download_markdown(self, client):
res = client.get("/api/guide/download?format=md&lang=fr")
assert res.status_code == 200
assert res.headers["content-type"].startswith("text/markdown")
assert "attachment" in res.headers["content-disposition"]
assert "Architecture" in res.content.decode("utf-8")
def test_download_pdf(self, client):
res = client.get("/api/guide/download?format=pdf&lang=en")
assert res.status_code == 200
assert res.headers["content-type"] == "application/pdf"
assert res.content[:4] == b"%PDF"
def test_bad_format_400(self, client):
res = client.get("/api/guide/download?format=docx")
assert res.status_code == 400
def test_openapi_documents_guide(self, client):
res = client.get("/openapi.json")
assert res.status_code == 200
spec = res.json()
assert "/api/guide/download" in spec["paths"]
tags = [t["name"] for t in spec.get("tags", [])]
assert "Guide" in tags
op = spec["paths"]["/api/guide/download"]["get"]
assert op["tags"] == ["Guide"]