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

This commit is contained in:
2026-09-18 13:06:30 -04:00
parent 82f6b4a791
commit fb2d83e9e3
25 changed files with 2216 additions and 45 deletions
+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")