# Changelog - FlowDeck ## 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 `