# Changelog - FlowDeck ## v7.71.5 (2026-10-10) — Galerie système : +94 templates (135 au total) ### Added - **Galerie système portée à 135 presets** (auparavant 41, + 6 bases legacy = 47 installés). Nouveaux templates, tous genres : - **+39 pages** — Semainier, Daily standup, Agenda de réunion, Compte rendu de sprint, Kickoff/Registre des risques/Leçons apprises/Note de cadrage/Plan de communication, Runbook d'incident, Checklist de code review, Doc d'API/architecture, Plan de test, Changelog, User persona, Guide d'entretien utilisateur, Roadmap produit, Business Model Canvas, SWOT, RACI, Proposition commerciale, Facture, Plan marketing, Brief, Script d'appel, Rapport de performance, Fiche de poste, Guide d'entretien d'embauche, Politique interne, Guide d'installation, Plan de repas, Liste de courses, Checklist de ménage, Suivi d'entraînement, Plan de carrière, Rétro perso, Checklist de valise, Boîte à idées. - **+9 blocs** — Étapes numérotées, Avantages/Inconvénients, Mise en garde + Astuce, Définition, Ressources/liens, Contacts clés, Présentation, Comparatif d'options, Signature d'email. - **+26 bases** — Pipeline de vente, Devis/Factures, Réunions, Registre des décisions, Backlog/Idées, Sprints, Roadmap/Fonctionnalités, Tests QA, Risques, Candidatures, Formations & cours, Compétences, Abonnements, Budget/Revenus, Dépenses partagées, Campagnes marketing, Publications réseaux sociaux, KPI/Métriques, Événements, Recettes, Films & séries, Voyages, Fournisseurs, Matériel/Équipement, Rendez-vous santé, Entraînements. - **+20 lignes** — Client, Opportunité, Devis, Facture, Réunion, Décision, Risque, Idée, Fonctionnalité, Cas de test, Vidéo, Événement, Voyage, Recette, Séance d'entraînement, Candidature, Formation, Fournisseur, Abonnement, Indicateur (base cible résolue par nom). Les installations existantes sont rafraîchies automatiquement au boot (`PRESET_VERSION`), sans toucher aux copies personnelles. ## v7.71.4 (2026-10-10) — Templates de page : titre automatique, plus d'erreur « variable requise » ### Fixed - **« Échec : HTTP 400 {"detail":"variable requise manquante : title"} »** — plusieurs presets de page déclaraient `title` comme variable *requise* ; le sélecteur l'envoyait vide → refus. `title` est désormais une **variable système réservée** (jamais requise) : une valeur fournie prime, sinon la résolution retombe sur le titre de la page cible puis sur le nom du template. Les presets concernés déclarent `title` en optionnel. - **Le titre du document suit le template choisi** — appliquer un template de page à la page courante (ou via `/template`, ou en créer une nouvelle) définit maintenant le titre à partir de `title_template` (ex. « CR — Compte rendu client (2026-10-10) »), au lieu de laisser « Untitled ». Le titre est mis à jour en place dans l'éditeur sans rechargement. ## v7.71.3 (2026-10-10) — Templates : commande `/` + galerie enrichie (> 20 presets) ### Added - **Commande slash `/template`** dans l'éditeur : elle ouvre le même sélecteur de templates mais **sans les templates de page** — filtres limités à *Bases / Lignes / Blocs* (plus « Tous »). Un template de blocs s'insère au curseur, un template de base crée une base inline dans le document, un template de ligne crée la ligne dans sa base (sans quitter la page). - **Galerie système enrichie à 38 presets** (auparavant 7), tous genres confondus : - **18 pages** : Note de réunion, Compte rendu client, 1:1, Revue hebdo, Journal quotidien, Journal de gratitude, Spécification produit, Plan de projet, Rétrospective, Post-mortem incident, Objectifs OKR, Décision (ADR), Procédure (SOP), Onboarding, Notes de cours, Fiche de lecture, Recette, Itinéraire, Fiche prospect, Communiqué… - **6 blocs** : Sommaire + actions, Questions de réunion, Matrice d'Eisenhower, FAQ, Citation + source, Checklist de démarrage. - **10 bases** : Tâches, Gestion de projet, Suivi de bugs, Contenu éditorial, Habitudes, Dépenses, Inventaire, Bibliothèque de liens, Objectifs, Annuaire d'équipe. - **5 lignes** : Tâche, Bug, Article, Dépense, Habitude (résolues par nom de base cible — utilisables depuis le menu *New ▾* d'une base ou le sélecteur). ### Fixed - **Types de blocs des presets** : les entêtes utilisaient `heading` (rendu comme un simple paragraphe) et les cases à cocher `todo` — corrigés en `heading_1/2/3` et `to_do` (types réels de l'éditeur). Les presets système déjà installés sont **rafraîchis automatiquement** au boot (`preset_version`), sans toucher aux copies personnelles de l'utilisateur. - **Manifeste des presets de base** : `ensure_system_presets` ignorait la définition `collection` (schéma) → les bases système étaient créées vides ; le manifeste est désormais transmis, et une base créée depuis un preset porte ses propriétés (Statut, Priorité, Échéance…). - **Templates de ligne** : la base cible d'un preset est résolue par nom (`target_collection_name`) dans le workspace, avec un message explicite si la base n'existe pas — au lieu d'un échec technique. ## v7.71.2 (2026-10-10) — Templates : flux de création des documents + menu contextuel ### Changed - **Un seul flux de création de document, cohérent partout.** Le bouton « New page » (barre de titre du workspace local, actions de section, menu « New » en bas de la sidebar, menu contextuel) **crée désormais un document vide** et navigue dessus — comportement Notion. Il n'ouvre plus l'ancien panneau « New page from template » (supprimé) dont les boutons Annuler/fermer étaient inopérants (collision d'`id` `fd-tpl-picker` avec le sélecteur de l'éditeur : `document.getElementById('fd-tpl-close')` renvoyait `null`, la fermeture n'était jamais câblée, et l'overlay bloquait le reste de l'UI — d'où le menu contextuel perçu comme cassé). - **Nouveau sélecteur global « Nouveau depuis un template »**, adossé au registre unifié (`/api/templates`) : accessible depuis le menu « New » de la sidebar (*From template…*), utilisable sur toutes les pages (sidebar, workspace local, library), avec recherche, filtres, « Gérer… », état vide/erreur explicite et Annuler/✕ fonctionnels. - **Filtres du sélecteur de l'éditeur restylés** (segmented control partagé `.fd-tpl-filter`, libellés FR : Tous / Pages / Bases / Lignes / Blocs, état actif accentué, survol) — mêmes classes que le sélecteur global, aucun nouveau langage visuel. ### Fixed - Menu contextuel clic droit de la section Workspace : plus bloqué par l'ancien overlay figé du panneau template. ### Tests - `e2e/templates_picker.spec.js` : +2 (le bouton « New page » crée un document vide et navigue ; le menu *New > From template* ouvre le sélecteur global et le ✕ ferme) ; filtres mis à jour (libellés FR). ## v7.71.1 (2026-10-10) — Templates : correctifs du sélecteur (centrage, filtres, aperçu, base inline) ### Fixed - **Fenêtre « Appliquer un template » centrée** — l'overlay portait `display:flex` en style inline ; Alpine, au retour de `x-show`, réapplique `display:block` et écrasait le flex → le dialogue restait collé en haut à gauche. Le flex vit désormais dans une enveloppe distincte de `x-show` (l'overlay ne contrôle plus que la visibilité). - **Boutons de filtre `Tous / page / blocks / row / database` inopérants** — les handlers inline contenaient deux expressions séparées par `;` (`tplKind = k; loadTplList()`), refusées par le parseur CSP d'Alpine.js (« CSP Parser Error: Unexpected token: loadTplList »). Une méthode unique `setTplKind(k)` / `setTplVar(name, value)` est appelée depuis les handlers (une seule expression par binding, invariant build CSP). - **« Aperçu indisponible : HTTP 400 » sur certains templates** — l'aperçu partageait la résolution stricte de l'instanciation : une variable déclarée requise non encore saisie levait `ValueError` → 400. L'aperçu utilise désormais un mode non strict (`_resolve_all(..., strict=False)`) : les tokens non résolus restent visibles avec un avertissement « À compléter », seule l'instanciation exige la saisie. - **Appliquer un template de type Database dans le document courant** — la création partait dans une base détachée sans insertion. Le sélecteur transmet `parent_page_id`, la collection est créée INLINE (`is_inline=1`, `parent_page_id`) et le front insère le bloc embed `collection` à la position du curseur, sans navigation (même patron que la pilule « Database »). ### Tests - `tests/test_fd_templates.py` : +3 (aperçu jamais 400, presets système, base inline rattachée à la page) — 13 tests. - `e2e/templates_picker.spec.js` : +2 (filtres par genre, insertion d'un template Database dans le document) — 4 tests. ## v7.71.0 (2026-10-10) — Templates unifiés façon Notion (registre + sélecteur + gestionnaire) ### Fixed - **Bouton « Templates » de la page vide réparé (cause établie par preuve, phase 0 du cahier des charges)** — la pilule portait `@click="gsMoreOpen = !gsMoreOpen"`, soit exactement le même flag que le bouton « ••• » : le clic ouvrait le menu de création de base (Table/Board/List…) au lieu d'un sélecteur (hypothèses H1+H4 confirmées par lecture du code ; H2 écartée — le composant `editorState` est bien enregistré via `Alpine.data`). La pilule et l'entrée « Apply template… » du menu ••• ouvrent désormais le sélecteur unifié. ### Added - **Registre unifié `templates` (migrations 48 + 49)** — catalogue les trois silos legacy (`database_templates`, `page_templates`, `page_global_templates`) sans les détruire (backfill idempotent au boot, adaptateurs de lecture) ; journal `template_runs` (compteurs, audit, idempotence) et `template_recurrences` (RRULE + scheduler 60 s dédupliqué par `occurrence_key`). - **`TemplateService.instantiate()` unique** — UI, API v2, agent (`apply_template` rebranché + nouvel outil lecture `list_templates`) et scheduler partagent la même instanciation transactionnelle : variables système (`{{today}}`, `{{now}}`, `{{me}}`, `{{date:+7d}}`…) et déclarées figées à la création, validation via `validate_property_value()`, dégradation avec avertissements (propriété renommée), aucun `{{token}}` survivant, pages-supports masquées (hors recherche/arbre/récents). - **Sélecteur (§7.1)** : recherche, filtres par genre, aperçu résolu, formulaire de variables, insertion de blocs au curseur / remplissage de la page en place / création + navigation, toast avec annulation. - **Gestionnaire `/templates`** (entrée sidebar + page SSR) : liste filtrable, side peek (aperçu, variables, défauts, récurrence, journal), création (vierge, depuis page/ligne/base, import `.json`), duplication, archivage, corbeille, export/import au format `flowdeck-template` v1. - **Menu *New ▾* des bases** : `GET /api/collections/{id}/templates` (défaut ★, créations dédiées) ; **galerie système** (7 presets) ; **API interne** `/api/templates…` et **API v2** `/api/v2/fd-templates…` (liste, instantiate idempotent, export, import). - **Tests** : `tests/test_fd_templates.py` (10 tests, §17.2) et `e2e/templates_picker.spec.js` (régression T0-3 : le clic ouvre le sélecteur et applique visiblement un template). ## v7.70.1 (2026-10-10) — Refonte navigation Repository : fix cache + erreurs console ### Fixed - **Sidebar Repository et pages forge : erreurs console « Undefined variable: filteredTreeItems / viewMode » corrigées** — la refonte avait fait évoluer `gitea_workspace.html` sans faire évoluer `?v=` (VERSION inchangée) : les navigateurs rejouaient l'ancien `gitea_workspace.js` en cache contre le nouveau HTML, d'où des variables Alpine inconnues et un arbre Repository inopérant. Le bump de version invalide le cache et resynchronise HTML/JS. - **Échecs réseau/Gitea désormais visibles au lieu d'un silence console** — `loadSidebarTree`/`loadMainTree` affichent un message dans la section Repository (« Connect Gitea/GitHub… », « Could not reach… » avec bouton Retry) au lieu de laisser « Loading... » bloqué et de n'écrire que dans la console. - **Clics sidebar Repository attachés une seule fois** — `setupSidebarClicks` posait un listener global `document` à chaque refresh, accumulant les doublons (double toggle/fetch). Garde `_sidebarClicksBound`. ## v7.70.0 (2026-10-09) — Projet Gitea : présentation + workspace local en parallèle ### Fixed - **Ouverture d'un projet Gitea : le workspace local miroir est désormais ouvert en parallèle dès le premier affichage** — `GET /gitea-workspace` calculait la sidebar **avant** de créer le workspace miroir, donc le premier rendu montrait « No workspace open » / un `localWsId: 0` et la section « Workspace » restait vide (il fallait recharger). Le miroir est maintenant créé/résolu **avant** `_sidebar_data`, qui reçoit en plus le contexte projet explicite `owner`/`repo` : la navigation directe ou un favori `?owner=…&repo=…` (sans cookie `flowdeck_workspace`) résout correctement le miroir au lieu de retomber sur « aucune espace ouverte ». - **La carte « Workspace » de la sidebar n'est plus écrasée par l'arbre du dépôt** — `loadGiteaTree()` (base.html) ciblait `#sidebar-workspace-items` (l'arbre local) au lieu de `#sidebar-gitea-items` (la section « Repository (Gitea) »), et son garde testait le mauvais conteneur : à chaque page en contexte Gitea, il remplaçait l'arbre du miroir local par une copie non interactive de la racine du dépôt. Il cible désormais la section Repository — l'équivalent local et le dépôt distant coexistent. - **`GET /api/sidebar/workspace-tree` rend le miroir local, plus « Remote workspace »** — après une création/renommage/déplacement (appel `refreshSidebarTree()`), le cookie `gitea:owner:repo` renvoyait un placeholder qui effaçait l'arbre du miroir. La route résout le workspace miroir et rend son arbre (repli « No pages yet » s'il n'existe pas). ### Tests - `tests/test_v7700_gitea_project_mirror.py` : création + affichage du miroir au premier `GET /gitea-workspace` (cookie ou URL seule), rendu du miroir par `/api/sidebar/workspace-tree`, et scan statique que `loadGiteaTree` cible bien `#sidebar-gitea-items`. ## v7.69.9 (2026-10-09) — /local-workspace : chemin du header + navigation dans les dossiers réparée ### Fixed - **/local-workspace : le chemin du header suit la navigation** — le header restait bloqué sur « Home / » quel que soit le dossier affiché. La route rend désormais `breadcrumb_items` (`_render_local_workspace`) au format des pages (`_nav_breadcrumb`) : **Home / / / / …**, chaque niveau cliquable (`/local-workspace?folder=`) et le dernier niveau courant ; au-delà de 4 niveaux, collapse « … » identique à l'éditeur de page. La navigation **sans rechargement** (clic sur un dossier dans la vue, breadcrumb interne, retour/avant) recalcule le chemin via le nouvel event `flowdeck:breadcrumb-changed`, écouté par `fdBreadcrumb` (`_header.html`) — les renommages continuent de suivre via `flowdeck:page-renamed`. - **/local-workspace : clic sur un dossier du sidebar = tous les composants affichés en même temps** — après une navigation partielle (`/local-workspace` → `/local-workspace?folder=`), Alpine échouait à remonter la page : `Alpine.data('wsInitData')` retournait **le même objet singleton** et Alpine redéfinit ses magics (`$nextTick`…) avec `Object.defineProperty` non-configurable → « Cannot redefine property: $nextTick », `initTree` abandonnait et **tout le contenu restait au state brut** (tree/list/détails/cartes, dropdowns, toast Undo, barre d'upload… visibles simultanément). La factory retourne désormais une enveloppe fraîche à chaque montage, qui délègue lecture/écriture au proxy réactif partagé (état et `wsPreview` inchangés). - **/local-workspace : le dossier de l'URL était perdu en navigation partielle** — `LW` (config `#lw-config`) n'était lu qu'à la première exécution du script (verrouillé par `__fdLocalWorkspaceScriptsLoaded`) : le 2e montage gardait le `folder_id`/`workspace_id` du premier chargement. La config est relue à chaque exécution et l'état transitoire (recherche, filtre, panneaux) est remis à zéro au montage. - **/local-workspace : breadcrumb interne toujours rendu** — il n'apparaît que si la page était chargée avec `?folder=` ; il est désormais présent dès la racine et suit la pile de dossiers. ### Tests - `e2e/regression_local_workspace_breadcrumb.spec.js` : parcours complet (sidebar → dossiers/sous-dossiers, clic dans la vue, breadcrumb interne, crumb workspace du header) avec vérification du chemin header et absence de composants peints à l'état brut. ## v7.69.8 (2026-10-09) — Library : ordre des colonnes visited/source ### Fixed - **Combinaison « Last visited time » + Source illisible** — les cellules de données rendaient Source avant Edited/Visited alors que l'en-tête l'attend en dernier : les valeurs tombaient sous les mauvais intitulés. L'ordre des cellules suit désormais l'en-tête (Name, Created by, Last edited by, Last edited time, Last visited time, Source), et les intitulés d'en-tête tronquent en ellipsis au lieu de déborder sur la colonne suivante (reproduit et mesuré au navigateur : alignement header/lignes vérifié colonne par colonne). ### Fixed - **Sidebar Favorites : icônes Workspace** — la liste utilisait des emoji bruts (voire rien) via `item.icon` ; `_build_item` renvoie désormais les noms SVG (`folder`/`file`/`edit`/`image`), idem pour les favoris board. - **/local-workspace : indicateurs de statut** — ★ favori (ambre), 👥 partagé (existant), 🌐 publié ; exposés par `_build_tree_children` (`favorited`, `published`, `is_shared`) et rendus dans l'arbre Alpine comme dans le rendu JS des enfants. - **Library « Created by » vide** — aucune source auteur n'était sélectionnée. Nouvelle provenance groupée : created by (première version, sinon propriétaire du workspace), last edited by (dernière version), last visited (table `recents`, tracée à chaque lecture de page). Nouvelles colonnes **Last edited by**, **Last edited time**, **Last visited time** ; le menu **Show columns** pilote chaque colonne, seul Page name est verrouillé. - **Colonne Source avec icône** — `file` (local), `link` (Gitea), `external-link` (GitHub), mêmes SVG que le reste de Flowdeck. - **Show columns réellement fonctionnel** — les règles de visibilité (spécificité 0,1,0) perdaient contre `.lib-table-header .th` et `.lib-row .row-cell` (0,2,0) : seule Last edited time s'affichait et masquait, les autres options semblaient mortes (reproduit et corrigé avec vérification en direct : les 5 toggles on/off + colonne Source avec icône + libellé, 0 erreur console). ### Tests - `tests/test_v696_sidebar_library_publish.py` (+3) : provenance et visite tracée, icônes favoris, flags de l'arbre local-workspace. ## v7.69.6 (2026-10-09) — Library AI Meeting Notes, Recents réels, Publish unifié ### Added - **Onglet Library « AI Meeting Notes »** (`GET /api/library/meetings`) : notes de réunion de l'utilisateur regroupées par date — Today, Yesterday, This week, Last week, This month, Older — avec icône calendrier, statut (Summary ready / Ready to record) en colonne Source, recherche et sélection réutilisées. Les notes sont exclues des onglets Recents / Workspace / Private (ainsi que de l'arbre Workspace et du navigateur local). ### Fixed - **Sidebar Recents vide** — elle ne listait que les dépôts Gitea. Elle liste désormais les fichiers réellement récents (`updated_at`, 8 derniers, dépôts conservés ensuite), avec les mêmes icônes SVG que le Workspace et les notes de réunion exclues (→ Meetings). - **Customize sidebar inutilisable** — l'overlay plein écran (z-index 998) recouvrait aussi le panneau d'options : chaque clic refermait le mode. Le panneau passe au-dessus (z-index 999). - **Publish qui ne « tenait » pas** — le share dialog écrivait `published`, les routes `/publish` écrivaient `is_published`, et chaque lecteur ne regardait qu'une colonne. Toutes les écritures maintiennent désormais les deux drapeaux (+ slug garanti), tous les lecteurs acceptent l'un ou l'autre : la page publiée reste publiée dans le dialogue, apparaît dans la section Published de la sidebar et dans l'onglet Published de la Library. ### Tests - `tests/test_v696_sidebar_library_publish.py` (6) : onglet meetings et exclusions, Recents sidebar, publish/unpublish des deux chemins, visibilité sidebar+library+dialogue, z-index du panneau customize. ## v7.69.5 (2026-10-09) — Meeting Notes : setup visible, retry qui ne perd plus rien ### Fixed - **Pas de « Setup check » visible avant l'enregistrement** — il était caché dans le menu ▾ du bouton Start, que personne n'ouvre. Le panneau idle affiche désormais une bande de vérification : état de la permission micro (via Permissions API), transcription live dispo ou repli manuel, mode onglet supporté ou non, et bouton **Test microphone** (4 s de niveau réel, qui fait aussi accorder l'autorisation avant le Start). - **Erreur 400 persistante au Stop/Retry après un collage de transcript** — `retry()`/`regenerate()` appelaient le serveur sans y verser d'abord le contenu local (segments + manuel collé) : le serveur restait vide et répondait 400 en boucle. Ils drainent désormais le tampon local (attente bornée) avant tout appel, et `retry()` détecte une session encore en vie côté serveur pour terminer par `/stop` plutôt que `/processing:retry`. - **Stop appelé alors que l'upload des segments échoue** — condamnait à un 400 (« transcript is empty ») malgré des segments en local. Le client n'appelle plus `/stop` dans ce cas : état `failed` avec message « envoi impossible, Retry », session serveur conservée. - **`/summarize` accepte `allow_empty`** (diagnostic `fragmentary` honnête au lieu de 400), comme `/stop` et `/processing:retry`. - **Cache-busting** : le bump 7.69.4 → 7.69.5 change `?v=` de tous les assets — les navigateurs ayant gardé l'ancien `meeting_block.js` (sans garde-fou ni diagnostic) reçoivent le nouveau code. C'était la cause la plus probable du « toujours l'erreur 400 ». - **Menu contextuel des sections Meetings et Workspace** : les notes de la section Meetings ont désormais le même clic droit que le Workspace (Open / Rename / Delete, via `showWsContext`) ; l'action **Open** est ajoutée au menu des deux sections (page → `/pages/{id}`, dossier → navigation dossier) ; rename/delete rafraîchissent la section Meetings. - **Sidebar sur /local-workspace** : la règle CSS globale `.ws-tree-item` de la page écrasait les items de la sidebar (espacements différents) — scopée à `.ws-tree` (arbre principal) ; et `refreshSidebarTree()` remonte désormais le fragment via `Alpine.initTree`, sans quoi l'arbre perdait menu contextuel, toggles et drag&drop après chaque refresh (d'où le menu « mort » sur cette page qui rafraîchit en continu). ### Tests - `tests/test_v711_meeting_notion.py` (+5) : `summarize` vide → 400 sans flag / diagnostic avec flag, retry intelligent quand la session serveur est encore live, `meeting_notes` + propriété, exclusion de l'arbre Workspace. ### Fixed - **Le bouton close de Settings devait être cliqué deux fois** — symptomatique depuis le menu utilisateur ; depuis `/workspaces` ça marchait. - Cause : `closeSettings()` faisait `history.back()` **puis** `window.location.reload()` après un délai **fixe de 100 ms**. Le retour est une navigation asynchrone : quand elle n'est pas terminée en 100 ms, le `reload()` retombe sur `/settings` et le panneau « se ferme puis revient ». La profondeur d'historique diffère selon le parcours, d'où le caractère intermittant et le fait que `/workspaces` échappait au symptôme. - → `location.replace()` vers la page d'origine : **une seule navigation déterministe**, ni `history.back()`, ni `popstate`, ni rechargement différé. Plus aucun chemin ne peut se terminer sans bouger. Effet secondaire souhaitable : « Retour » ne rouvre plus Settings. - La branche `window.opener` ne fait plus de `return` inconditionnel : `window.close()` échoue en silence quand l'onglet n'a pas été ouvert par du script, et l'ancien code abandonnait alors sans navigation de repli. ### Tests - `e2e/v7694_settings_close.spec.js` — **contrôle négatif fait** : le même spec a été exécuté contre deux builds. - ancien `closeSettings()` → le parcours menu-utilisateur **échoue** (`Received: "…/settings"`), `/workspaces` passe ; - correctif → **les deux passent**. Le test verrouille aussi que le panneau `position:fixed` n'est plus peint (`.settings-overlay` absent), qui est le symptôme exact rapporté. ### Correctifs de diagnostic - Le diagnostic posé en v7.69.3 (« Alpine ne lie pas le composant ») était **faux** — un artefact de mon banc d'essai `target="_blank"`, pas le parcours réel. Corrigé dans `ROADMAP.md`. ## v7.69.3 (2026-10-08) — La section Aide est positionnée comme Settings ### Fixed - **Aide s'affichait autrement que Settings** alors qu'ils partagent le même gabarit (`settings-overlay` / `settings-panel`) et le même CSS. `help.html` neutralisait son overlay **en style inline** : `position:static`, `background:none`, `backdrop-filter:none`, `padding:0`, et forçait un panneau `100% / max-width:1050px / height:calc(100vh - 120px)`. Résultat : Aide était un bloc posé dans la page, Settings une modale centrée floutant le fond. - → les surcharges inline sont retirées ; la géométrie vient entièrement de `settings.css`, commun aux deux pages. Vérifié en e2e : **mêmes valeurs mesurées** des deux côtés (overlay `fixed` avec fond, panneau 1050×765 centré à `top:68px`, bouton close à 13/13 px du coin du panneau). ### Tests - `tests/test_v7693_help_positioning.py` (5) verrouillent l'absence de surcharge inline et l'identité de balise avec Settings ; `e2e/v7693_help_positioning.spec.js` compare les géométries calculées. > **Corrigé en v7.69.4** : le double-clic sur le bouton close de Settings. > Le diagnostic porté ici (« Alpine ne lie pas le composant ») était **faux**, > voir `CHANGELOG.md § v7.69.4`. ## v7.69.2 (2026-10-08) — Le favicon est enfin le vrai logo ### Fixed - **Le favicon était un placeholder** : un carré bleu `#2383E2` avec les lettres « FD » écrites en `` SVG — génériques, sans lien avec la marque, et rendus avec la police du visiteur. - → `static/favicon.ico` généré depuis `assets/logo.jpg` : 16/32/48 px dans un seul fichier (un `` pour tous les navigateurs), 4,1 Ko. **Pourquoi un fond sombre** : le logo est blanc à ~82 % (mesuré : 50,7 % de blanc pur + 32 % de blancs proches). Posé nu, il disposerait sur un onglet clair — cas nominal de Firefox et Safari en thème clair. Il est donc posé sur un carré `#191919` arrondi, **le même fond que les icônes PWA** générées en v7.69.0, ce qui garde une identité cohérente. Contraste mesuré à 16 px : σ = 106 (contre σ = 49 pour le logo seul — le fond sombre double la lisibilité). Mise à jour des **5 templates** qui référençaient l'ancien SVG (`base`, `import`, `landing`, `public_page`, `welcome`) + la liste de pré-cache du service worker. `static/favicon.svg` **supprimé** : plus aucune référence. ### Tests - `test_favicon_is_the_real_logo` (tailles ICO, absence du placeholder) + `test_every_template_points_at_the_real_favicon` (5 templates + `sw.js`). ## v7.69.1 (2026-10-08) — `og:image` en URL absolue ### Fixed - **`og:image` / `twitter:image` étaient des URL relatives** — les crawlers (LinkedIn, Slack, Discord, iMessage…) ignorent une URL relative : la carte de partage était vide. → global Jinja `app_base_url()` construit à partir de `settings.app_base_url`, **la même source que les liens de partage de pages** (`collaboration.py`) : si elle est mal renseignée, ces liens l'étaient déjà. Lecture paresseuse, comme `_plugin_enabled`, pour éviter l'import circulaire. ## v7.69.0 (2026-10-08) — La sélection multi-blocs fonctionne enfin (+ identité visuelle) ### Fixed - **Ctrl+C / Ctrl+X multi-blocs ne faisaient RIEN** — cause racine : le navigateur ne sait pas sérialiser une sélection qui traverse deux `contenteditable` (chaque bloc en a un). Résultat : `getSelection().toString()` renvoie `""` et Chromium **n'émet aucun événement `copy`** — nos handlers `ct.addEventListener('copy')` n'étaient donc jamais atteints, et le raccourci mourait en silence. → le raccourci est maintenant intercepté au `keydown` (capture sur `document`, garde `closest('#_blocksCt')` pour ne pas voler Ctrl+C ailleurs) : on écrit le markdown dans le presse-papier nous-mêmes. La sélection mono-bloc reste native. - `Ctrl+X` supprime les blocs traversés en conservant la tête du premier et la queue du dernier. - **Glisser la souris d'un bloc à l'autre sélectionnait un seul bloc** — cause racine : la séquence complète est `mouseup` (on reconstruit la plage) → `click` → `focusin` → **le navigateur efface la sélection**. La plage était donc reconstruite puis détruite dans la foulée. → la plage reconstruite est mise en attente (`_mdSelRange`) et restaurée au tour de boucle suivant, si le focus l'a effectivement effacée. Un nouveau geste (mousedown) remet l'attente à zéro, donc un clic pour placer le curseur reste normal. - **Le clic droit effaçait le surlignage** — la recherche du menu prenait le focus 10 ms après son ouverture, ce qui détruisait la sélection. → on ne vole plus le focus quand un texte est sélectionné. (Les actions Clipboard utilisaient déjà la sélection figée à l'ouverture du menu : `d._clipSt`.) - Le `preventDefault` du clic droit est bien sur `mousedown` (sur `contextmenu` il arrive trop tard — le curseur a déjà déplacé la sélection). ### Added - **Identité visuelle** : la bannière et le logo du projet sont intégrés. - assets optimisés : `static/img/banner.webp` **22,5 Ko** (l'original fait 1,6 Mo) + `banner.jpg` de secours, `logo.webp` **5,3 Ko** + `logo-512.jpg` ; originaux conservés dans `assets/` pour régénération. - icônes PWA régénérées à partir du vrai logo (72 → 512 px, marge maskable sur 192/512) + `apple-touch-icon`. - landing : logo dans la barre de navigation, bannière en `` (WebP + JPEG) avec dimensions déclarées pour éviter tout décalage de mise en page ; `og:image`, `twitter:card` et `meta description` ajoutés au ``. ### Tests - `tests/test_v769_selection_persistence.py` (19 tests statiques) + `e2e/v769_selection_persistence.spec.js` (5 tests, **vrais gestes** : glisser de souris, `Ctrl+C`/`Ctrl+X` au clavier, clic droit). - `e2e/v768_columns_clipboard.spec.js` : les gestes simulés (`execCommand`, plage fabriquée en `evaluate`) sont remplacés par de vrais gestes. ## v7.68.0 (2026-10-08) — Colonnes rendues, tables IA, copier/couper/coller multi-blocs ### Fixed - **Colonnes / équation / TOC n'étaient JAMAIS rendus** : `renderBlock()` n'appelait pas `renderColumnsBlock()` / `renderMathBlock()` / `renderTOC()` (3 renderers morts depuis l'extraction v7.22) → un bloc colonnes — créé par le slash « Column list », par **Turn into → 2/3/4/5 colonnes** — tombait dans le rendu générique : un paragraphe vide. Dispatch ajouté. - la **× de suppression de colonne** était en `opacity:0` **sans règle de survol** (invisible mais cliquable) → visible au survol du bloc colonne. - **Tables rendues par l'IA** : `_tblIsData`/`_tblIsSep` exigeaient un `|` de tête → le GFM sans pipe (`A | B` / `--- | ---`, ce que produit souvent le modèle) devenait des paragraphes. Le pipe de tête est **optionnel** en GFM ; garde conservée : une ligne de séparation doit faire ≥2 cellules (un « texte » suivi d'un « --- » reste un séparateur de section). `md2b` et `paste2b` partagent les helpers → alignés d'office. - **Coller un SEUL bloc structuré** (table, code, titre) retombait en texte brut (`if(parsed.length<=1) return fallback()`) → seul un bloc paragraphe (une ligne) garde le comportement inline. - **« Copy link to block »** du menu de bloc : `data-bm-act="link"` alors que le handler attend `'copy-link'` → l'action ne faisait rien. ### Added - **Copier / couper une sélection de texte MULTI-BLOCS** : - `_clipState()` (sélection vivante traversant ≥2 blocs) + `_clipMarkdown()` (début partiel → blocs entiers sérialisés par `blocksToMarkdown` → fin partielle) ; `clipCopy()` / `clipCut()` (la coupure garde la tête du 1ᵉʳ bloc et la queue du dernier, le milieu part) ; - listeners **`copy` / `cut`** sur `#_blocksCt` : Ctrl+C / Ctrl+X passent par les événements natifs du navigateur ; dans un seul bloc, on laisse le navigateur faire (copie native) ; - groupe **Clipboard** (`Cut Ctrl+X` / `Copy Ctrl+C` / `Paste Ctrl+V`) en tête du menu du clic droit, avec la sélection **figée à l'ouverture** du menu (la recherche prend le focus et détruit la sélection) ; - clic droit **sur un bloc sélectionné** → la sélection multi-blocs (handle + Maj-clic) est conservée et le Clipboard agit dessus (`copySelectedBlocks`) ; - `pasteBlocks()` colle après le dernier bloc quand aucun bloc n'est focalisé. ### Tests - `tests/test_v768_columns_clipboard.py` (4). - **e2e** `e2e/v768_columns_clipboard.spec.js` (4) : slash « Column list » + « Add column » + **Turn into → 2 colonnes** (bloc `.block-column` réellement rendu), table IA **sans pipe de tête** (vrai `.ftable-editor` + garde « texte »/« --- »), copier→`readText` → couper (blocs réduits à la tête + queue) → coller (blocs revenus), clic droit → groupe Clipboard → Copy. - le raccourci clavier n'est pas déclenché en headless Chromium (pas de presse-papier OS) : le test passe par `document.execCommand('copy'/'cut')`, qui émet les **mêmes** événements que Ctrl+C/X. - `pytest tests/ -q -n auto` → **1394 passed, 0 failed**, `ruff check app tests` → OK. ## v7.67.0 (2026-10-08) — Réactions dans le tiroir Comments (+ suppression) et refonte du menu de sélection ### Added - **Les réactions vivent dans le tiroir « Comments »** : section **Reactions** au-dessus des fils — émoji, compte (si >1) et **texte ancré** lu dans le DOM (`reactionList()` / `_anchorText()`, offsets source hors pastilles). L'état vide « No comments yet » ne s'affiche plus s'il reste des réactions. - **Suppression d'une réaction — deux entrées, un seul chemin** : - **✕** sur la ligne de la section Reactions (visible uniquement sur **mes** réactions) ; - au **survol de la pastille**, l'émoji que j'ai posé devient une **croix** (`::after`, aucun nœud ajouté → offsets source inchangés), un clic retire. - les deux appellent `removeReaction()` → `POST /reactions` (toggle) → `loadReactions()` + repeindre les ancres. ### Changed - **Refonte du menu de sélection façon capture** : - ligne 2 `[A] B I U T Tx` (le barré est conservé — absent de la maquette, décision volontaire pour ne pas perdre la fonction) ; - ligne 3 `🔗 S √x …` (la réaction en est sortie) ; - **ligne 4 = une rangée scannée** : `💬 Comment` (libellé) à gauche, `🙂+` réaction à droite (`.ft-row-split`) ; - entête **Skills** avec `≡` à droite (`.ft-skills-head::after`), liste **scrollable** (`.ft-skills`, `max-height` + `overflow-y`) ; - **✨ Ask AI retiré** de la toolbar : doublon exact de « Edit with AI » du pied (même `fmtAskAI()`). ### Fixed - **Flake de gate corrigé (racine)** : `test_v55.py::TestOGParser:: test_fetch_og_metadata_success` dépendait d'un **vrai** `socket.getaddrinfo ("example.com")` via le garde SSRF `_is_public_host` ; sous `pytest -n auto` un `socket.timeout` (non couvert par `except socket.gaierror`) remontait dans le `except Exception` de `fetch_og_metadata` → repli silencieux (`title='example.com'`) → AssertionError, 3 fois sur 5 runs. - `_is_public_host` catch désormais **`OSError`** : le garde renvoie un booléen, il ne lève jamais (les autres appelants gagnent aussi) ; - fixture `stub_dns` sur `TestOGParser` (même motif que `test_agent_web_tools.stub_dns`) : zéro DNS dans les tests OG. ### Tests - `tests/test_v767_reactions_drawer.py` (3) ; `tests/test_v762_selection_toolbar.py` (ligne 4 + styles) ; `tests/test_v764_comments_reactions.py` (position de `fmtReact`). - **e2e** : `v764_comments_reactions` étendu — section Reactions dans le tiroir (texte ancré), **suppression A** (✕ du tiroir → pastille disparue), **suppression B** (croix `::after` visible seulement au survol, puis clic) ; `v762_selection_toolbar` — `Comment` + `ft-row-split .ft-react`, `ft-skills-head` = « Skills », plus de bouton ✨. - `pytest tests/ -q -n auto` → **1390 passed, 0 failed** (flake corrigé), `ruff check app tests` → OK. ## v7.66.0 (2026-10-08) — Escape du menu slash retire le « / » + bouton « commenter » à droite du bloc ### Added - **Bouton « commenter ce bloc » à droite du bloc** (`.block-comment-btn`, gouttière 64px, visible au survol/focus comme le handle) : icône **bulle de conversation avec un +**, `onclick="E.commentBlock(idx)"` → tiroir de commentaires ancré sur le bloc (ou sur la sélection vivante s'il y en a une). Icône = tracé `message-square` (Feather) + `+` centré. Masqué sur mobile (2 breakpoints, comme le handle) et sur page verrouillée. - remplace l'ancien `.block-actions` de `renderBlock` : rendu en `opacity:0` **sans règle de survol** (jamais de classe `.visible`) → un `+` invisible qui capait les clics dans la gouttière droite. ### Fixed - **Escape ferme le menu slash ET retire le « / »** : nouvelle `SM.escClose()` appelée par les **deux** chemins de fermeture (focus dans le bloc → `onKd`, focus dans l'entrée du menu → listener `document`). Ne touche au caractère que si c'est le `/` d'ouverture, remet le caret et sauvegarde (`sync` + `autoSave`). - **`fmtComment()` restaure la sélection de la toolbar** (`_fmtRestore()`) avant d'ancrer : `commentBlock()` ne se replie plus sur `this._sel` (plage potentiellement périmée reprise par le bouton de droite / le menu de bloc). ### Tests - **e2e** `e2e/v766_slash_esc_comment_btn.spec.js` : « / » → menu visible → Escape → menu fermé et bloc vide ; bouton présent, `opacity` 0 → 1 au survol, **à droite** du wrapper (boîte mesurée), clic → tiroir → ancre = le bloc entier. - `tests/test_v766_slash_esc_comment_btn.py` (3) ; `pytest tests/ -q -n auto` → **1386 passed**, `ruff check app tests` → OK. ## v7.65.0 (2026-10-08) — Comment ancré sur la SÉLECTION de texte (pas le bloc) + retrait du bouton bleu « 💬 Comment » ### Changed - **`commentBlock()` ancre sur la sélection de texte** : la plage vivante dans le bloc est utilisée d'abord (plage de la toolbar de sélection en repli, le clic sur le bouton ayant pu la faire disparaître du DOM), sinon tout le bloc. Les offsets passent par `_srcOffset()` → `anchor_start/anchor_end` = offsets dans le texte source, donc l'ancre jaune ne couvre que le texte sélectionné (`_paintAnchors` / réactions inchangés). - **Retiré le bouton bleu flottant « 💬 Comment »** (`#_commentSelBtn`) qui apparaissait au-dessus de la sélection : le déclencheur est le bouton 💬 de la toolbar de sélection, le menu de bloc (💬 Comment, Ctrl+⇧+M) et le raccourci. `showCommentBtn()`/`hideCommentBtn()` et le listener `mousedown` associés sont supprimés. - Fermer le tiroir (`toggleComments`) remet `_commentSel` à zéro : un brouillon abandonné ne peut plus attacher le commentaire suivant à une ancienne plage. ### Tests - **e2e** `e2e/v764_comments_reactions.spec.js` : sélection d'une **sous-chaîne** (`reaction a un`) → toolbar 💬 → tiroir → envoi ; assert `#_commentSelBtn` absent et `.fd-anchor[data-cid]` = **uniquement** la plage sélectionnée (avant **et** après reload). - `pytest tests/ -q -n auto` → **1383 passed**, `ruff check app tests` → OK. ## v7.64.1 (2026-10-08) — Fix : Delete sur un bloc vide supprime le bloc ### Fixed - **La touche Delete ne supprimait pas le bloc vide** : seul `Backspace` avait la garde « bloc sans texte → supprimer le bloc ». La branche couvre désormais les **deux touches** (`onKd`, une seule garde partagée) — Delete vide le bloc puis le supprime au coup suivant, comme Backspace ; le dernier bloc de la page n'est jamais supprimé (l'éditeur garde un paragraphe). - Le vide est jugé avec **`gtTok(el)`** (le texte lu à la sauvegarde) et non `textContent` : une pastille de réaction seule dans le bloc ne l'empêche plus d'être supprimé. ### Tests - **e2e** `e2e/v7641_delete_key.spec.js` : texte → Delete (bloc vidé, pas supprimé) → Delete (bloc supprimé, le précédent reste) → dernier bloc vide jamais supprimé. ## v7.64.0 (2026-10-08) — Commentaires façon Notion : ancres jaunes, tiroir refondu, réactions sur texte ### Added - **Réactions emoji sur un texte sélectionné** : - bouton **🙂+** dans la 3ᵉ ligne de la toolbar de sélection, infobulle « React to selected text » → ouvre le **sélecteur d'émoji existant** en mode réaction : en-tête = texte sélectionné (+ ✋), onglets masqués (emoji only), ligne **Recent** au-dessus de la grille **People**, Filter… et barre de catégories inchangées (dont `+` émoji custom). - **stockage** : migration 37 (`text_reactions`, une ligne par plage × émoji × utilisateur, `UNIQUE` qui rend le POST idempotent) + endpoints `GET/POST /api/pages/{id}/reactions` (GET groupé par plage : `emoji/count/mine`, POST = toggle avec validation de plage non vide). - **affichage** : la plage est peinte en jaune (`.fd-anchor-react`) avec une **pastille** juste après (`😀` + compte si >1) ; clic sur la pastille → réouvre le sélecteur pour cette plage (ajout/suppression). - les pastilles ne sont **pas** du contenu : `gtTok`/`gtMd` les ignorent et les offsets `_srcOffset/_srcNodeLen` les excluent → `sync()` conserve le texte d'origine (vérifié dans le e2e) et les ancres ne décalent jamais. - **Ancres de commentaires visibles** — `_paintAnchors()` (appelé après chaque `render()`, après `loadComments` et `loadReactions`) entoure la plage commentée d'un **jaune** (`.fd-anchor`, gris si résolu) ; **clic sur l'ancre → le tiroir s'ouvre et centre le fil** (flash de surlignage). - **Tiroir de commentaires refondu** (image 1) : fil en carte arrondie avec **avatar coloré + nom + horodatage + ✓ (resolve/reopen) + ⋯** (menu Copy link / Delete), étiquette « 💬 on selected text », et champ de saisie **« Add a comment... »** avec **@** (insère la mention) et **bouton circulaire ↑** (envoi, `Entrée` aussi) — remplace le bouton « Comment ». - **Liens de commentaire** : `Copy link` copie `…#cmt-` ; au chargement d'une page avec ce hash, le fil correspondant s'ouvre et se centre. - **Chargement initial** : `loadComments()` + `loadReactions()` à l'ouverture de la page → les ancres/pastilles sont peintes dès le premier rendu (et `commentCount` du tiroir est enfin renseigné). ### Fixed - **`resolveComment` ne pouvait pas rouvrir** : il envoyait toujours `resolved:true` → accepte désormais l'objet du fil et bascule (`resolveComment(c)`), le bouton ✓ du fil est donc réellement un toggle. - **`showCommentBtn` ignorait les sélections à ancre « élément »** (double-clic, sélection programmatique) → `closest('[data-bid]')` partait du wrapper et le bouton 💬 n'apparaissait pas → gère les 2 types de nœud (même fix que `showFmt` en v7.62). - **`gtTok` chemin rapide (`innerText`) incluait les pastilles de réaction** → sélecteur enrichi (`.fd-react-chip`) pour basculer sur le marcheur qui les exclut : le texte sauvé reste identique. ### Tests - `tests/test_v764_comments_reactions.py` (8 tests) : table + migration 37, endpoints enregistrés, **toggle API réel** (added → removed → added, grouping, 400 sur plage vide), markup toolbar/tiroir, peinture des ancres, mode réaction du sélecteur. - **e2e Playwright** `e2e/v764_comments_reactions.spec.js` : bouton 🙂+ avec la bonne infobulle → sélecteur (en-tête = texte, Filter…, onglets masqués) → pastille + plage jaune **avec contenu inchangé après `sync()`** → 💬 → tiroir (avatar, nom, ✓, ⋯, Copy link) → ancre jaune cliquable → ✓ résout → reload : ancres et pastilles rechargées depuis la base ; **3/3 specs e2e verts** (`--workers=1`). - **Suite complète** : `pytest tests/ -q -n auto` → **1383 passed / 0 failed**, `ruff check app tests` → All checks passed. ## v7.63.0 (2026-10-08) — Menus mobiles façon Notion (barre au « / » + Insert block plein écran) ### Added - **Barre horizontale mobile** — sur un viewport ≤ 768 px, taper `/` sur une ligne vide affiche une **barre flottante défilante sous le curseur** (au lieu de la popup desktop), 15 boutons dans l'ordre Notion : `+▾`, `Turn into▾`, `@`, 💬, 🖼, 🗑, `←`, `→`, `↑`, `↓`, `↩`, `↪`, `Color▾`, `More▾`, `⌨` : - `+▾` → **feuille plein écran « Insert block »** : titre centré, bouton bleu « Cancel », liste verticale par groupes (Basic blocks / AI writing / Media / Data / Synced / Actions) avec icône, nom et **raccourci markdown** à droite (`#`, `##`, `-`, `1.`, `[]`, `>`, `"`, `---`…) — items partagés avec la popup desktop (`SM._slashItemHtml`), choix → `applySlash` → tout se referme. - `Turn into▾` / `Color▾` → feuilles réutilisant **les mêmes générateurs d'items** que les sous-menus du menu de bloc (`_blockTurnItems` / `_blockColorItems` extraits de `_blockTurnMenuOpen` / `_blockColorMenuOpen`). - `More▾` → menu contextuel de bloc existant (recherche + 10 actions + pied). - `@` insère `@` et ouvre le sélecteur de mention ; 💬 commentaire ; 🖼 insère un bloc image ; 🗑 supprime ; `⌨` masque le clavier ; `↩ ↪` annuler/rétablir. - **Déplacement de bloc `moveBlock(bid, dir)`** — `↑↓` réordonne (y compris dans un parent toggle/columns), `→` indente dans le frère précédent (toggle/columns), `←` ressort à la racine ; garde-fous avec toast (déjà en haut/bas, déjà à la racine, pas de cible). - **Fermeture automatique** de la barre quand le `/` n'est plus au début du bloc ; taps dans la barre/la feuille n'essaient plus de fermer le menu (exclusion `MTB.contains` sur le handler mousedown hors-zone). - **Raccourcis slash alignés sur les préfixes markdown** (description Notion) : `-` (puces), `[]` (à faire), `>` (toggle), `---` (séparateur) — partagés avec la popup desktop ; groupe `SYNCED` libellé « Synced ». ### Tests - `tests/test_v763_mobile_menus.py` : ordre des 15 boutons, câblage de chaque action, structure de la feuille (titre/Cancel/groupes), branchement mobile, styles — et **`moveBlock` exécuté pour de vrai sous node** (ordre, indent, outdent, garde-fous). - **e2e Playwright mobile** `e2e/v763_mobile_menus.spec.js` (viewport 390×844, `hasTouch`) : barre visible au `/` avec les 15 boutons dans l'ordre + `overflow-x:auto` et popup desktop masquée, « Insert block » (groupes, raccourci `#` de Heading 1) → le bloc devient `heading_1`, « Turn into » (coche du type courant), « Color » (3 groupes), « More » (menu de bloc + pied), `↑` qui réordonne réellement les blocs. - `tests/test_v761_block_menu.py` adapté aux extractions `_blockTurnItems` / `_blockColorItems`. ## v7.62.0 (2026-10-08) — Menu contextuel au highlight de texte (toolbar façon Notion) ### Added - **Toolbar de sélection** — dès qu'un texte est surligné dans l'éditeur, un menu flottant apparaît **sous la sélection** (au-dessus si manque de place), centré et clampé au viewport, en 4 lignes comme dans Notion : 1. **Style de texte** — `T` + libellé **dynamique** du type de bloc (Normal Text, Heading 1… ; le type courant) + `›` → ouvre le sous-menu **Turn into** existant (coche sur le type actuel, fermeture au survol sortant). 2. **A** (sous-menu Color : texte/fond + dernière couleur), **B** gras, *I* italique, **U** souligné, **T** barré, **Tx** code inline. 3. 🔗 lien, **S** surlignage (``), `` bloc code, **√x** équation inline (prompt LaTeX → KaTeX), **…** → menu contextuel de bloc (recherche + 10 actions + pied « Last edited by … / N words, M characters »). 4. 💬 Comment (`Ctrl+⇧+M`), ✨ Ask AI (`Ctrl+J`). puis la section grise **Skills** (Improve writing, Proofread, Explain, Reformat → `runSkill` sur le bloc) et le pied **Edit with AI** `Alt+⇧+E`. - **Info-bulle claire à droite** des éléments du menu — extension du système de tooltip existant (`app.js`) : `data-tooltip-html` (ex. en italique + description pour la ligne 1) + classe `.tooltip-light`, position à droite de l'élément (débord → à gauche), sous l'élément hors toolbar. - **Persistance du formatage inline** : nouveau sérialiseur **`gtMd()`** (DOM formaté → source markdown) appelé par `sync()` (blocs + enfants toggle/columns) — le formatage posé ressort en `**gras**`, `*ital*`, `__souligné__`, `~~barré~~`, `` `code` ``, `==surlignage==`, `[texte](url)` ; les chips wiki gardent leur token. `mdEsc()` rend en plus `__…__` (``), `==…==` (``) et `$$…$$` (équation inline KaTeX, `data-tex` conservé pour l'aller-retour). - **Fermeture propre** : clic hors sélection → le menu se referme ; clic **dans** la toolbar → sélection et menu intacts (`@mousedown.prevent` + garde `mouseup`). ### Fixed - **`askAI(idx)` était écrasé par `askAI()`** (même clé dans l'objet éditeur, définie plus bas) : « Ask AI » du menu de bloc et `Ctrl+J` ouvraient l'IA de **page** au lieu du composeur du bloc → renommé **`askAIBlock(idx)`** (appels : menu de bloc, `Ctrl+J`, `Alt+⇧+E`). - **`ed.__x` introuvable sous Alpine v3** — le data de `.page-editor-wrapper` était lu via `ed.__x` (API Alpine v2, plus exposée depuis le bundle CSP) : `showFmt`, `hideCommentBtn` et les handlers de mentions ne s'exécutaient **jamais** → la toolbar de sélection n'apparaissait pas. Accès unifié via `_xed()` (`_x_dataStack[0]`, repli `__x`), 4 sites éditeur. - **Ancre de sélection** — `closest('[data-bid]')` partait du parent de l'ancre : quand l'ancre est un nœud **élément** (sélection programmée, double-clic), le bloc n'était jamais trouvé → `showFmt` pas appelé. Gestion des 2 types de nœud. - **Barre d'espace = exception** — `checkMd()` recevait l'**id** du bloc au lieu de l'index : chaque espace levait `Cannot read properties of undefined (reading 'id')` et les raccourcis markdown (`# `, `- `) ne se déclenchaient jamais. ### Tests - `tests/test_v762_selection_toolbar.py` : ordre des 4 lignes + Skills + pied, câblage réel des actions, persistance `gtMd`/`mdEsc` (**roundtrip réel sous node**), placement/fermeture, styles ; `tests/test_v761_block_menu.py` adapté (`askAIBlock`). - **e2e Playwright** `e2e/v762_selection_toolbar.spec.js` (instance 8081) : structure des 4 lignes, sous-menu Turn into (coche du type courant), menu « … » (recherche + pied), géométrie (sous la sélection, dans le viewport), **persistance du gras et du surlignage après reload**, fermeture hors sélection → 1 passed (captures dans `e2e/shots/` — répertoire gitignoré). - **Suite complète** : `pytest tests/ -q -n auto` → **1368 passed / 0 failed**, `ruff check app tests` → All checks passed. ## v7.61.0 (2026-10-08) — Menu contextuel de bloc refondu façon Notion ### Added - **Panneau de gauche** : barre de recherche « Search actions... » (filtre en direct), titre de section « Text », puis les 10 actions dans l'ordre Notion : Turn into ›, Color ›, Copy link to block (`Alt+⇧+L`), Duplicate (`Ctrl+D`), Move to (`Ctrl+⇧+P`), Delete (`Del`), Comment (`Ctrl+⇧+M`), Suggest edits (`Ctrl+⇧+Alt+X`), Ask AI (`Ctrl+J`), Skills › — avec les raccourcis affichés, et le pied de page « Last edited by … / Today at H:MM / N words, M characters » (auteur lu dans le compte, heure = `updatedAt` du bloc page, taille calculée sur le contenu du bloc). - **Sous-menu « Turn into »** (survol du panneau de gauche → panneau de droite, fermeture différée) : Text ✓ (type actuel coché), Heading 1-4, Bulleted list, Numbered list, To-do list, Toggle list, Code, Quote, Callout, Block equation, Synced block, et **2/3/4/5 columns** avec info-bulle « Create N columns of blocks » ; `turnInto(idx, type, cols)` crée désormais N colonnes et ouvre le sélecteur de source pour les blocs synchronisés. - **Sous-menu « Color »** : sections **Last used** (raccourci `Ctrl+⇧+H`), **Text color** (Default, Gray, Brown, Orange, Yellow ✓, Green, Blue, Purple, Pink, Red — glyphe « A » à la couleur) et **Background color** (même palette avec carré de couleur) ; la couleur utilisée est cochée, la dernière est mémorisée (`localStorage fdLastBlockColor`). - **Sous-menu « Skills »** : liste réelle des compétences de l'espace (`GET /api/agent/skills`) → applique le `prompt_template` au contenu du bloc via l'IA et insère le résultat dessous. - **Raccourcis** — handler clavier dédié : `Ctrl+J` (Ask AI), `Ctrl+⇧+H` (dernière couleur), `Ctrl+⇧+M` (commenter le bloc : sélection du contenu + tiroir de commentaires), `Ctrl+⇧+Alt+X` (suggérer une édition → résultat inséré dessous), `Alt+⇧+L` (copier le lien), `Ctrl+⇧+P` (déplacer vers…), `Ctrl+D` (dupliquer — déjà géré par la saisie, pas de double), `Del` (**seulement avec le menu ouvert**, jamais pendant la saisie), `Échap` (fermer). - **Actions Suggest edits / Ask AI / Comment / Skills** réelles côté éditeur (`suggestEdit`, `askAI`, `commentBlock`, `runSkill`) — Ask AI ouvre le composeur inline sans effacer le contenu du bloc. - **Styles** : `.bm-group`, `.bm-foot`, `.bm-check`, `.bm-current`, `.bm-dot`, sous-menu `60vh` défilable, recherche focalisée à l'ouverture. - **Tests** — `tests/test_v761_block_menu.py` : **9 tests** (structure et ordre du panneau, dispatch des 10 actions, 3 sous-menus, survol/fermeture, les 9 raccourcis, comportement node : `turnInto(…, 'columns', 3)` → 3 colonnes, styles) + `test_v7593` adapté à la nouvelle signature. ### Removed - Entrées **Copy / Cut / Paste blocks** du menu (absentes de la spécification Notion) — les raccourcis `Ctrl+C/X/V` sur blocs sélectionnés sont conservés. - Pastilles de couleurs inline : remplacées par le sous-menu Color nommé (palette portée à 10 couleurs alignées Notion : +Brown, Pink réel). ## v7.60.0 (2026-10-07) — Éditeur : bouton + à gauche du handle de bloc ### Added - **Bouton `+` à gauche du handle** (les 6 points qui ouvrent le menu ⋮⋮ / servent au drag) : clique → **nouveau bloc inséré sous le bloc courant**, via le mécanisme déjà en place (`E.addAfter(idx)` → `addAt(idx+1)`). - Positionné dans la gouttière (`left:-64px`, gouttière `.blocks-container` de 64px, `-56px` quand elle passe à 32px), apparaît au survol/focus du bloc comme le handle, masqué avec lui sur écran étroit (≤768px et le second breakpoint — aucun débordement horizontal ajouté au conteneur. - `aria-label` + `title` « Add block below » (accessibilité). - **Tests** — `tests/test_v760_block_insert_btn.py` : **2 tests** (ordre dans le wrapper, câblage `E.addAfter`, positions/masquages CSS). ## v7.59.3 (2026-10-07) — Correction : « Turn into » du menu contextuel ### Fixed - **« Turn into… » ne faisait rien** — la garde validait le type avec `_BLOCK_TURN_TYPES.indexOf(type)` alors que ce tableau contient des **objets** `{id, name}` : `indexOf` d'une chaîne renvoyait toujours `-1` → `turnInto()` retournait immédiatement après l'ouverture du sous-menu. Correction : `_BLOCK_TURN_TYPES.some(t=>t.id===type)`. - **Texte conservé** — convertir un bloc en **To-do** ou **Toggle** vidait le contenu (ces deux types étaient exclus de la conservation par erreur) ; seul un type sans champ texte (`image`, `embed`, `divider`, `button`, `columns`, …) le vide désormais, comme dans `Notion`. - **Tests** — `tests/test_v7593_turn_into.py` : **4 tests**, dont 1 comportemental **node** qui exécute la vraie `turnInto()` extraite de la source : conversion effective du type, texte conservé (To-do = étiquette + case décochée), contenu vidé pour un type sans champ texte. ## v7.59.2 (2026-10-07) — Corrections : aide, « Insert below », listes numérotées ### Fixed - **Aide** — bouton **✕** en haut à droite du panneau (classe partagée `settings-close` : 32 px desktop / **44 px mobile**) + méthode `close()` dans `help.js` (retour à la page précédente, sinon `/workspaces`) ; `.settings-panel` passé en `position:relative` pour ancrer les boutons au panneau (hamburger + ✕) plutôt qu'à la page. - **Éditeur — « Insert below » (/Write with AI)** : on insère le **markdown source** conservé à la génération (`AIC._md`) au lieu du texte extrait du HTML rendu (`_resultText()` ne reste qu'en repli pour les messages d'erreur) — titres, listes, gras et numéros survivent à l'insertion. - **Listes numérotées** — elles n'avaient **aucun numéro au rendu** : le CSS ne déclarait que le retrait, jamais le marqueur. Le rang des blocs `numbered_list` contigus est désormais calculé au rendu (`data-num`) et affiché via `content: attr(data-num) ". "` → **`/numbered list` et l'insertion depuis l'assistant affichent enfin 1, 2, 3**. - **`md2b`** (markdown → blocs, chemin agent) : les cases à cocher `- [ ]` / `- [x]` sont testées **avant** la branche des puces (une ligne `- [ ]` était avalée par cette branche) — aligné sur l'ordre déjà correct de `paste2b` ; le séparateur `1)` est accepté (CommonMark). - **Menu slash** — une requête tapée **dans le bloc** (`/numbered list`, focus qui n'a pas rejoint l'entrée du menu) est répercutée dans le filtre au lieu d'être ignorée. - **Tests** — `tests/test_v7592_ui_fixes.py` : **5 tests**, dont 1 test comportemental **node** qui exécute le vrai `md2b` extrait de la source. - `ruff` : ordre d'imports de `tests/test_agent.py` (I001 préexistant au pull). ## v7.59.1 (2026-10-07) — Fix fournisseur Mistral (clé valide, 403 tier_not_allowed) ### Fixed - **Mistral « Test connection » échouait malgré une clé valide** — cause racine : `mistral-large-latest` (modèle par défaut + tête de liste) n'est pas servi par les plans d'abonnement basiques → `403 tier_not_allowed` (code 1910). La clé elle-même fonctionne (`/v1/models` 200, chat 200 sur small/medium/nemo/codestral — vérifié en direct). - `PROVIDERS["mistral"]` default → `mistral-small-latest` (tous plans). - `PROVIDER_MODELS["mistral"]` réordonnée : modèles tous-plans d'abord (le test de connexion sélectionne le premier candidat). - `mistral` ajouté à `_CHAT_VALIDATED_PROVIDERS` : le fetch des modèles sonde chat/completions et exclut les modèles hors plan (46 listés → 25 réellement utilisables avec la clé du compte). - **Migration 36** : `default_model` des lignes mistral pointant sur un modèle bloqué réinitialisé (→ nouveau défaut), `llm_config.model` idem. ### Tests - `tests/test_agent.py::test_mistral_defaults_are_tier_safe` · suite agent OK · test de connexion live sur l'instance déployée : `{"ok":true, "model":"mistral-small-latest","reply":"PONG","verified":true}`. ## v7.59.0 (2026-10-07) — Mobile : drawer réglable, side-nav settings, aide refondue ### Fixed - **Scroll souris du drawer sidebar mobile** — `display:block` du breakpoint ≤768px cassait le `flex:1` de `.sidebar-scroll` (hauteur = contenu, jamais de zone scrollable). Remis en `display:flex;flex-direction:column`. - **Rate limiter du routeur de test** — fenêtre remise à zéro à chaque login : suite e2e via un seul login `beforeAll` + cookies `storageState`. ### Changed - **Settings mobile : le menu devient une side-navigation gauche** ouverte par un hamburger (`.settings-menu-btn`) avec backdrop, au lieu des onglets horizontaux ; fermeture auto au choix de section. Squelette panneau (overlay/panel/nav) extrait de `settings.html` vers `/static/css/settings.css` partagé avec l'aide. - **Page `/help` refondue au complet** — ancien monologue HTML inline de la route remplacé par `app/templates/help.html` : même panneau 2 colonnes que settings, nav latérale en 13 sections couvrant toute l'app (démarrer, éditeur, bases & vues, tâches/dépendances, workspaces/forges, collaboration, bibliothèque/recherche, agent IA & automations, API v2/webhooks/clipper, PWA offline, comptes & SSO, raccourcis, tips) ; `static/js/help.js` (`Alpine.data helpInit`). ### Tests - Gate e2e `mobile_regression.spec.js` : 6/6 (drawer + wheel scroll + side-nav settings + aide mobile/desktop) · `test_app.py` + `test_pwa_offline.py` : 230/230. ## v7.58.0 (2026-10-06) — Add plugins : catalogue on/off à effet réel (phase 8/8) ### Added - **`app/services/plugins.py` + migration 35** — table `plugins` (`slug`, `name`, `description`, `enabled`) pré-remplie avec **3 modules câblés** : `web-tools`, `web-clipper`, `automations`. Ligne absente = activé (défaut sûr : rien n'est coupé par accident). - **Effet réel, jamais un simple drapeau** : - `automations` OFF → dépendance FastAPI posée **à l'`include_router`** (3 lignes dans `main.py`, aucun router touché) → toutes les routes `/workspace/automations*` refusées, **et** le scheduler de fond passe en veille (garde sur chaque tick) ; - `web-clipper` OFF → `GET /extensions` + tout `/api/v2/web-clipper/*` refusés ; - `web-tools` OFF → `web_search` et `fetch_url` retirés du **schéma** de l'agent et de `execute()` (`ToolRegistry._all()`) : le LLM ne les voit plus. - **UI masquée côté serveur** — nouveau global Jinja `plugin_enabled(slug)` : `settings.html` rend la nav « Extensions » / « Automations » sous `{% if %}` (élément **absent** du DOM, pas seulement caché) et conditionne les sections en `x-show`. - **Menu +** — l'entrée « Add plugins » devient une vraie section : liste des 3 plugins (état + description) avec bascule ; `GET /api/agent/plugins` + `PATCH /api/agent/plugins/{slug}` (slug inconnu → 404, 401 sans session). - **Tests** — `tests/test_v758_plugins.py` : **10 tests** — catalogue, persistance en base, routes refusées (302 hors `/api` + 404 JSON pour `/api*`, handler unifié de `main.py`), outils retirés du registre, nav disparue du `/settings` rendu, câblage du menu, 401. ## v7.57.0 (2026-10-06) — Connecteurs : Discord, Telegram, Teams, MCP (phase 7/8) ### Added - **Presets Discord / Telegram** — champ **Type** dans le formulaire « Ajouter un connecteur » (Personnalisé / Discord / Telegram / Serveur MCP) : URL et schéma d'authentification pré-remplis. Colonnes `kind` + `auth` (`bearer`/`bot`/`none`) ajoutées à `agent_connectors` (migration **34**) ; Discord envoie son jeton préfixé dans l'en-tête d'autorisation, Telegram place le jeton **dans l'URL** via le substitut `{secret}` — substitué à l'appel, **jamais stocké en clair** (la colonne url ne contient que le motif). - **Teams** — scope `ChannelMessage.Read.All` ajouté aux Graph scopes M365 (consentement admin requis à la reconnexion). - **`app/services/mcp_client.py`** — client MCP (streamable HTTP) : handshake `initialize` → `notifications/initialized` → `tools/list` → `tools/call` (JSON-RPC 2.0, réponse JSON **ou** premier `data:` d'un `text/event-stream`), garde SSRF conservée, erreurs JSON-RPC remontées telles quelles, sortie bornée à 20 000 car. Les outils sont **cachés** en base (`tools_json`) — le bouton « Tester le connecteur » rejoue le handshake et met à jour la liste. - **Outils dynamiques au registre** — `ToolRegistry._all()` merge ce cache à chaque run : chaque outil est exposé au LLM sous `mcp__` avec son `inputSchema` d'origine, et `execute()` dispatche sur `tools/call`. Serveur désactivé → outil **absent du schéma** ; échec réseau → `ToolResult(error)` sans casser le run. - **Menu +** — badge `· N outil(s)` sur la fiche d'un serveur MCP ; `POST /api/agent/connectors` accepte `kind` + `auth` (400 sur valeurs inconnues). - **Tests** — `tests/test_v757_mcp_discord.py` : **12 tests**, 0 appel réseau réel (`_rpc` / `_get` monkeypatchés) — presets, `{secret}` absent de la base, handshake + séquence d'appels, cache, dispatch LLM, serveur désactivé, erreurs RPC, parse SSE, scopes Teams, câblage menu. ## v7.56.0 (2026-10-06) — Connecteurs : Google + Microsoft 365 (phase 6/8) ### Added - **`app/services/oauth_connectors.py`** — flow OAuth2 complet **PKCE (S256)** pour 2 fournisseurs décrits par 1 dict : - **Google** : Drive / Gmail / Calendar **en lecture seule** ; - **Microsoft 365** : Graph `Files.Read` / `Mail.Read` / `Calendars.Read` ; - `begin()` → URL d'autorisation + state + code_verifier ; `complete()` → échange du code ; `access_token()` → **refresh automatique** (60 s avant expiration, `refresh_token` conservé si le fournisseur n'en renvoie pas) ; `api_get()` → lecture bornée, `path` absolu refusé + validation SSRF ; - tokens chiffrés **Fernet** via `_encrypt_tokens` de `calendar_sync` (réutilisation, aucune nouvelle dépendance) dans la table **`connector_tokens`** (migration **33**, PK `(kind, user_id)`). - **4 routes OAuth** (`/api/agent/connectors/oauth/{kind}/…`) : `status`, `authorize` (cookies d'état HttpOnly 10 min + retour same-origin validé), `callback` (GET = SAFE_METHODS, `state` comparé en temps constant, tokens stockés puis cookies purgés, redirection `?oauth=connected` / `?oauth_error=…`), `disconnect`. **OpenAPI : 523 chemins**. - **Config + `.env.example`** : `GOOGLE_CLIENT_ID/SECRET`, `MS_CLIENT_ID/SECRET` (vidés = « non configuré »), redirect URI dérivé d'APP_BASE_URL. - **Catalogue** : les 2 nouveaux natifs apparaissent avec le badge « non connecté — lancer la connexion OAuth » / « connecté · N scope(s) » ; `connector_fetch` et `Tester` passent par l'API Graph/Google avec le token de l'utilisateur (outil LLM = `user_id` désormais transmis). - **Menu +** : « Se connecter » (redirige vers le fournisseur) / « Déconnecter » sur la fiche du connecteur, + toast au retour du flux (URL nettoyée via `history.replaceState`). ### Tests - `tests/test_v756_oauth_connectors.py` — **13 tests** : URL d'autorisation (PKCE, scopes, redirect URI, cookies), 400 non configuré, callback → tokens **chiffrés** en base + redirection + `status.connected`, **state forgé refusé sans aucun appel réseau**, callback sans session → `oauth_error=session`, refresh (grant_type, conservation du refresh_token), `api_get` (Bearer + hôte fixe + URL absolue refusée), non connecté, déconnexion, catalogue, outil LLM, 401 sans session, 404 kind inconnu, câblage menu. Aucun appel réseau réel (`_post_form` / `_api_get` monkeypatchés). - `test_v755` adapté : **5 natifs** (gitea, github, web, google, ms365). - **Suite complète : 1319 passed / 0 failed** (`-n auto`), `ruff` 0, `eslint` 0 problème. ## v7.55.0 (2026-10-06) — Menu + : Connecteurs — socle (phase 5/8) ### Added - **Table `agent_connectors`** (migration **32**) : `name`, `url`, `secret_encrypted`, `enabled`, `status`, `detail` — colonnes **plates**, pas de `config_json` (les scopes OAuth des phases 6-7 ajouteront le leur). - **`app/services/connectors.py`** — 1 fichier (pas de package) : - **3 connecteurs natifs** (`gitea`, `github`, `web`) servis à la volée, statut dérivé de la config **sans réseau** (`native_state()`) ; - CRUD des connecteurs personnalisés : l'URL passe par **`_validate_url` (garde SSRF)** à la création, la clé est **chiffrée Fernet** (`encrypt_secret`) et **jamais renvoyée** (seul `has_secret` l'est) ; - `probe()` : appel de contrôle (`_get`, timeout 10 s, re-vérification de l'hôte après redirection) qui **persiste** `status`/`detail` ; - `connector_fetch()` : lecture pour l'LLM — API Gitea/GitHub (native), recherche web (native) ou GET du connecteur personnalisé, borné à 20 000 car. - **API** (`/api/agent/connectors`) — `GET` (catalogue), `POST` (création, 400 sur URL privée), `PATCH` (name/enabled/secret), `DELETE`, `POST /connectors/probe` ; session requise partout (401), CSRF global. **OpenAPI : 519 chemins**. - **Outil LLM `connector_fetch`** (`tool_registry`, 26ᵉ outil) — args `connector` (id / nom / kind), `path`, `query` ; connecteur désactivé ou inconnu = erreur outil, **jamais une exception qui casse le run**. - **Menu + : section « Connecteurs »** — catalogue avec badge de statut (✓/✗/⚠/?), fiche par connecteur (Tester, Activer/Désactiver, Supprimer — les 2 derniers masqués pour les natifs) et formulaire « Ajouter un connecteur personnalisé » (nom, URL, clé en `type="password"`). ### Tests - `tests/test_v755_connectors.py` — **13 tests** : 3 natifs listés, clé jamais renvoyée + chiffrée en base (roundtrip Fernet), **5 URLs refusées** (localhost, 127.0.0.1, metadata cloud, ftp:, file:), toggle persisté + 404, 401 sans session, probe OK (clé bien envoyée) / probe erreur persistée, outil `connector_fetch` (schéma, succès, désactivé, inconnu), câblage du menu. - `test_v751`/`test_v753`/`test_v754` adaptés : **1 seule section « bientôt »** (plugins). - **Suite complète : 1306 passed / 0 failed** (`-n auto`), `ruff` 0, `eslint` 0 problème. ## v7.54.0 (2026-10-06) — Menu + : Mémoire de l'agent (phase 4/8) ### Added - **Mémoire de l'agent** — le moteur n'envoie jamais l'historique des messages (`AgentEngine.run()` ne construit que système + objectif courant) : chaque run repartait de zéro. Désormais une **ligne résumé par conversation** (`app/services/agent_memory.py`, table `agent_memory`, migration 31) est écrite à la fin de chaque run et **ré-injectée au contexte du run suivant** quand le toggle est activé. - `context_for()` : bloc « ## Mémoire de la conversation », **budget 4 000 caractères** (troncature par la gauche avec marqueur) ; - `remember()` : 1 note par échange (objectif 180 car. → réponse 700 car.), **20 notes max**, upsert, et **jamais une erreur ne fait échouer un run** ; - OFF = ni lecture ni écriture (contexte strict du run courant). - **Toggle « Mémoire » dans le menu +** — entrée racine à l'état réel (🧠 activée / 💭 désactivée avec le libellé correspondant), bascule via `PATCH /api/agent/conversations/{id}` (`memory_enabled` ajouté au whitelist), état persisté et reflété à l'ouverture/à la création de la conversation. - **`agent_conversations.memory_enabled`** (migration **31**, défaut `1`) + config **`AGENT_MEMORY_DEFAULT`** (`settings.agent_memory_default`) appliquée à la création d'une conversation ; la réponse de création expose la valeur. ### Tests - `tests/test_v754_memory.py` — **8 tests** : colonne + table créées par la migration, défaut ON à la création, toggle persisté relu en base, injection ON / absence OFF / persistance à travers un cycle OFF→ON, budget et nombre de notes bornés, **2 runs réels du moteur** (FakeLLM qui capture le prompt : mémoire présente ON, absente OFF), câblage front/back. - `test_v751` + `test_v753` adaptés : **2 sections « bientôt »** restantes (connecteurs, plugins). - **Suite complète : 1293 passed / 0 failed** (`-n auto`), `ruff` 0, `eslint` 0 problème. ## v7.53.0 (2026-10-06) — Menu + : Design System – Canevas (phase 3/8) ### Added - **Section « Design System – Canevas »** — liste des canevas (`GET /board/api/page-templates` : intégrés + personnels), puis une page de détail avec 3 actions : - **Créer une page à partir du canevas** (`POST …/0/use` avec `{key}` *dans le body* pour un intégré, `POST …/{id}/use` pour un personnel) ; - **Insérer dans le document ouvert** — `GET …/blocks` (nouvelle route) → `ensureBlockIds()` (global côté éditeur) → concaténation dans `E.blocks` + `autoSave/render`, désactivé si aucun document éditable n'est ouvert ; - **Enregistrer le document ouvert comme canevas** (`POST /board/api/page-templates` avec `page_id`). - **`GET /board/api/page-templates/{template_id}/blocks`** — blocs d'un canevas ( intégré via `template_id=0&key=`, sinon id) ; 404 clé/id inconnu, 500 si le JSON est corrompu. **OpenAPI : 516 chemins**, `info.version` 7.53.0. - **Canevas « Design System »** intégré (`design_system` dans `app/services/block_templates.py`) : callout tokens, TOC, 3 toggles composants, grille & espacement, checklist revue UI, citation. - **Navigation** : en-tête ← gère le niveau canevas → liste. ### Fixed - **Prefixe des canevas** : le front du panneau utilisait `/api/page-templates` (404 silencieux) — la route réelle est `/board/api/page-templates` (router `prefix="/board"`), comme `base.html` le fait déjà. ### Tests - `tests/test_v753_canvases.py` — **5 tests** : canevas `design_system` listé, route blocks intégré (sans `id`, 404 clé inconnue, `empty` par défaut), route blocks personnel + 404, création de page depuis le canevas (blocs lus en base, ids assignés), câblage des sections (3 « bientôt » restantes) + garde-fou « `key` dans le body ». `test_v751` adapté (3 sections désactivées). - **Suite complète : 1285 passed / 0 failed** (`-n auto`), `ruff` 0, `eslint` 0 problème. ## v7.52.0 (2026-10-06) — Menu + : Compétences-skills « Gérer » + « Parcourir » (phase 2/8) ### Added - **Sous-menu « Compétences-skills »** — 11 skills builtin + les skills enregistrés, avec **libellés affichés** « Deep research » (`/research`) et « Skill-creator » (`/create-new-skill`) via `FD_SKILL_ALIAS` (slugs intacts) ; le clic épingle le skill au contexte, même chemin que la palette `/`. - **« Gérer les compétences »** — section du menu : liste des skills, formulaire intégré (nom, description, prompt) pour créer/éditer, suppression avec nettoyage du chip épinglé, **export JSON** téléchargé et **import** en fichier (`overwrite:true` → jamais de doublon). - **« Parcourir les compétences »** — section galerie filtrable (nom / description / slug), installation en 1 clic (`installGallerySkill`). - **`PATCH /api/agent/skills/{skill_id}`** — édition partielle, whitelist de colonnes figée côté serveur, réponses 400 (champ vide / aucun champ), 401 (sans session), 404, 409 (nom déjà pris). **OpenAPI régénéré** : `docs/openapi-v2.json` → 515 chemins, `info.version` 7.52.0. - **En-tête ← + titre de section** dans le menu (indispensable à 3 niveaux : racine → compétences → gérer/parcourir/formulaire). - **Fix** : épingler un skill enregistré **n'efface plus** le message déjà tapé (`_pinDbSkill` ne vide le composer que s'il est vide). ### Tests - `tests/test_v752_skills_menu.py` — **7 tests** : PATCH complet + partiel, validations 400/404, 401 sans session (CSRF valide pour isoler la route), import sans doublon (409 puis `overwrite` → 1 seule ligne), aller-retour export → import, câblage des sections + alias, markup rendu dans la page. - `tests/test_v751_plus_menu.py` adapté : 4 sections « bientôt » au lieu de 5. - **Suite complète : 1280 passed / 0 failed** (`-n auto`), `ruff check app tests` propre, `eslint static/js/agent_panel_2.js` **0 problème**. ## v7.51.0 (2026-10-06) — Menu + de l'assistant : hub de contexte (phase 1/8) ### Added - **Menu à sections sur le bouton +** (`agent_panel.html` + `agent_panel_2.js`) : le + ouvre *Ajouter des fichiers ou répertoires*, *Compétences-skills*, *Connecteurs*, *Design System – Canevas*, *Add plugins*, *Mémoire (on/off)*. Sections pas encore livrées affichées « bientôt (phase N) » et **désactivées** (décision D3 : le menu ne promet rien que le code ne fait). Navigation clavier ↑/↓/Entrée/Échap, focus visible, Échap revient d'un niveau. - **Parcours fichiers & répertoires** — « Parcourir… » affiche un niveau à la fois via `/api/nav/menu` (contrat : dossier = `icon === 'folder'`) avec fil d'Ariane cliquable ; un document s'épingle (`document:`), un dossier s'épingle depuis la ligne « 📌 Épingler le dossier » (`folder:`). - **Jeton `folder:`** résolu par `ContextBuilder._mentions_context` : titre du dossier + documents directs, budget ~12k caractères (marqueur « contenu du dossier tronqué »). Enfants directs seulement — récursion à ajouter si les arbres profonds deviennent réels. - **« Rechercher… »** conserve l'ancien sélecteur de mentions (recherche par nom, `@` inline) à l'identique : zéro régression du chemin existant. ### Tests - `tests/test_v751_plus_menu.py` — **7 tests** : résolution du dossier, budget borné, id inconnu, contrat `/api/nav/menu`, rendu du menu dans la page, 6 sections dont 5 désactivées, jeton `folder:` côté front et côté backend. - **Suite complète : 1273 passed / 0 failed** (`-n auto`), `ruff check app tests` propre, `eslint static/js` **0 erreur**. ## v7.50.2 (2026-10-06) — Peek de Library : nu comme local-workspace (plus de bande ni double scrollbar) ### Fixed - **Le document ouvert par le bouton « Open » de Library n'occupait pas le panneau** — `.peek-body` gardait un `padding:16px 20px` + `overflow-y:auto` (héritage de l'ancien peek à rendu HTML) : l'iframe était amochée (519×816 mesurés contre 559×848 en local-workspace) avec une bande grise autour du document et une double scrollbar. Corps nu (`padding:0`, `overflow:hidden`) : l'éditeur remplit le panneau, exactement comme le peek « parfait » de /local-workspace (mesuré Playwright, cookie de session forgé). ### Tests - `test_library_peek_body_has_no_inset_padding`. Suite 1266 passed / 0 failed (-n auto) + ruff 0. ## v7.50.1 (2026-10-06) — Peek des bases : le contenu se rend enfin + tableau pleine largeur ### Fixed - **Une base ouverte en side peek restait à « Loading database… »** — `page_editor_embed.html` (le peek de `/pages/{id}?embed=1`) n'incluait pas `database_table.js` : `window.FlowDeckDB` était undefined dans l'iframe et le bloc `embed/collection` ne s'hydrat jamais. Inclus désormais (même traitement que `page_editor.html`). - **L'icône « Open » de fin de ligne d'un tableau de base ne faisait rien hors page dédiée** — le panneau `#db-side-peek` n'existait que dans le markup de `_database_table.html` ; sans lui, `openPageInSidePeek` était un no-op. Le panneau est créé à la demande par `database_table.js` (`ensureDbPeek`, comme `mt-peek` de My Tasks) : markup unique, le partial ne contient plus le panneau statique. - **Barre de scroll sous le tableau des bases** — `.database-table-container` plafonnait à 1100 px (règle héritée de Library) : le tableau ne prenait pas la largeur du document. Plafond retiré ; `max-width: 100%` aussi pour les conteneurs `.db-first` (base embarquée en premier bloc) et le titre des pages de base — les colonnes dictent seules le scroll horizontal. ### Tests - `test_embed_editor_can_render_inline_databases` (include peek + panneau créé à la demande, source unique de markup). Suite 1265 passed / 0 failed (-n auto) + ruff 0. ## v7.50.0 (2026-10-06) — Side peek : le document seul (mode édition), sans sidebar ni barre ### Fixed - **Tous les boutons « Open » ouvraient l'habillage pleine page dans le panneau latéral** — `library.js` (OPEN de hover), `local_workspace.js` (↢ Open + « Open in side peek » du menu contextuel) et `my_tasks.js` (peek des tâches) chargeaient `/pages/{id}` SANS `?embed=1` : le peek affichait sidebar + barre de navigation + éditeur (régression v7.49.0). Tous repassent en `/pages/{id}?embed=1` = le document en mode édition SEUL ; le cadre du panneau (garde/close/resize partagés `fdWirePeekResize`) reste identique. - **Le bouton « Open » du calendrier My Tasks ne faisait rien** — cause racine : le handler de clic testait `e.target.dataset.openRow`, mais les boutons `.mt-cal-open` du calendrier (et du Kanban) ne contiennent que leur `` : cliquer l'icône donnait `e.target` = `` sans dataset. Le handler résout désormais `t.closest('[data-open-row]')` — un seul chemin pour tableau, calendrier (5 vues) et Kanban. - **Menu contextuel global « Open in side peek »** — le clic ne faisait qu'afficher « Side peek not available in browser » : il route maintenant vers le peek de la page courante (`_libData.openPeekById` en Library, `_wsData.openSidePeek` en workspace local), repli nouvel onglet ailleurs. - **Peek d'une page de base (`content_format='collection'`)** — `?embed=1` forçait le template éditeur : une base ouverte dans le peek montrait un éditeur de blocs vide au lieu du tableau. Le template collection est gardé en mode embed (body.embed-mode retire sidebar + barre, le tableau reste). ### Added - **Bouton « Open » sur les tuiles du Kanban My Tasks** — le même que les lignes du tableau (`openButton(t)`, `.mt-hover-only` révélé au survol de la carte, overlay absolu pour ne pas déformer la tuile) ; ouvre le contenu de la tâche dans le panneau latéral. - **Bouton « pleine page » des peeks de bases** — navigue vers `/pages/{pageId}` (pleine page, sidebar + barre conservées) au lieu de l'URL `?embed=1` de l'iframe ; `dataset.page` porte l'id résolu. ### Tests - `test_my_tasks_workspace_coherence.py` : les 4 peek JS (`library`, `my_tasks`, `database_table`, `local_workspace`) doivent porter `?embed=1` (gate de cohérence inversé — l'ancien test imposait l'URL nue) ; nouveaux : résolution `closest('[data-open-row]')`, bouton Open sur tuiles Kanban, collection embed = tableau. Suite 1264 passed / 0 failed (-n auto) + ruff 0. ## v7.49.2 (2026-10-05) — Pages de base : titre au-dessus, tableau à gauche ### Fixed - **Le tableau d'une page de base commençait au 1/3 de la page** — une base créée depuis un document est un bloc `embed collection` rendu dans la colonne éditeur `.blocks-container` (`max-width: 900px; margin: 0 auto`) : centrée sur un grand écran, donc décalée loin du sidebar (mesuré x=694 sur 1920 px). Nouvelle classe `.db-first` sur le wrapper éditeur quand le premier bloc est une base : titre et blocs passent à `margin: 0` + `padding-left: var(--space-lg)` (liseré de la topbar) — tableau au ras du sidebar (x=256 mesuré), titre au-dessus, plus de débordement de l'embed (`max-width: 100%`, le scroll horizontal reste géré par `.db-table-wrapper`). - **Les pages de base dédiées (`content_format='collection'`) n'avaient pas de titre** — `page_editor_collection.html` rend le tableau sans en-tête de page. Un `

