v7.69.4
37
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7468e497c6 |
fix: le bouton close de Settings ferme en un clic (v7.69.4)
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 intermittent et le fait que /workspaces
échappait au symptôme.
Correctif : location.replace() vers la page d'origine. Une seule navigation
déterministe — ni history.back(), ni popstate, ni rechargement différé. Aucun
chemin ne peut plus se terminer sans navigation. Effet secondaire assumé et
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 — le bouton semblait mort.
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é.
Correction de diagnostic : le diagnostic posé en v7.69.3 (« Alpine ne lie pas
le composant x-data="settingsInit()" ») était FAUX — un artefact de mon banc
d'essai target="_blank", pas le parcours réel. La page étant réactive après le
premier clic, le handler s'exécutait bien. ROADMAP.md et CHANGELOG.md § v7.69.3
corrigés pour ne pas laisser une piste fausse en héritage.
pytest 1421 passed / 0 failed · ruff OK · OpenAPI 526 chemins / 7.69.4
e2e 18/18 verts sur l'instance déployée
|
||
|
|
162eb257c7 |
fix: la section Aide est positionnée comme Settings (v7.69.3)
La section Aide et la section Settings partagent le même gabarit (`.settings-overlay` / `.settings-panel`) et la même feuille de style (`static/css/settings.css`), mais affichaient deux rendus différents. Cause : `help.html` neutralisait son overlay via un style INLINE sur la balise — `position:static`, `background:none`, `backdrop-filter:none`, `padding:0` — et imposait par-dessus un panneau `width:100%;max-width:1050px; height:calc(100vh - 120px);margin:0 auto`. Settings, lui, laissait `settings.css` faire son travail : overlay `fixed;inset:0` avec fond flouté, panneau `85vh` centré par `margin:auto`. Résultat perçu : Aide s'affichait comme un bloc posé dans la page, Settings comme une modale centrée floutant l'arrière-plan — malgré un code quasi identique. Correctif : les surcharges inline sont retirées de `help.html`. La géométrie vient désormais entièrement de `settings.css`, commun aux deux pages. Aucune valeur n'est dupliquée ; les deux balises sont strictement identiques. Vérifié en e2e en comparant les géométries CALCULÉES des deux pages (pas des valeurs de code) : overlay `fixed` avec fond opère des deux côtés, panneau 1050x765 centré à top:68px, bouton close à 13/13px du coin du panneau — les deux jeux de mesures sont identiques. Tests : `tests/test_v7693_help_positioning.py` (5) verrouille l'absence de surcharges inline et l'identité de balise avec Settings ; `e2e/v7693_help_positioning.spec.js` compare les géométries calculées. NON INCLUS — double-clic sur le bouton close de Settings : le bug a été REPRODUIT en e2e et sa cause racine identifiée, mais le correctif n'est volontairement PAS dans ce commit. Voir ROADMAP.md § v7.69.3 pour le relevé complet et la prochaine étape. pytest 1421 passed / 0 failed · ruff OK · e2e 15/15 verts sur l'instance déployée |
||
|
|
3d3b46b58b |
fix: sélection multi-blocs fonctionnelle (Ctrl+C/X, drag souris, clic droit) + identité visuelle (v7.69.0)
Trois bugs d'éditeur, chacun avec sa cause racine prouvée par trace
d'événements — le mécanisme Clipboard livré en v7.68 était inutilisable.
1) Ctrl+C / Ctrl+X multi-blocs ne faisaient RIEN.
Cause : le navigateur ne sait pas sérialiser une sélection qui traverse
deux `contenteditable` (chaque bloc en a un). `getSelection().toString()`
renvoie `""` et Chromium n'émet AUCUN événement `copy` — les handlers
`ct.addEventListener('copy')` n'étaient donc jamais atteints.
→ le raccourci est intercepté au keydown (capture sur document, garde
`closest('#_blocksCt')` pour ne pas voler Ctrl+C ailleurs) et le markdown
est écrit dans le presse-papier par nos soins. Sélection mono-bloc =
comportement natif inchangé.
2) Glisser la souris d'un bloc à l'autre ne sélectionnait qu'un bloc.
Cause : `mouseup` (on reconstruit la plage) → `click` → `focusin` → le
navigateur efface la sélection. La plage était 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. Chaque
nouveau geste (mousedown) remet l'attente à zéro, donc un clic pour
placer le curseur reste normal.
3) Le clic droit effaçait le surlignage.
Cause : `.bm-search` prenait le focus 10 ms après l'ouverture du menu.
→ le menu ne vole plus le focus quand un texte est sélectionné. Les
actions Clipboard utilisaient déjà la sélection figée à l'ouverture
(`d._clipSt`) ; le `preventDefault` du clic droit est remonté sur
`mousedown` (sur `contextmenu` il arrive trop tard).
Identité visuelle (bannière + logo du projet ajoutés à la racine) :
- assets optimisés servis : `static/img/banner.webp` 22,5 Ko (l'original
fait 1,6 Mo) + JPEG 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 <picture>
(WebP + JPEG) avec dimensions déclarées (pas de décalage de mise en page).
- `og:image`, `twitter:card` et `meta description` ajoutés sur la landing
ET dans base.html — la landing a son propre <head>.
Tests : `tests/test_v769_selection_persistence.py` (19 statiques) +
`e2e/v769_selection_persistence.spec.js` (5, vrais gestes : drag souris,
Ctrl+C/Ctrl+X au clavier, clic droit). `e2e/v768_columns_clipboard.spec.js`
corrigé : les gestes simulés (`execCommand`, plage fabriquée en evaluate)
sont remplacés par de vrais gestes, et le token statique obsolète
`si>=ei)return null;` aligné sur la garde `isCollapsed` réelle.
pytest 1413 passed / 0 failed · ruff OK · OpenAPI 526 chemins / 7.69.0
|
||
|
|
7cf922106f |
fix: colonnes rendues, tables IA sans pipe de tête, copier/couper/coller multi-blocs (v7.68.0)
Colonnes / équation / TOC - renderBlock() n'appelait JAMAIS renderColumnsBlock()/renderMathBlock()/renderTOC() (3 renderers morts) → un bloc colonnes (slash « Column list », Turn into 2/3/4/5 colonnes) tombait dans le rendu générique = paragraphe vide. Dispatch ajouté ; la × de suppression de colonne (opacity:0 sans survol) devient visible au survol. Tables rendues par l'IA - _tblIsData/_tblIsSep exigeaient un | de tête → le GFM sans pipe (A | B / --- | ---) devenait des paragraphes. Pipe de tête optionnel, séparateur gardé à ≥2 cellules (« texte » + « --- » reste un séparateur). md2b et paste2b partagent les helpers. Copier / couper / coller - Sélection de texte MULTI-BLOCS : _clipState()/_clipMarkdown() (début partiel → blocs entiers via blocksToMarkdown → fin partielle), clipCopy()/clipCut() (coupure = tête du 1er bloc + queue du dernier), listeners copy/cut (Ctrl+C/X via les événements natifs), groupe Clipboard (Cut/Copy/Paste) dans le menu du clic droit avec la sélection figée à l'ouverture (la recherche prend le focus), clic droit sur un bloc sélectionné = sélection multi-blocs conservée. - Coller un seul bloc structuré (table/code/titre) = vraie insertion (avant : texte brut) ; pasteBlocks colle après le dernier bloc sans focus. - « Copy link to block » réparé (data-bm-act link → copy-link). Tests: pytest tests/ -q -n auto → 1394 passed / 0 failed ; ruff OK ; e2e v768_columns_clipboard (4 : slash+Turn into colonnes, table IA sans pipe, copier/couper/coller, clic droit Clipboard) + v762/v764/v766/v7641/v763/ regression_editor_mount → verts sur :8081 (image flowdeck:v7.68.0). |
||
|
|
d58c321c07 |
feat: réactions dans le tiroir Comments (+ suppression) et refonte du menu de sélection (v7.67.0)
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 ne s'affiche plus s'il reste des réactions. - Suppression : ✕ sur la ligne du tiroir (mes réactions) ET croix ::after au survol de la pastille sur l'émoji que j'ai posé (aucun nœud ajouté → offsets intacts). Deux entrées → removeReaction() → POST toggle → loadReactions(). Menu de sélection (maquette ASCII) - Ligne 4 = rangée scannée : 💬 Comment (libellé) à gauche, 🙂+ réaction à droite (.ft-row-split) ; la réaction sort de la ligne 3. - Entête Skills avec ≡ à droite (::after), liste Skills scrollable. - ✨ Ask AI retiré de la toolbar : doublon exact de « Edit with AI » du pied. - Écart volontaire à la maquette : le barré (T) de la ligne 2 est conservé. Fix racine (flake de gate) - _is_public_host attrape OSError (le garde SSRF renvoie un booléen, il ne lève plus) : un socket.timeout DNS ne bascule plus fetch_og_metadata dans son except Exception → repli silencieux → AssertionError. - fixture stub_dns sur TestOGParser (même motif que test_agent_web_tools) : zéro vrai DNS dans les tests OG. Tests: pytest tests/ -q -n auto → 1390 passed / 0 failed (flake corrigé) ; ruff check app tests → OK ; e2e v764 (réactions dans le tiroir + suppression A/B), v762 (nouvelle ligne 4), v766, v7641, v763 → 6 passed sur :8081 (image flowdeck:v7.67.0 rebuild + redéployée). |
||
|
|
ab6977733d |
feat: Escape du menu slash retire le « / » + bouton « commenter » à droite du bloc (v7.66.0)
- SM.escClose() : un seul implément, appelé par les deux chemins de fermeture du menu slash (focus dans le bloc → onKd, focus dans l'entrée du menu → listener document). Retire le « / » d'ouverture seulement, remet le caret, sync() + autoSave(). - Bouton .block-comment-btn à droite du bloc (gouttière 64px) : icône bulle de conversation + , visible au survol/focus comme le handle, masqué sur mobile (2 breakpoints) et sur page verrouillée ; onclick = E.commentBlock(idx). Remplace l'ancien .block-actions de renderBlock (opacity:0 sans règle de survol → un + invisible qui capait les clics dans la gouttière). - fmtComment() restaure la plage de la toolbar (_fmtRestore) avant d'ancrer : commentBlock() ne se replie plus sur this._sel (plage périmée éventuelle). Tests: pytest tests/ -q -n auto → 1386 passed ; ruff check app tests → OK ; e2e v766 (Esc → menu fermé + bloc vide ; bouton masqué → visible au survol, à droite du wrapper, ancre = bloc visé) + v764/v762/v7641/v763/ regression_editor_mount → 7 passed sur :8081. |
||
|
|
00c5e2a60d |
feat: commentaire ancré sur la SÉLECTION de texte (pas le bloc) + retrait du bouton bleu « 💬 Comment » (v7.65.0)
- commentBlock() : l'ancre est la sélection vivante dans le bloc (repli : la plage mémorisée par la toolbar de sélection, qui survit au clic du bouton), sinon tout le bloc ; offsets via _srcOffset() → anchor_start/anchor_end exacts dans le texte source, l'ancre jaune ne couvre que le texte choisi. - Suppression de #_commentSelBtn (bouton bleu flottant « 💬 Comment » au-dessus de la sélection), de showCommentBtn()/hideCommentBtn() et du listener mousedown associé ; déclencheurs restants : 💬 de la toolbar de sélection, menu de bloc, Ctrl+⇧+M. - toggleComments() remet _commentSel à zéro à la fermeture du tiroir : un brouillon abandonné ne peut plus ancrer le commentaire suivant ailleurs. - e2e v764_comments_reactions : sous-sélection « reaction a un » → ancre = la plage seule (avant/après reload), #_commentSelBtn absent. Tests: pytest tests/ -q -n auto → 1383 passed ; ruff check app tests → OK ; e2e v764/v762/v763/v7641/regression_editor_mount → 5 passed sur :8081. |
||
|
|
858649e3c3 |
fix: la touche Delete supprime maintenant le bloc vide (comme Backspace) (v7.64.1)
- onKd : la garde « bloc sans texte → supprimer le bloc » couvre Backspace ET Delete (une seule branche partagée) — Delete vide d'abord le bloc, puis le supprime au coup suivant ; 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 n'empêche plus la suppression - Tests : e2e/v7641_delete_key.spec.js (texte → Delete vide le bloc → Delete supprime le bloc → dernier bloc jamais supprimé) ; 4/4 specs e2e verts, suite complète 1383 passed / 0 failed, ruff OK, OpenAPI 7.64.1 (526 chemins) |
||
|
|
400a827e3f |
feat: commentaires façon Notion — ancres jaunes, tiroir refondu, réactions emoji sur texte (v7.64.0)
- Réaction sur texte : bouton 🙂+ (« React to selected text ») dans la toolbar de sélection → sélecteur d'émoji EXISTANT en mode réaction (en-tête = texte sélectionné + ✋, onglets masqués, ligne Recent au-dessus de la grille People, Filter… et barre de catégories avec + custom inchangés) - Backend : migration 37 text_reactions (UNIQUE plage × emoji × utilisateur → POST idempotent) + GET/POST /api/pages/{id}/reactions (GET groupé par plage emoji/count/mine, POST toggle, 400 sur plage vide) - Affichage : plage commentée/réagie peinte en jaune (_paintAnchors après render/loadComments/loadReactions), pastille 😀+compte après la plage, clic pastille = rouvrir le sélecteur, clic ancre = ouvrir et centrer le fil - Pastilles = annotations : gtTok/gtMd les ignorent (y compris le chemin rapide innerText) et les offsets _srcOffset/_srcNodeLen les excluent → le texte sauvé est identique (vérifié dans le e2e) - Tiroir refondu : carte de fil (avatar, nom, horodatage, ✓ toggle resolve/reopen, ⋯ → Copy link #cmt-<id>/Delete), saisie « Add a comment... » avec @ et envoi ↑/Entrée ; #cmt-<id> ouvre et centre le fil au chargement ; loadComments + loadReactions à l'ouverture (commentCount enfin renseigné) - Fixes : resolveComment ne pouvait pas rouvrir (toujours resolved:true) ; showCommentBtn ignorait les sélections à ancre élément (bouton 💬 jamais affiché) - Tests : tests/test_v764_comments_reactions.py (8, dont toggle API réel), e2e/v764_comments_reactions.spec.js (8 étapes) — 3/3 specs e2e verts, suite complète 1383 passed / 0 failed, ruff OK, OpenAPI 7.64.0 (526 chemins) |
||
|
|
699165469d |
feat: menus mobiles façon Notion — barre horizontale au « / » + Insert block plein écran (v7.63.0)
- Barre flottante défilante sous le curseur (mobile ≤ 768 px) au lieu de la popup desktop, 15 boutons dans l'ordre de la description : +▾, Turn into▾, @, 💬, 🖼, 🗑, ←, →, ↑, ↓, ↩, ↪, Color▾, More▾, ⌨ - +▾ → feuille plein écran « Insert block » (titre centré, Cancel bleu, groupes Basic blocks / AI writing / Media / Data / Synced / Actions, raccourcis markdown à droite) ; items partagés avec la popup desktop (SM._slashItemHtml) - Turn into ▾ / Color ▾ → feuilles réutilisant les générateurs extraits _blockTurnItems / _blockColorItems (extraits de _blockTurnMenuOpen / _blockColorMenuOpen, comportement desktop inchangé) - More ▾ → menu contextuel de bloc existant ; @ insère + ouvre les mentions ; 💬 commentaire ; 🖼 bloc image ; 🗑 suppression ; ⌨ masque le clavier ; ↩ ↪ annuler/rétablir - moveBlock(bid, dir) : ↑↓ réordonne (racine + enfants toggle/columns), → indent dans le frère précédent, ← outdent, garde-fous avec toast - Fermeture auto de la barre quand le « / » n'est plus en tête de bloc ; taps dans la barre/les feuilles exclus du handler « clic hors-zone » ; Cancel ne referme que la feuille (la barre reste tant que le / est tapé) - Raccourcis slash alignés sur les préfixes markdown (-, [], >, ---), groupe SYNCED libellé « Synced » - Tests : tests/test_v763_mobile_menus.py (ordre, câblage, structure, styles + moveBlock exécuté sous node), e2e/v763_mobile_menus.spec.js (390×844, 1 passed) ; test_v761 adapté aux extractions ; suite complète 1375 passed / 0 failed, ruff OK, OpenAPI 7.63.0 (525 chemins) |
||
|
|
3a6445ca4e |
feat: menu contextuel au highlight de texte — toolbar 4 lignes façon Notion (v7.62.0)
- Toolbar de sélection sous le surlignage : ligne de style (T + type de bloc dynamique → sous-menu Turn into), A/B/I/U/T barré/Tx, lien/surlignage/ code/équation/…, Comment + Ask AI, section grise Skills (4 actions) et pied « Edit with AI » (Alt+⇧+E) ; positionnée sous la sélection (au-dessus si manque de place), fermée au clic hors sélection - « … » → menu contextuel de bloc existant (recherche « Search actions… », groupe Text, 10 actions + raccourcis, pied auteur/heure/taille) - Info-bulle claire à droite des éléments du menu : data-tooltip-html + classe .tooltip-light dans le tooltip global (app.js) - Persistance du formatage inline : sérialiseur gtMd() (DOM formaté → source markdown) branché sur sync() ; mdEsc() rend __…__ (u), ==…== (mark) et $$…$$ (équation inline KaTeX, data-tex conservé) - Fixes racine : askAI(idx) écrasé par askAI() → askAIBlock(idx) (menu de bloc, Ctrl+J, Alt+⇧+E) ; ed.__x (Alpine v2) inexistant sous v3 → _xed() via _x_dataStack[0] (showFmt/hideCommentBtn/mentions ne s'exécutaient jamais) ; ancre de sélection nœud élément gérée ; checkMd() appelé avec un id au lieu de l'index (exception à chaque barre d'espace) - Tests : tests/test_v762_selection_toolbar.py (roundtrip gtMd/mdEsc sous node) + e2e/v762_selection_toolbar.spec.js (structure, sous-menus, géométrie, persistance bold/highlight après reload) ; suite complète 1368 passed / 0 failed, ruff OK, OpenAPI régénérée 7.62.0 (525 chemins) |
||
|
|
efa3d57e93 |
feat(mobile): side-nav settings + scroll drawer + aide refondue settings-style (v7.59.0)
- Sidebar mobile: display:block du breakpoint ≤768px cassait flex:1 de .sidebar-scroll → molette/touch incapables de scroller le drawer. Flex restauré. Gate: wheel scroll scrollTop > 0. - Settings mobile: nav horizontale remplacée par une side-navigation gauche drawer (hamburger .settings-menu-btn + backdrop, fermeture auto au choix). Squelette panneau extrait vers static/css/settings.css (partagé). - /help refaite au complet: template help.html (13 sections couvrant toute l'app — éditeur, databases/vues, tâches, workspaces, collab, agent IA, API v2, PWA, SSO, raccourcis, tips) avec le même panneau 2 colonnes + drawer mobile; route réduite à un render; help.js (Alpine.data helpInit). E2E mobile_regression 6/6 + smoke 2/2 · pytest app+pwa 230/230 |
||
|
|
992200a90a |
fix(mobile): hamburger ouvrait sidebarCollapsed au lieu du drawer + layout settings responsive (v7.58.1)
Le handler du hamburger était une expression Alpine avec un effet de bord dans un ternaire (sidebarCollapsed ? toggleSidebar() : (mobileSidebarOpen = true, ...)). Évaluée sans eval dans le build CSP d'Alpine, elle ne produisait aucun effet visible sur mobile: le bouton ne faisait rien (prouvé via Alpine.evaluate@390px, open=false). Remplacée par la méthode fdHamburgerClick() (matchMedia 768px: drawer mobile, sinon toggle desktop). Settings: panel 1050px + nav verticale 200px inutilisable sur 390px → @media 768px: plein écran (dvh), nav en onglets horizontaux scrollables, rows en colonne, prov-grid 1 colonne, close 44px. + gate e2e mobile_regression.spec.js (drawer open/close + viewport settings) |
||
|
|
1d1cdbd618 |
fix: side peek des bases repasse en vanilla JS + largeur 1100px standard (v7.49.0)
- Panneau peek: les bindings Alpine (x-data absent du conteneur) rendaient loovverture et le redimensionnement inoperants -> cblage direct sur le document. - Helper unique window.fdWirePeekResize (app.js): pointer capture, 300px-90vw, clic=fermer, largeur persiste fd_peek_width partagee entre les 4 peeks. - database-table-container margin:0 (tableau colle a gauche, marge Library). - .lib-container remonte dans app.css (trash etait pleine largeur), .db-index 1100px. - ObsiGate verifie sans code: creation .xlsx OK (openpyxl, #186). |
||
|
|
894004a1f5 |
feat: confirmations destructives de /trash en fenêtre thématisée (v7.45.6)
Les 3 confirmations destructives de la poubelle (suppression définitive unitaire, suppression groupée, Empty Trash) remplacent le confirm() du navigateur par une fenêtre FlowDeck : - réutilise les classes flowdeck-modal-overlay / .flowdeck-modal déjà présentes dans base.html (variables --bg-secondary / --border / --accent, rayon 12 px, boutons Cancel + confirmation coloré) → aucun nouveau CSS ; - overlay cliquable pour annuler, Échap pour annuler, × pour annuler ; - bouton de confirmation porteur du libellé exact (Delete / Delete all / Empty Trash) et message reprenant le nom de la page ou le nombre de pages ; - role="alertdialog" aria-modal + aria-labelledby ; - composant dans trashData() : askConfirm / cancelConfirm / acceptConfirm, mutations de propriétés individuelles (pitfall réactivité Alpine) ; - id #fd-trash-confirm (la classe flowdeck-modal-overlay sert déjà à #fd-global-folder-modal → locator unique côté test) ; - aucun confirm() natif ne subsiste dans trash.html. Gates : e2e/trash_ui.spec.js étendu — ouverture + titre + message de la fenêtre, ANNULATION vérifiée (page toujours présente), confirmation (suppression réelle : plus dans la poubelle + contenu effacé), fenêtre Empty Trash ouverte/annulée quel que soit le contenu, purge réelle seulement si la poubelle ne contient que des pages E2E (instance partagée) ; le handler dialog du test échoue si un dialog natif revient (NATIVE_DIALOG). Suite e2e 11 passed · pytest 1099 passed · ruff OK · OpenAPI 510 chemins / 7.45.6. |
||
|
|
6582df3bcb |
feat: poubelle flexible (sélection groupée, purge, tri/filtres, dates) + 4 bugs (v7.45.5)
Page /trash alignée sur ce que fait ce type de section :
- sélection multiple (Select all porté au filtre courant) + barre d'actions
groupées Restore / Delete / Clear ;
- Empty Trash via POST /board/api/trash/empty (purge en masse, 1 requête) ;
- tri Recently deleted / Oldest first / Name et filtre emplacement RÉELS —
remplacent les deux boutons décoratifs « Last edited by ▾ » et « In ▾ » qui
ne faisaient rien ;
- date de suppression + jours restants par élément (rétention 30 j alignée sur
app/services/trash.py, horodatages UTC/ISO gérés) ;
- compteur de résultats, états vides distincts (vide vs filtre sans résultat +
Clear filters), toasts sur chaque action.
Bugs trouvés en route et corrigés :
1. Restore/Delete de la page ne marchaient JAMAIS : getCsrf() renvoie la chaîne
du jeton mais le code faisait csrf?.[1] → 2e caractère → 403 CSRF silencieux
avalé par if (r.ok).
2. « Move to Trash » de l'éditeur = 404 permanent : route appelée
/board/api/pages/{id}/trash (inexistante) alors que la route réelle est
/api/pages/{id}/trash, et le .then() naviguait quand même → la page partait
à l'accueil SANS être mise à la poubelle. URL corrigée + r.ok vérifié.
3. Page restaurée invisible en Library/Recents/Private : la suppression éditeur
réécrivait parent_section='Trash' et la restauration ne le remettait pas,
alors que tous les listings filtrent parent_section != 'Trash'. Écriture
retirée (deleted_at = source de vérité unique) + réparation idempotente au
boot dans db.init_db + requête sidebar /trash alignée sur deleted_at.
4. Routes trash sans aucune authentification (le CSRF ne protège pas : cookie
lisible + en-tête forgé) : 401 ajouté sur list/restore/delete/empty et sur
POST /api/pages/{id}/trash — vérifié anonyme → 401.
Gates : tests/test_trash_api.py (5) · e2e/trash_ui.spec.js (1, avec garde « on
ne vide jamais la poubelle d'autrui ») · pytest 1099 passed · ruff OK ·
e2e probe_nav_perf + partial_nav + editor_mount + logout_dnd + smoke = 10 passed ·
OpenAPI 510 chemins / 7.45.5.
|
||
|
|
aa88cf6665 |
fix: anti-FOUC au chargement complet (x-cloak sur .app-layout) + nettoyage (v7.45.4)
- Symptôme rapporté : clic sur Home → rafale de menus/fenêtres + bande rouge « You are offline. Changes will sync when connection is restored. ». - Constat : le masquage de v7.45.3 ne couvrait QUE les swaps partiels fdLoad. Un chargement COMPLET de document (F5, première visite, navigation servie par le service worker) peignait toute l'app tant qu'Alpine n'avait pas initialisé .app-layout : sidebar brute (sections ouvertes, menu utilisateur) + zone brute dont la bande hors ligne (aucun x-cloak dessus). Le symptôme Home n'est par ailleurs PAS rejouable en navigateur frais (nav, clic Home en ligne/hors ligne, plein chargement → 0 frame de contenu brut, 0 erreur console). - Fix : x-cloak sur le nœud racine Alpine .app-layout (base.html) — sidebar + zone + bandeau masqués jusqu'au montage d'appState(), retrait par Alpine à l'init. Le probe asserte que l'attribut est bien retiré et que le node est visible en fin de parcours (garde-fou contre la page blanche). - Suppression de la classe fd-navigating (posée à chaque fdLoad, aucune règle CSS dans le dépôt → code mort). - e2e/probe_nav_perf.spec.js : 4 scénarios avec assertions (nav 5 pages, clic Home réel, Home hors ligne avec SW, plein chargement 3 pages) + capture screenshot auto si contenu brut + collecte console. Gates : probe_nav_perf 4/4 · regression_partial_nav + regression_editor_mount + regression_logout_dnd + smoke = 6 passed (errs=0 sur 7 pages × 3 passages) · pytest 1094 passed · ruff OK · OpenAPI 7.45.4. |
||
|
|
1051a72b52 |
fix: flashs de navigation — la zone swapée n'est plus peint non montée (v7.45.3)
- Symptôme : à chaque changement de page/section, une rafale de fenêtres / menus / états s'affiche une fraction de seconde avant la page voulue. - Cause : pendant la fenêtre x-ignore (swap fdLoad → Alpine.initTree, mise en place en v7.45.2), .main-wrapper était peint NON montée : tous ses [x-show] visibles à leur valeur brute, puis masqués d'un coup au montage. Mesure (e2e/probe_nav_perf.spec.js sur l'instance locale) : 19 éléments bruts visibles 87 ms sur /library (CLS 0.149, sources lib-loading/lib-empty/ lib-table), 80 éléments pendant 732 ms sur /settings, 7 sur /workspaces. - Fix (app.js, handler htmx:afterSwap) : opacity:0 + pointer-events:none posés dans la même task que le swap (aucun paint intermédiaire possible), retirés APRÈS Alpine.initTree sur les 3 chemins de démontage (scripts chargés, zéro script, filet 4 s). reveal() est appelé avant les early-returns de done() → impossible de rester bloqué invisible. - Après : 0 frame de contenu brut peint sur les 4 navigations du probe, 1–2 frames masquées, zone révélée (opacity:1, pointer-events:auto) et montée (0 [x-show] non montés, x-ignore absent) à chaque fois. - Gate : e2e/probe_nav_perf.spec.js — marques htmx, fenêtre x-ignore, échantillon par frame des [x-show] bruts peints, layout-shift + assertion (0 brut / zone révélée et montée), exécutable en CI. - Reste (non traité, voir ROADMAP) : CLS résiduel 0.057 sur /library (états loading → table), fenêtre x-ignore de ~250 ms sur /settings (coût des scripts de page), topbar incluse dans .main-wrapper re-swappée à chaque nav. Gates : probe_nav_perf + regression_partial_nav + regression_editor_mount + regression_logout_dnd + smoke · pytest 1094 passed · ruff OK · OpenAPI 7.45.3. |
||
|
|
afbc236cc2 |
fix: navigation partielle Alpine sans course + scripts de page idempotents (v7.45.2)
- app.js : x-ignore sur .main-wrapper au swap fdLoad (nœud remplacé uniquement), démontage unique quand tous les <script src> ont exécuté (load/error + filet 4s) via Alpine.initTree idempotent (_x_marker) — fini les cascades « Undefined variable » (éditeur) et « reading 'has' » ($store.fdCtx) : la zone s'initialisait avant composants et stores. - 5 scripts de page gardés contre la ré-exécution (SyntaxError « Identifier 'LW' has already been declared » à la 2e visite partielle) : local_workspace, board, settings, database_table, page_editor_realtime. - if (window.Alpine) → Alpine.data immédiat sinon listener alpine:init (déjà passé sur swap) : 8 scripts + 7 templates inline. - app.css : purge des 7 blocs @font-face Inter orphelins (fichiers inexistants → 302 HTML → « Failed to decode downloaded font »). - Gates : e2e/regression_editor_mount + e2e/regression_partial_nav (7 pages x complet/1er/2e passage) + regression_logout_dnd + smoke = 6/6 verts sur l'image rebuildée · pytest 1094 passed · ruff OK · OpenAPI 7.45.2. |
||
|
|
c20aeaada1 |
fix: logout servi « hors ligne » par le SW + drag&drop upload en 403 CSRF (v7.45.1)
- SW : le fetch event de navigation arrive en redirect:'manual' → toute 302 du serveur (logout → /auth/login) se lisait opaqueredirect (status 0) → « bad status » → page hors ligne. networkFirst rejoue la requête en redirect:'follow' et sert une 302 synthétique vers l'URL finale (Chromium refuse une response 'redirected' servie à une navigation → ERR_FAILED). timeoutFetch annule désormais réellement (AbortController branchée). - Local workspace : _doUpload (upload/upload-folder) et toggleFavorite n'envoyaient pas X-CSRF-Token → 403 systématique depuis A19 (derniers appels mutants du front, balayage complet refait). - Porte : e2e/regression_logout_dnd.spec.js (2 tests verts, joués sur l'image rebuildée) · pytest 1094 passed · ruff OK · OpenAPI 7.45.1. |
||
|
|
6d7af3fb64 |
feat: éditeur visuel d'automations (pipeline steps) + fix CSP multi-instructions (v7.45.0)
Added — Settings → Automations, pipeline visuel (API steps v7.0) :
- Cartes ordinees (trigger/condition/delay/action) : resume + edition
TYPÉE par kind/type (datalist evenements, 7 ops, 8 types d'action avec
leurs champs reels), ajout/edition/suppression/haut-bas via
POST/PUT/DELETE /workspace/automations[/steps]/...
- ✨ Convertir le JSON en pipeline (legacy → steps ordonnes) ; textareas
JSON masques des qu'un step existe.
- Gate E2E « editeur visuel de steps » : creation → edition → ajout →
carte « Action · webhook » sous CSP reel.
Fixed (trouve par le gate) — 51 expressions Alpine MULTI-INSTRUCTIONS
(`a=1; b()`) = interdites par le parseur CSP (une seule expression par
directive ; ';' = token inattendu) — INVISIBLE pour le scan par tokens :
- Conversion en methodes dans 11 fichiers : nav settings x8 (navTo),
menu section base x7 (closeAndSetCount/Move/...), parts+editeur x13
(setSharePerm, more*, markAndSave...), breadcrumb x5 (hover*/goClose),
library/local x6 (menus popup), board x3 (pickStatus/...), ctx-menu x2
(addTagAndClear), agent/card/gitea/workspaces x6.
- Scanner dedie scan_semi (inventaire ';' hors chaines) ajoute au lot.
Verifs : 39 templates Jinja parse OK · scan expressions = 0 incompatible
(4 faux positifs en chaines) · **E2E 8/8** · suite **1094/1094** ·
ruff OK · docs a jour
|
||
|
|
b0af1bbfb6 |
feat: palette Ctrl+K — onglets Pages / ✨ Réponses IA (v7.44.0)
Added :
- Onglets dans la palette (markup + CSS + wiring IIFE) :
· Pages — comportement inchange (recherche /api/search + actions).
· ✨ Réponses IA — POST /api/v2/search/ask (debounce 150 ms, CSRF via
getCsrf(), gardes staleness onglet+requete) → answer_markdown rendu :
echappement AVANT injection, [[fdpage:ID]] → lien citation (ids =
chiffres, type = [a-z]+ — pas d'injection), **gras**, bloc « Sources »
avec liens /pages/{id} · /db/{id} ; etats hint/chargement/erreur.
- Gate E2E etendu : ouverture → clic onglet IA → reponse non-echappee.
(probe : ask = 200 en 1,6s, 8 citations, provider actif)
Reporte (backend absent) : onglet Fichiers — /api/search ne renvoie que
pages/collections → endpoint a creer d'abord (ROADMAP, entree mise a jour).
Notes : 401 transitoire sur le 1er ask d'un run E2E observe une fois
(session fraiche), non reproductible ensuite — signale CHANGELOG.
suite **1094/1094** · ruff OK · E2E **7/7** · docs a jour
|
||
|
|
48b5551b93 |
fix: BUG TOPBAR — boutons du header servis échappés sur toutes les pages (v7.42.0)
Cause racine : {% set right_actions = '…' ~ fd_icon(…) ~ '…' %} —
fd_icon est une macro → Markup, et Markup.__radd__/__add__ ÉCHAPPE ses
arguments str → tous les segments littéraux sortent entité-és ("/<),
et le |safe de _header:141 est no-op sur un Markup déjà échappé.
Régression probable depuis A10 (activation d'autoescape).
Fix (5 templates, forme idiomatique) : conversion en block-set
{% set right_actions %}…{{ fd_icon(…) }}…{% endset %} — source brute,
interpolation Markup brute : gitea_workspace, page_editor,
page_editor_collection, workspace, workspaces. (Piège script : regex
greedy multi-lignes avalait le set suivant → matcher sur UNE ligne.)
Tests :
- NOUVEAU tests/test_topbar_right_actions.py (permanent) : /workspaces
doit servir class="topbar-btn" parsé et ZÉRIE entité "
- gate éditeur CSP : assertion .star-btn RÉTABLIE (les boutons rendent)
- debug temporaires (DBGCLS/DBGVAL) retirés
suite **1094/1094** (+1 nouveau test) · ruff OK · E2E **7/7** (5
csp_preview + 2 smoke) · docs à jour (CHANGELOG détail, ROADMAP bug →
CORRIGÉ)
|
||
|
|
e4552c3763 | chore: probe Playwright jetable retirée (e2e/_probe_edit.js) | ||
|
|
6914780f24 |
feat: A20 phase 3 LOT 3b — gitea + agent + éditeur verts en CSP (v7.41.0)
Changed :
- gitea_workspace : x-data="giteaWorkspace" → appel giteaWorkspace(),
new Date(…) → fmtGwDate(pp), x-html icône arbre → bindGwIcon (x-init +
Alpine.effect).
- agent_panel : x-html markdown → bindMarkdown($el, m) (effet reactif).
- page_editor : les 12 sites window.E du topbar right_actions →
délégués appState (edCall('…') x6, edTimeAgo, edCommentCount, edShared,
bindStar — les 2 branches du ternaire favorited étaient identiques) ;
+ 3 sites dans _page_editor_content (edCall commentOnSelection,
openBacklink, fmtImportSize, bindIconHtml). Garde Jinja : quotes \' dans
le set délimité par ' (quote nue = 500).
- Gate éditeur (csp_preview) : création collection → /pages/{id},
délégués + editorState liés, filet 0-erreur.
Fixed :
- x-html iconHtml() du contenu éditeur = directive INTERDITE sous build
CSP (attrapé par le filet) → x-init + Alpine.effect.
⚠️ BUG pre-existant identifie (pas introduit ici) : les right_actions du
topbar sont servis ÉCHAPPÉS sur TOUTES les pages (entities "/< —
boutons Share/Star/Settings en texte brut). _header:141 a bien |safe,
ENV standard, rendu local = PARSED ; cause serveur à cerner → suivi
ROADMAP dédié. Le gate éditeur n'asserte donc pas la présence boutons.
suite **1093/1093** · ruff OK · E2E **7/7** (5 csp_preview + 2 smoke)
· docs a jour
|
||
|
|
d7d9966edf |
feat: A20 phase 3 LOT 3a — 5 surfaces CSP vertes + fix bug /import (v7.40.0)
Ajout :
- Gate csp_preview « surfaces simples » : /welcome, /trash, /accounts,
/workspace, /import — 0 modification necessaire sur les 4 premieres
(scan statique 0 expression/x-html + registres Alpine.data du lot 1).
8 surfaces couvertes au total.
Fixed (pre-existant, visible sous les DEUX builds) :
- /import : x-text "'🔗 '+report.relations…" evalue avec report=null
(le x-show parent ne masque pas, il initialise quand meme) →
pageerror « Cannot read property ... 'relations' » → garde
report && report.relations.
Reste ph3 documente dans ROADMAP : page_editor (12 sites window.E dans
right_actions), gitea_workspace (new Date), agent_panel (x-text+x-html
markdown), board/table_view/teamload/card_detail (scan propre, gates lies
au contexte Gitea) → bascule reel ensuite.
suite **1093/1093** · ruff OK · eslint 0/0 · E2E **6/6** (4 csp_preview +
2 smoke) · docs a jour
|
||
|
|
3cab76fed5 |
feat: A20 phase 3 LOT 2 — settings + local workspace verts en CSP preview (v7.39.0)
Ajout : - 2 gates csp_preview de plus : settings (composant lie, overlay visible) et local workspace (recherche focalisee via Alpine.nextTick, chips filtre en SVG via bindSvg, 0 erreur) → 3 surfaces vertes sous build CSP : library, settings, local workspace. Changed : - settings : window.history.back()/new Date(...) → methodes historyBack/fmtLastLogin/fmtAuditDate ; ?. → ternaires. - local workspace : x-data="_wsInitData" → registre wsInitData() ; 14 x-html → x-init + Alpine.effect (bindSvg/bindFileIcon/bindNodeIcon/ bindChildren/bindPreview) ; $nextTick+$refs arrow → toggleSearch() ; window.FlowDeck.* → createPageAt/createFolderAt ; ?. → ternaires ; @contextmenu="_wsInitData.*" → appel de methode. Piesges resolus (CHANGELOG en details) : - snapshot ji du build CSP = valeurs globalThis au boot → l'objet mis sur window avant Alpine est banni (« Accessing global variables ») → objet porte par une CONST LEXICALE (non propriete globalThis) + factory Alpine.data → MEME objet partage, reactivite intacte. - bloc preview hors div racine (structure pre-existante, masquee par le fallback window standard) → composant wsPreview DELEGUANT vers _wsInitData via Alpine.reactive (wrapper unique : les magics $nextTick ne sont redefinissables qu'une fois). - .env local : RATE_LIMIT_REQUESTS=600 (rafales E2E vs 60/min par IP ; defaut produit inchange). suite **1093/1093** · ruff OK · eslint 0/0 · E2E **5/5** (3 csp_preview + 2 smoke) · CSP preview ET standard = 0 erreur sur /local-workspace · docs a jour |
||
|
|
6ff88237fc |
feat: A20 phase 3 LOT 1 — shell + library migres, harnais csp_preview vert (v7.38.0)
Ajout : - e2e/csp_preview.spec.js — apercu CSP strict SANS deployer : le build officiel @alpinejs/csp (e2e/fixtures/alpine.csp.js, 0 eval) est servi a la place de alpine.min.js par interception Playwright ; toute expression que le parseur maison ne digere pas = pageerror (filet). Premiere surface VERTE : library (composant lie, icones SVG via Alpine.effect, recherche ouverte + focalisee, 0 erreur). Changed : - 16 composants x-data="fn()" enregistres via Alpine.data (registre = seule resolution du build CSP, probe « Undefined variable » ; scripts classiques executes pendant le parsing => alpine:init toujours joint) : appState, libraryPage, workspacesPage, editorState, board x4, settings/import/table_view/team_load/trash/workspace/welcome/accounts/ card_detail. - base.html (shell) migre : x-effect document.* -> syncSidebarClass(), $nextTick(arrow) -> initSidebarSort(), window.FlowDeck.* -> fdCreatePage/fdCreateFolder/fdGwRefresh, Object.keys/Math.min/ window.innerWidth dans x-for et :style -> sidebarSections()/ sectionMenuPos() — tout = simple appel de methode. - x-html restants du shell -> x-init + Alpine.effect : icone agent, carte projet, library x3 ; recherche library -> toggleSearch() (Alpine.nextTick) ; openMoveSelected() pour Object.keys en expression. - eslint : 70 warnings -> 0/0 (globals getCsrf depuis A38 ph1, /* exported openCardDetail */ + /* global owner, repo */, 3 ;; residuels). suite **1093/1093** · ruff OK · eslint 0/0 · E2E 3/3 (csp_preview + smoke x2) · docs a jour (ROADMAP ph3 LOT 1, CHANGELOG, WORKLOAD, VERSION) |
||
|
|
840d2b2615 |
feat: A20 — htmx allowEval off + plan Alpine CSP phase 3 scopé par probes (v7.37.0)
Changed :
- htmx `allowEval: false` dans le meta htmx-config (base.html) : plus
d'évaluation JS côté htmx (hx-on/hx-vars/hx-vals = 0 usage grep → zéro
régression possible) ; unsafe-eval reste UNIQUEMENT pour Alpine standard.
- Gate E20 renforcée : le smoke vérifie que `Alpine.$data()` lie un vrai
composant [x-data] de la page (lien composant = cœur de toute bascule CSP).
- sw.js : cache bump flowdeck-v7 (purge + re-precache après Inter).
Probes (non conservés, retirés après mesure) — A20 phase 3 scopée :
- Build `@alpinejs/csp` téléchargé et TESTÉ : 72 Ko, 0 eval/new Function,
parseur d'expressions maison, tourne sous CSP strict (meta sans
unsafe-eval) — le lint sélectif fonctionne.
- Mais bloqué sur FlowDeck :
(a) 13 expressions non parsables par la grammaire restreinte
(arrows ×2, typeof ×1, new Date ×4, optional-chaining ×6 ;
base, library, local_workspace, settings, gitea_workspace) —
le gate E2E a attrapé la première : `CSP Parser Error: Unexpected
token: PUNCTUATION ")"` ;
(b) 24 `x-html` réactifs (icônes SVG + markdown agent + preview) =
INTERDITS par le build CSP (innerHTML) → architecture d'icônes à
reposer ;
(c) scope des expressions CSP = données du composant uniquement
(probe : `Undefined variable: fmtDate` / `document`) → chaque site
devient une méthode Alpine.data enregistrée.
- Conséquence : build CSP reverté (alpine.min.js ×3 templates + sw),
unsafe-eval maintenu, fichier alpine.csp.min.js retiré (re-téléchargeable),
assert test CSP de nouveau `in`. Plan de migration composant par composant
(library → settings → local_workspace → gitea → base) + gate E2E par
surface documenté dans ROADMAP (A20 phase 3).
suite **1093/1093** · ruff OK · E2E **2/2** (dont assertion Alpine.$data)
· docs à jour (ROADMAP A20 phase 3, CHANGELOG, WORKLOAD, VERSION)
|
||
|
|
ab6ac1e84c |
feat: fondations E2E + 2 bugs trouvés (onglets ?view=, Inter CSP) (v7.36.0)
Ajout — e2e/smoke.spec.js (2 gates verts contre l'instance de test) : - gate A39 : bascule de vues d'une collection (clic onglet Calendar → ?view_type=calendar, grille .calendar + .cal-header rendue ; collection créée puis SUPPRIMÉE = répétable) - gate A20 : palette Ctrl+K (ouverture Alpine .open, recherche GET rend .cmd-palette-item, fermeture Échap) - filet console : 0 erreur JS/CSP (bruit Failed to load resource 401/403 filtré) - Service Workers bloqués : /sw.js sert sa page « hors ligne » sur les navigations redirigées (redirect:'manual') — pwa_offline.spec.js couvre le SW - bootstrap autonome : login OU création du compte e2e documenté (jamais de mot de passe deviné), workspace si absent - commande : cd e2e && node node_modules/@playwright/test/cli.js test smoke.spec.js Fixed — trouvés par les gates : 1. Bascule de vues standalone JAMAIS fonctionnelle : les onglets émettaient ?view=… mais la route lit `view_type` (FastAPI) → l'onglet restait sur Table quel que soit le clic (bug pré-existant, A28 n'y est pour rien). Onglets → ?view_type= ; test_all_view_tabs_present adapté + assertion comportementale (GET ?view_type=calendar rend .calendar). 2. Inter bloqué par la CSP depuis v7.27 : app.css importait encore Google Fonts (@import raté par le grep de la passe v7.27) → violation style-src sur chaque page + police en fallback. Inter auto-hébergé : 2 faces variables (100-900, latin + latin-ext) dans static/fonts/, @import supprimé (8 fichiers dupliqués dédupliqués → 2). suite **1093/1093** · ruff OK · E2E **2/2** · docs à jour |
||
|
|
1706ad1ee9 |
feat: v7.3.0 — cycle v6.8.0→v7.3.0 (Sites, Search, Automations, Calendar, SCIM, Wiki) + audit A9
- v6.8.0 Sites & Forms publics (migrations 24)
- v6.9.0 Recherche sémantique hybride + Ask AI (migration 25)
- v7.0.0 Automations v2 multi-étapes + Workers sandboxés (migration 26)
- v7.1.0 Calendar sync Google/CalDAV + Meeting Notes (migration 27)
- v7.2.0 Enterprise : SCIM 2.0, 2FA TOTP/passkeys, audit UI, agent approvals (migration 28)
- v7.3.0 Wiki/Teamspaces, verified pages, collab polish, charts, unfurl (migration 29)
- docs V68→V73, ROADMAP/CHANGELOG/WORKLOAD à jour, VERSION 7.3.0
- A9 : flowdeck.db, flowdeck_dev.db, test-commit.md, upload_test.txt et e2e/{node_modules,shots,test-results} désindexés + ignorés (.gitignore/.dockerignore)
|
||
|
|
189ed5bbca | chore: track opencode.json + e2e diag shots | ||
|
|
b5207216f1 |
feat: v6.0.0 PWA offline support
- manifest + icones, service worker (precache, network-first, Background Sync)
- module client FlowOffline (IndexedDB, queue, delta, flush) + hook editeur
- endpoints /api/v2/sync/{delta,batch,status} + moteur de sync (conflits LWW/orpheline/copie offline)
- migrations offline_sync_queue + sync_version (triggers)
- UI offline (banner, badge sync, toasts, icone dirty) + doc /help
- tests pytest (sync, migrations, SW, offline) + E2E Playwright; bump 6.0.0
|
||
|
|
0d475c3d2d | fix(workspace): compteurs de tags dynamiques + counts scoped par workspace | ||
|
|
9ab47d8113 | fix(workspace): menu contextuel après navigation partielle + sync sidebar/header au rename | ||
|
|
4038e9bdad |
fix(editor): repair block identity for template pages (duplicate/reordered lines)
Template-created pages (weekly report, project doc, meeting notes, to-do list, ...) were persisted without block ids. The editor assigned ids client-side, but the realtime room loaded the raw id-less content and sent it back on 'sync'; applySync then merged id-less server blocks with local blocks, producing data-bid='undefined' collisions and duplicated/shuffled lines as soon as the user edited. Editing an empty page was unaffected because the server state was empty. Fixes: - board.use_page_template: materialize unique block ids (recursively) when instantiating built-in or user templates. - realtime_server: unique block_id() (uuid) + ensure_block_ids() on room load and on insert ops. - editor: recursive ensureBlockIds() in init; gtTok() now restores [[fddate:...]] tokens (date chips survived as labels before). - realtime client: applySync() normalizes ids, no longer appends unknown local blocks (duplication), and keeps local content when server is empty. Tests: pytest (templates + realtime) and Playwright e2e covering to-do list, weekly report date chip, Enter ordering and legacy id-less repair. |
||
|
|
c7d4fd901f |
feat(e2e): ajouter tests Playwright flux utilisateur core (login, workspace, palette, dashboard)
- e2e/flowdeck_e2e_final.spec.js: 4 tests E2E validés 1. Login UI local (#email, #password, .btn-primary) 2. Créer workspace local + entrer (modal .dialog-input) 3. Ouvrir palette Ctrl+K et valider 'Créer une collection' 4. Dashboard accessible - e2e/ : package.json, playwright.config.js, install chromium |