` est ajouté au-dessus du conteneur, avec le même liseré gauche que le tableau. ## v7.49.1 (2026-10-05) — Tableau des bases aligné sur le titre ### Fixed - **Le tableau d'une page de base était décalé sous le titre** — le conteneur `.database-table-container` avait `padding: 0 24px` tandis que la topbar (fil d'Ariane/titre) est à `var(--space-lg)` = 16 px : deux marges gauches différentes. Le conteneur passe à la même valeur que la topbar, `margin: 0` conservé — le bord gauche du tableau s'aligne désormais au ras du titre et de la marge. ## v7.49.0 (2026-10-05) — Side peek : l'éditeur plein écran, et un calendrier complet ### Added - **Calendrier My Tasks : cinq vues** — Mois, Année, Semaine, Jour et Planning, avec une navigation propre à chaque granularité (`←` / `→` et « Aujourd'hui » se recalculent : par exemple 5 – 11 octobre → 12 – 18 octobre en semaine, ou un jour en année). L'Année affiche les douze mini-mois de l'année courante et le Planning une ligne par jour avec ses tâches réparties par statut. - **Icône d'ouverture sur chaque tâche du calendrier** — même bouton « Open in side peek » que le tableau, révélé au survol. Elle porte le même attribut de ligne (`data-open-row`), donc un seul gestionnaire de clic pour toutes les vues : le titre est transmis par `data-open-title`, disponible là où il n'y a pas de ligne `.mt-row` (vues Jour, Semaine et Année). ### Fixed - **Impossible d'ajuster la largeur du side peek des bases** — le panneau avait été réécrit en bindings Alpine (`:class="dbPeekOpen"`, `@click.outside`) alors que le conteneur n'a aucun `x-data` : bindings inertes, `openPageInSidePeek` passait par `container.__x.$data` (undefined), le panneau ne s'ouvrait pas et la poignée de redimensionnement ne réagissait pas. Repassé en vanilla JS (câblage direct sur le document, repli discret pour les collections embarquées inline). Au passage : les quatre implémentations dupliquées du drag de largeur (Library, workspace local, My Tasks, bases) sont remplacées par un helper unique `window.fdWirePeekResize` (`app.js`) — pointer capture, borne 300 px–90 vw, clic = fermer, largeur persistée (`fd_peek_width`) et commune à tous les peeks, restaurée au chargement. My Tasks ne capturait ni le pointeur ni la largeur : son drag était erratique et non mémorisé. - **Largeur des pages standardisée sur celle de Library (1100 px)** — le conteneur des bases (`.database-table-container`) passe à `margin: 0` : le tableau est collé à gauche, au ras de la marge, au lieu d'être centré dans la page. `/trash` utilisait la classe `.lib-container` dont la règle vivait dans le `