Commit Graph
84 Commits
Author SHA1 Message Date
bruno 162eb257c7 fix: la section Aide est positionnée comme Settings (v7.69.3)
FlowDeck CI / lint (push) Successful in 1m32s
FlowDeck CI / test (push) Failing after 16m17s
FlowDeck CI / docker (push) Skipped
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
2026-10-08 21:07:17 -04:00
bruno b9a776b626 fix: le favicon est enfin le vrai logo, plus un placeholder (v7.69.2)
FlowDeck CI / lint (push) Successful in 1m34s
FlowDeck CI / test (push) Failing after 3h6m20s
FlowDeck CI / docker (push) Skipped
Le favicon était un carré bleu #2383E2 avec les lettres « FD » écrites en
<text> SVG : génériques, sans aucun lien avec la marque, et rendues avec la
police installée chez le visiteur (donc instables).

static/favicon.ico est désormais généré depuis assets/logo.jpg — la même
source que la bannière et les icônes PWA. 16/32/48 px dans un seul fichier,
un seul <link> pour tous les navigateurs, 4,1 Ko.

Pourquoi un fond sombre et pas le logo nu :
  le logo est blanc à ~82 % (mesuré via Pillow — 50,7 % de blanc pur, plus
  32 % de blancs proches). Posé nu, il se dissoudrait sur un onglet clair,
  qui est le cas nominal de Firefox et de Safari. Il est donc posé sur un
  carré #191919 arrondi, LE MÊME fond que les icônes PWA générées en
  v7.69.0, ce qui garde l'identité cohérente entre onglet, écran d'accueil
  et partage.

Contraste mesuré à 16 px : σ = 106 avec le fond sombre, contre σ = 49 pour
le logo seul — le fond sombre double littéralement la lisibilité à cette
taille. Le test statique verrouille les tailles embarquées dans l'ICO.

Mise à jour des 5 templates qui pointaient vers l'ancien SVG (base, import,
landing, public_page, welcome) + la liste de pré-cache du service worker.
static/favicon.svg SUPPRIMÉ : plus aucune référence dans le dépôt, le test
le vérifie pour éviter qu'il revienne.

pytest 1416 passed / 0 failed · ruff OK · OpenAPI 526 chemins / 7.69.2
2026-10-08 19:41:37 -04:00
bruno 674baf6954 fix: og:image et twitter:image en URL absolue (v7.69.1)
FlowDeck CI / lint (push) Successful in 1m34s
FlowDeck CI / test (push) Failing after 3h12m7s
FlowDeck CI / docker (push) Skipped
Les meta `og:image` / `twitter:image` ajoutés en v7.69.0 pointaient vers une
URL relative (`/static/img/logo-512.jpg`). Les crawlers — LinkedIn, Slack,
Discord, iMessage — ignorent une URL relative : la carte de partage de FlowDeck
était donc vide partout où un lien est collé.

Correction via le motif déjà en place dans `app/templating.py` : un global
Jinja `app_base_url()` construit à partir de `settings.app_base_url`, qui est
LA MÊME source que les liens de partage de pages (`collaboration.py:32`).
Si cette valeur est mal renseignée, ces liens l'étaient déjà — comportement
cohérent plutôt qu'une deuxième source de vérité à maintenir.

Lecture paresseuse de `settings` dans la fonction, à l'image de
`_plugin_enabled` : un import au chargement du module créerait un import
circulaire.

Rendu vérifié sur l'instance déployée :
  og:image" content="http://localhost:8080/static/img/logo-512.jpg

Tests : `test_og_image_url_is_absolute_via_base_url` vérifie le global ET
l'absence de la forme relative dans les deux templates. Piège évité au passage
— la forme absolue CONTIENT l'ancienne en sous-chaîne, donc l'assertion
négative porte sur `content="/static/img/` (le guillemet avant la barre) et
non sur l'URL complète.

pytest 1413 passed / 0 failed · ruff OK · OpenAPI 526 chemins / 7.69.1
e2e 14/14 verts sur l'instance redéployée
2026-10-08 19:00:13 -04:00
bruno 3d3b46b58b fix: sélection multi-blocs fonctionnelle (Ctrl+C/X, drag souris, clic droit) + identité visuelle (v7.69.0)
FlowDeck CI / lint (push) Successful in 1m34s
FlowDeck CI / test (push) Failing after 17m31s
FlowDeck CI / docker (push) Skipped
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
2026-10-08 17:27:14 -04:00
bruno 7cf922106f fix: colonnes rendues, tables IA sans pipe de tête, copier/couper/coller multi-blocs (v7.68.0)
FlowDeck CI / lint (push) Successful in 1m33s
FlowDeck CI / test (push) Failing after 18m53s
FlowDeck CI / docker (push) Skipped
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).
2026-10-08 15:23:46 -04:00
bruno d58c321c07 feat: réactions dans le tiroir Comments (+ suppression) et refonte du menu de sélection (v7.67.0)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
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).
2026-10-08 14:25:48 -04:00
bruno ab6977733d feat: Escape du menu slash retire le « / » + bouton « commenter » à droite du bloc (v7.66.0)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
- 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.
2026-10-08 12:47:00 -04:00
bruno 00c5e2a60d feat: commentaire ancré sur la SÉLECTION de texte (pas le bloc) + retrait du bouton bleu « 💬 Comment » (v7.65.0)
FlowDeck CI / lint (push) Successful in 1m34s
FlowDeck CI / docker (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 12m52s
- 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.
2026-10-08 12:19:56 -04:00
bruno 858649e3c3 fix: la touche Delete supprime maintenant le bloc vide (comme Backspace) (v7.64.1)
FlowDeck CI / lint (push) Successful in 1m34s
FlowDeck CI / test (push) Failing after 18m41s
FlowDeck CI / docker (push) Skipped
- 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)
2026-10-08 11:48:16 -04:00
bruno 400a827e3f feat: commentaires façon Notion — ancres jaunes, tiroir refondu, réactions emoji sur texte (v7.64.0)
FlowDeck CI / lint (push) Successful in 2m5s
FlowDeck CI / test (push) Failing after 3h5m38s
FlowDeck CI / docker (push) Skipped
- 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)
2026-10-08 10:46:14 -04:00
bruno 699165469d feat: menus mobiles façon Notion — barre horizontale au « / » + Insert block plein écran (v7.63.0)
FlowDeck CI / lint (push) Successful in 1m32s
FlowDeck CI / test (push) Failing after 21m41s
FlowDeck CI / docker (push) Skipped
- 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)
2026-10-08 09:36:37 -04:00
bruno 3a6445ca4e feat: menu contextuel au highlight de texte — toolbar 4 lignes façon Notion (v7.62.0)
FlowDeck CI / lint (push) Successful in 1m32s
FlowDeck CI / test (push) Failing after 20m17s
FlowDeck CI / docker (push) Skipped
- 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)
2026-10-08 08:39:39 -04:00
bruno 81746d9440 fix: peek de Library nu comme local-workspace - iframe pleine (v7.50.2)
FlowDeck CI / test (push) Failing after 13m34s
FlowDeck CI / lint (push) Successful in 2m32s
FlowDeck CI / docker (push) Skipped
.peek-body gardait padding:16px 20px + overflow-y:auto (heritage de l'ancien
peek a rendu HTML) : l'iframe etait amochee (519x816 mesures contre 559x848
en local-workspace) avec bande grise autour du document + double scrollbar.
Corps nu (padding:0, overflow:hidden) : l'editeur remplit le panneau,
exactement comme le peek de /local-workspace.

Mesure Playwright (cookie de session forge, les 14 boutons Open de Library) :
embed OK partout, bases hydratees, aucun jeton « Loading database... ».
Test: test_library_peek_body_has_no_inset_padding. Suite 1266/0 (-n auto)
+ ruff 0. Bump VERSION/main.py 7.50.2, CHANGELOG, WORKLOAD, OpenAPI.
2026-10-06 15:53:04 -04:00
bruno f53205bc45 fix: peek des bases hydrate FlowDeckDB + Open de ligne, tableau pleine largeur (v7.50.1)
FlowDeck CI / test (push) Failing after 14m37s
FlowDeck CI / lint (push) Successful in 2m11s
FlowDeck CI / docker (push) Skipped
- page_editor_embed.html incluait seulement _page_editor_scripts.html :
  window.FlowDeckDB etait undefined dans l'iframe -> une base ouverte en side
  peek restait a « Loading database... ». Include _database_table_scripts.html
  ajoute (meme traitement que page_editor.html).
- Le panneau #db-side-peek n'existait que dans _database_table.html : hors page
  dediee, openPageInSidePeek etait un no-op. Desormais cree a la demande par
  ensureDbPeek (database_table.js) ; markup unique retire du partial.
- .database-table-container : plafond 1100px retire (regle heritee de Library)
  + max-width 100% pour .db-first et database-page-title : le tableau prend la
  largeur du document, plus de barre de scroll parasite sous le tableau.
- Tests: test_embed_editor_can_render_inline_databases. Suite 1265/0 (-n auto)
  + ruff 0. Bump VERSION/main.py 7.50.1, CHANGELOG, WORKLOAD, OpenAPI.
2026-10-06 15:15:01 -04:00
bruno 3706689eca fix: side peek = document seul en edition + bouton Open calendrier/Kanban (v7.50.0)
FlowDeck CI / lint (push) Successful in 2m15s
FlowDeck CI / test (push) Failing after 13m47s
FlowDeck CI / docker (push) Skipped
- Les 4 peeks (library.js, local_workspace.js, my_tasks.js, database_table.js)
  chargent /pages/{id}?embed=1 : le panneau ne montre plus que le document en
  mode edition, sans sidebar ni barre (regression v7.49.0). Le cadre du
  panneau (close/full/resize partages fdWirePeekResize) est inchange.
- Calendrier My Tasks : le handler testait e.target.dataset.openRow mais les
  boutons .mt-cal-open ne contiennent que leur <svg> -> clic sur l'icone =
  e.target = <svg>, rien ne partait. cause racine corrigee par
  t.closest('[data-open-row]') : un seul chemin tableau/calendrier/Kanban.
- Kanban My Tasks : meme bouton Open ajoute sur les tuiles (openButton(t),
  overlay absolu revele au survol de la carte .mt-card).
- Menu contextuel global « Open in side peek » : etait un toast d'erreur ;
  route maintenant vers le peek de la page courante (_libData.openPeekById /
  _wsData.openSidePeek), repli nouvel onglet ailleurs.
- /pages/{id}?embed=1 sur une page de base (content_format=collection)
  rendait un editeur de blocs vide : le template collection est garde en
  embed (body.embed-mode retire sidebar+barre, le tableau reste). Bouton
  pleine page des peeks de bases : navigue via dataset.page (URL propre)
  au lieu du src ?embed=1.
- Tests : garde de coherence inversee (?embed=1 exige sur les 4 fichiers),
  +3 nouveaux (closest, tuiles Kanban, collection embed). Fix d'un test
  pre-existant casse (assertion panel.style.display, reecriture vanilla JS
  v7.49.0). Suite 1264 passed / 0 failed (-n auto) + ruff 0.
- Bump VERSION/main.py 7.50.0, CHANGELOG, WORKLOAD, OpenAPI regenere.
2026-10-06 12:48:48 -04:00
bruno 7dbaefb381 fix: page de base - titre au-dessus du tableau, tableau au ras du sidebar (v7.49.2)
FlowDeck CI / lint (push) Successful in 2m11s
FlowDeck CI / test (push) Failing after 13m46s
FlowDeck CI / docker (push) Skipped
- .db-first (page_editor_scripts._applyLayout) quand le premier bloc est une
  base embarquee : .blocks-container/.page-title-block passent a margin:0 +
  padding-left:var(--space-lg) au lieu de max-width:900px centre -> le tableau
  demarre a x=256 (marge topbar) au lieu de x=694 sur 1920px (mesure Playwright).
- .block-embed-collection { max-width:100%; overflow:hidden } : l'embed ne
  deborde plus de sa colonne (les popovers fixes restent hors du clipping).
- page_editor_collection.html : h1.database-page-title au-dessus du conteneur
  (les pages collection dediees n'avaient pas de titre).
- Bump VERSION/main.py 7.49.2, CHANGELOG, WORKLOAD, OpenAPI regenere.
- Instance locale flowdeck-final rebuildee flowdeck:v7.49.2 :8081, health OK.
2026-10-06 09:49:16 -04:00
bruno 15b6b163f4 fix: tableau des bases aligne sur le titre (v7.49.1)
FlowDeck CI / lint (push) Successful in 2m10s
FlowDeck CI / test (push) Failing after 15m54s
FlowDeck CI / docker (push) Skipped
.database-table-container: padding 0 24px -> 0 var(--space-lg), identique a
celui de la topbar (16px) : le bord gauche du tableau s'aligne au ras du
fil d'Ariane/titre, au plus pres de la marge gauche. Bump VERSION/main.py,
CHANGELOG, WORKLOAD, OpenAPI. Instance locale flowdeck-final rebuild ee
v7.49.1, health OK, CSS verifie dans le conteneur.
2026-10-06 08:18:02 -04:00
bruno 1d1cdbd618 fix: side peek des bases repasse en vanilla JS + largeur 1100px standard (v7.49.0)
FlowDeck CI / test (push) Failing after 3h13m58s
FlowDeck CI / lint (push) Successful in 2m12s
FlowDeck CI / docker (push) Skipped
- 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).
2026-10-05 22:47:36 -04:00
bruno 894004a1f5 feat: confirmations destructives de /trash en fenêtre thématisée (v7.45.6)
FlowDeck CI / docker (push) Successful in 1m52s
FlowDeck CI / lint (push) Successful in 1m59s
FlowDeck CI / test (push) Successful in 15m20s
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.
2026-10-03 10:16:45 -04:00
bruno 6582df3bcb feat: poubelle flexible (sélection groupée, purge, tri/filtres, dates) + 4 bugs (v7.45.5)
FlowDeck CI / docker (push) Successful in 1m52s
FlowDeck CI / lint (push) Successful in 2m1s
FlowDeck CI / test (push) Successful in 15m21s
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.
2026-10-03 01:35:26 -04:00
bruno aa88cf6665 fix: anti-FOUC au chargement complet (x-cloak sur .app-layout) + nettoyage (v7.45.4)
FlowDeck CI / docker (push) Successful in 1m52s
FlowDeck CI / lint (push) Successful in 2m15s
FlowDeck CI / test (push) Successful in 15m22s
- 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.
2026-10-03 00:54:28 -04:00
bruno 1051a72b52 fix: flashs de navigation — la zone swapée n'est plus peint non montée (v7.45.3)
FlowDeck CI / docker (push) Successful in 1m52s
FlowDeck CI / lint (push) Successful in 1m58s
FlowDeck CI / test (push) Successful in 15m25s
- 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.
2026-10-03 00:15:41 -04:00
bruno afbc236cc2 fix: navigation partielle Alpine sans course + scripts de page idempotents (v7.45.2)
FlowDeck CI / docker (push) Successful in 1m51s
FlowDeck CI / lint (push) Successful in 1m57s
FlowDeck CI / test (push) Successful in 15m16s
- 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.
2026-10-02 21:29:46 -04:00
bruno c20aeaada1 fix: logout servi « hors ligne » par le SW + drag&drop upload en 403 CSRF (v7.45.1)
FlowDeck CI / docker (push) Successful in 1m51s
FlowDeck CI / lint (push) Successful in 1m56s
FlowDeck CI / test (push) Successful in 15m11s
- 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.
2026-10-02 17:41:07 -04:00
bruno 6d7af3fb64 feat: éditeur visuel d'automations (pipeline steps) + fix CSP multi-instructions (v7.45.0)
FlowDeck CI / docker (push) Successful in 1m51s
FlowDeck CI / lint (push) Successful in 1m56s
FlowDeck CI / test (push) Successful in 15m9s
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
2026-10-02 16:56:29 -04:00
bruno b0af1bbfb6 feat: palette Ctrl+K — onglets Pages / ✨ Réponses IA (v7.44.0)
FlowDeck CI / lint (push) Successful in 1m57s
FlowDeck CI / test (push) Successful in 15m21s
FlowDeck CI / docker (push) Successful in 1m51s
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
2026-10-02 16:27:52 -04:00
bruno de751ffe35 feat: A20 TERMINÉ — Alpine en build CSP, unsafe-eval retiré de la CSP (v7.43.0)
FlowDeck CI / test (push) Successful in 15m24s
FlowDeck CI / lint (push) Successful in 2m1s
FlowDeck CI / docker (push) Successful in 1m52s
La bascule A20 phase 3 :
- static/js/alpine.csp.min.js (build officiel @alpinejs/csp, 0
  eval/new Function, parseur maison) servi partout : base.html,
  import.html, welcome.html + entree sw.js (cache bump v8).
- CSP : script-src 'self' 'nonce-…' — unsafe-eval SUPPRIMÉ (ne servait
  plus qu'Alpine standard). htmx allowEval:false deja pose (v7.37).
- Assertion test inversée : assert "'unsafe-eval'" not in script_src.
- Scan statique final sur TOUS les templates : 0 expression incompatible
  (4 residus = faux positifs dans des chaines de texte).

Pré-requis réunis par les lots 1-3 : 12 surfaces migrées + gateées
(csp_preview), registres Alpine.data, x-html → x-init+Alpine.effect,
délégués window.E, partage d'état lexical, bug topbar corrigé.

Verifs : suite **1094/1094** · ruff OK · eslint 0/0 · **E2E 7/7 sous
CSP reel** (script-src sans unsafe-eval verifie sur l'instance).

Hors gate (scan propre, gitea down) : board/table_view/teamload/
card_detail → à vérifier au premier usage avec gitea remonté (noté
ROADMAP/CHANGELOG).
2026-10-02 16:00:12 -04:00
bruno 48b5551b93 fix: BUG TOPBAR — boutons du header servis échappés sur toutes les pages (v7.42.0)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
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 (&#34;/&lt;),
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é &#34;
- 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É)
2026-10-02 15:39:57 -04:00
bruno 6914780f24 feat: A20 phase 3 LOT 3b — gitea + agent + éditeur verts en CSP (v7.41.0)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
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 &#34;/&lt; —
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
2026-10-02 15:15:26 -04:00
bruno d7d9966edf feat: A20 phase 3 LOT 3a — 5 surfaces CSP vertes + fix bug /import (v7.40.0)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
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
2026-10-02 13:49:11 -04:00
bruno 3cab76fed5 feat: A20 phase 3 LOT 2 — settings + local workspace verts en CSP preview (v7.39.0)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
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
2026-10-02 13:24:46 -04:00
bruno 6ff88237fc feat: A20 phase 3 LOT 1 — shell + library migres, harnais csp_preview vert (v7.38.0)
FlowDeck CI / lint (push) Successful in 1m56s
FlowDeck CI / test (push) Successful in 15m32s
FlowDeck CI / docker (push) Successful in 1m51s
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)
2026-10-02 12:08:18 -04:00
bruno 840d2b2615 feat: A20 — htmx allowEval off + plan Alpine CSP phase 3 scopé par probes (v7.37.0)
FlowDeck CI / docker (push) Successful in 1m49s
FlowDeck CI / lint (push) Successful in 1m55s
FlowDeck CI / test (push) Successful in 15m4s
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)
2026-10-02 11:14:47 -04:00
bruno ab6ac1e84c feat: fondations E2E + 2 bugs trouvés (onglets ?view=, Inter CSP) (v7.36.0)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 3m26s
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
2026-10-02 10:23:34 -04:00
bruno 3a74ea8bbd fix: A35 TERMINÉ — drift Python 3.12→3.13 aligné, rebuild validé (v7.35.0)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
Dernier reliquat de l'A35 (docs/périmètre dérivés) :

- Dockerfile : python:3.12-slim → python:3.13-slim (builder + runtime)
- .gitea/workflows/ci.yml : python-version '3.12' → '3.13' (lint + test)
- README.md : « Python 3.12 » et « python:3.12-slim » → 3.13 (×2)
- pyproject.toml : ruff target-version py312 → py313 (0 nouvelle
  remarque ruff)
- zéro référence 3.12 résiduelle ; uv.lock (requires-python >=3.13) et
  le venv (3.13.14) étaient déjà bons

Validation (le point laissé « à faire par un rebuild d'image ») :
- docker build VERT sur python:3.13-slim → image flowdeck:a35-py313
- dans le conteneur : python -V = 3.13.16, `import app.main` OK
  (v7.35.0) → wheels requirements.txt construits + importables sur 3.13

A35 = TERMINÉ (OpenAPI/README/titre dupliqué faits en 7.3.9 + drift).

suite **1093/1093** · ruff OK (target py313) · docs à jour
2026-10-02 09:39:02 -04:00
bruno 13dc8fdaad fix: A38 phase 2 — 0 doublon de fonction globale + garde-fou (v7.34.0)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
- Inventaire exhaustif des 13 noms `function NAME` définis 2+ fois
  (templates + static/js) avec scan de profondeur de brace (strings,
  comments, backticks gérés) : 12 sont déjà scopés dans des IIFEs
  depuis A27 (escHtml/flush/emit/setMeta/initials/up/esc/show/close…) —
  aucun conflit de page possible.
- Seul doublon GLOBALE = openCardDetail (corps byte-identiques ×2 dans
  board_fragment + detailed_board, fragments de vues mutuellement
  exclusifs) → dédupliquée vers static/js/app.js, 2 copies supprimées ;
  les onclick/@click des deux fragments appellent la même définition
  (owner/repo globaux fournis par board.js au moment du clic).
- test_no_duplicate_global_functions : garde-fou 0-doublon entre
  templates et static/js (scanner naïf, plafond ponytail commenté).

Reste A38 : méthodes jumelles library/local_workspace (~9-21 noms
communs, corps divergents) → fusion workspace-tree.js reportée
(réconciliation sans E2E, même logique que A39/A20).

suite **1093/1093** · ruff OK · node --check vert · docs à jour
2026-10-02 09:18:04 -04:00
bruno 770fdc2b68 fix: A43 TERMINÉ + A38 phase 1 — CSRF rendu côté serveur, helper unique (v7.33.0)
FlowDeck CI / lint (push) Successful in 1m57s
FlowDeck CI / test (push) Successful in 15m20s
FlowDeck CI / docker (push) Canceled after 0s
A43 (les 4 items sont clos) :
- `__CSRF_PLACEHOLDER__` supprimé : ContextVar CSRF_TOKEN posée par
  CSRFMiddleware avant call_next (même mécanique que le nonce CSP),
  global `{{ csrf_token() }}` dans templating, base.html rend
  `{"X-CSRF-Token":{{ csrf_token()|tojson }}}` — vide si cookie absent,
  `htmx:configRequest` re-lit le cookie à chaque appel → plus jamais de
  jeton factice servi.
- Palette : `fetch('/api/search…')` SANS header (GET ∈ SAFE_METHODS →
  CSRF inapplicable) — le JSON.parse du body-attr par frappe disparaît.
- utcnow déprécié = 0 dans app/**.py ; health loggé (A25) + booléens db/
  gitea = raison ; probe réseau = voulu (test de connectivité).

A38 phase 1 (CSRF unifié) :
- `window.getCsrf()` unique dans le <head> de base.html.
- 76 lectures brutes du cookie → getCsrf() dans 13 fichiers (47 formes
  `(…||[])[1]||''`, 25 déclarations `const X = match(…)` avec conversion
  de leurs usages `X?X[1]:''` → `X` (noms collectés PAR FICHIER, jamais
  de règle globale), 4 formes espacées).
- Définitions dupliquées supprimées : card_detail.html (multi-lignes),
  database_table.js (1 ligne dans IIFE → chute sur le global).
- Les 3 variantes de base.html (IIFE getCsrf + 2 getCsrfToken) →
  `return getCsrf()`.
- welcome.html garde sa lecture locale (page autonome sans base) ;
  reste exactement 1 raw dans base.html (la déf head) + 2 dans welcome.

Test : test_csrf_server_rendered_no_placeholder (pas de placeholder +
token du hx-headers == cookie csrf_token).

suite **1092/1092** · ruff OK · node --check vert · docs à jour
2026-10-02 08:45:27 -04:00
bruno 0bc74ad728 refactor: A28 TERMINÉ — board.py (2 101 L) → package 14 fichiers (v7.32.0)
FlowDeck CI / lint (push) Successful in 2m1s
FlowDeck CI / test (push) Successful in 15m17s
FlowDeck CI / docker (push) Canceled after 0s
Lot 4/4 de l'A28 (god files) : l'ancien app/routers/board.py (2 101
lignes, 53 routes) devient le package `app/routers/board/` :

- 12 modules de routes : pages 271 L (7 r.), page_api 229 (5),
  board_views 223 (8), page_ops 176 (3), sharing 175 (10), synced 144 (8),
  page_media 122 (4), import_ 85 (2), wiki 76 (2), library 66 (1),
  embed 64 (2), sync 51 (1)
- _common.py (878 L) : 23 helpers dont 4 async + les 4 constantes
  (STATUS_COLORS, STATUS_LABELS, AI_KEYWORD_COLORS, _REPO_REF_RE)
- __init__.py : __all__ complet — importateurs inchangés (api.py ×4
  top-level, webhooks top-level, dashboard ×5 lazy, tests ×4)

Preuve contractuelle : docs/openapi-v2.json régénéré = IDENTIQUE
byte-à-byte (509 chemins, ordre préservé).

Pièges rattrapés :
- constantes d'état oubliées dans _common à la 1ʳᵉ passe (F821 +
  ImportError au chargement) → ré-insérées avec les valeurs exactes
- docstring du header copié → F404 → slice [1:21]
- helpers `async def` non détectés par `def ` seul

A28 TERMINÉ en 4 lots : api_v2 (7.29.0), dashboard (7.30.0),
collections (7.31.0), board (7.32.0) — 0 changement d'URL sur les 4.

suite **1091/1091** · ruff OK · OpenAPI 509 identique · docs à jour
2026-10-02 08:27:57 -04:00
bruno adf56a2dd8 refactor: A28 lot 3 — collections.py (2 622 L) → package 13 fichiers (v7.31.0)
FlowDeck CI / docker (push) Successful in 1m54s
FlowDeck CI / lint (push) Successful in 1m55s
FlowDeck CI / test (push) Successful in 15m28s
Découpe par concern de l'ancien app/routers/collections.py (2 622 lignes,
53 endpoints / 52 fonctions) en package `app/routers/collections/` :

- 10 modules de routes : crud 337 L (6 r.), properties 322 (8),
  linked 286 (7), structure 267 (8), dashboard_views 214 (3),
  meta 197 (5), views 187 (6), pages 184 (4), data_api 122 (2),
  boards 61 (3)
- _common.py (220 L) : 8 helpers auth/permissions/validation
- _renderers.py (667 L) : 15 rendus HTML des vues + CHART_MAX_GROUPS
- __init__.py : ré-exports connus (_validate_page_properties pour
  automations ; _chart_values/_chart_aggregate/_fmt_number/_render_chart
  pour les tests) + __all__

Preuve contractuelle : docs/openapi-v2.json régénéré = IDENTIQUE
byte-à-byte (509 chemins, ordre préservé).

Pièges rattrapés :
- docstring d'origine conservée dans le header copié → F404
  (from __future__ après un statement) → slice [1:30]
- décorateurs empilés (view_collection ×2) : segment sans def →
  skip du 2e décorateur (53 endpoints = 52 unités)
- CHART_MAX_GROUPS hors détection des helpers (F821) → import ._renderers
- test_csp_no_cdn_and_vendor lisait collections.py → balayage du package

Reste A28 : board.py 2 101 L (lot 4).

suite **1091/1091** · ruff OK · OpenAPI 509 identique · docs à jour
2026-10-02 08:16:03 -04:00
bruno c0925e511b refactor: A28 lot 2 — dashboard.py (2 735 L) → package 10 fichiers (v7.30.0)
FlowDeck CI / lint (push) Successful in 1m56s
FlowDeck CI / docker (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 9m39s
Découpe par concern de l'ancien app/routers/dashboard.py (2 735 lignes,
63 routes) en package `app/routers/dashboard/` :

- 8 modules de routes : local_workspace 559 L (15 r.), pages_html 485 (6),
  account_settings 439 (16), workspace 321 (9), pages_api 240 (6),
  workspaces 131 (6), public 78 (1), account_api 77 (4)
- _common.py (774 L) : les 15 helpers top-level INTERCALÉS dans l'ancien
  fichier + état (logger, _VERSION, WORKSPACE_COOKIE)
- __init__.py : ordre d'enregistrement identique à l'origine, re-export
  complet (7 importateurs inchangés : main, board ×3, my_tasks,
  web_clipper, wiki, sites `_dash._render_blocks_public`, tests) + __all__

Preuve contractuelle : docs/openapi-v2.json régénéré = IDENTIQUE
byte-à-byte (509 chemins, ordre préservé).

Pièges rattrapés :
- segment décorateur sans sa fonction → assert `def in seg` + récupération
  git (corps perdus en silence à la 1ʳᵉ exécution)
- collision `settings` (section vs from app.config import settings →
  hasattr du fromlist) → renommée account_settings
- WORKSPACE_COOKIE utilisé sans import dans workspaces.py (F821)
- script __all__ mangeant la fin du fichier → __init__ réécrit

Reste A28 : collections.py 2 622 L, board.py 2 101 L (lot 3).

suite **1091/1091** · ruff OK · OpenAPI 509 identique · docs à jour
2026-10-02 07:47:41 -04:00
bruno 6a5fe0524a refactor: A28 lot 1 — api_v2.py (2 110 L) → package 14 fichiers (v7.29.0)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
Découpe par concern de l'ancien app/routers/api_v2.py (2 110 lignes,
115 routes) en package `app/routers/api_v2/` :

- 12 modules de routes : collections 566 L (23 r.), engagement 338 (21),
  workspaces 230 (9), templates_io 205 (9), webhooks 195 (8),
  identity 195 (7), views 164 (8), sharing 160 (8), properties 151 (7),
  planning 148 (7), projects 93 (4), admin 91 (4)
- `_common.py` : helpers partagés (_hash, _v2_rate_check)
- `__init__.py` : router = APIRouter(prefix="/api/v2") + include_router
  sur les routers de sections (sans prefix, tags « api-v2 »)

Preuve contractuelle : `docs/openapi-v2.json` régénéré = IDENTIQUE
byte-à-byte (0 changement de chemin/tag/operation_id). Seul importateur
(app/main.py : from app.routers.api_v2 import router) fonctionne via le
package. En-tête d'imports copié par module puis émondé par ruff --fix
(143 imports morts), I001 réordonnés.

Reste A28 : dashboard.py 2 735 L, collections.py 2 622 L, board.py 2 101 L
(même recette, lots suivants).

suite **1091/1091** · ruff OK · OpenAPI 509 identique · docs à jour
2026-10-01 23:26:45 -04:00
bruno 3bb8e87ef2 fix: A42 terminé — client httpx partagé par boucle (v7.28.0)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
- `app/services/http_client.py` : `async with shared_client(timeout=15)
  as client:` remplace les 49 créations `async with httpx.AsyncClient(`
  de 14 fichiers (gitea ×21, providers oidc/oauth ×11, calendar ×4,
  automations ×3…) — le pool de connexions est réutilisé au lieu d'être
  recréé à chaque appel. __aexit__ no-op (le client partagé ne se ferme
  pas à la sortie).
- Cache par (boucle d'event, kwargs) en WeakKeyDictionary : un
  AsyncClient n'est JAMAIS partagé entre deux loops (piège des tests
  « Event loop is closed ») — une boucle par test = client propre
  collecté avec la boucle. Clé = kwargs triés, repr() pour les valeurs
  non hashables (`headers=` dict → TypeError rattrapé par la suite).
- Laissés délibérément : github_adapter (transport MockTransport
  injecté), webhook_outbound (client « own_client » fermé par la
  fonction).
- Tests : `test_http_client_shared_and_loop_scoped` (réutilisation mêmes
  kwargs / cloisonné kwargs / cloisonné loop) ; le stub des webhooks
  patche aussi la fabrique `http_client.httpx` + purge du cache (avant :
  webhook_outbound.httpx patché mais la fabrique partagée créait un vrai
  client → réseau réel dans les tests).

suite **1091/1091** · ruff OK · docs à jour
2026-10-01 23:09:45 -04:00
bruno 069c438aae fix: A20 phase 2 — chart/leaflet vendorisés + connect-src fermé (v7.27.0)
FlowDeck CI / lint (push) Successful in 1m54s
FlowDeck CI / docker (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 10m41s
- Vendorisation : chart.js 4.5.1 + leaflet 1.9 (leaflet.js, leaflet.css,
  5 images marker/layer) vers static/js/vendor/ (déjà ignoré par eslint) ;
  les 3 URL CDN des vues chart/map (collections.py) pointent en local →
  la CSP n'a plus AUCUN hôte tiers dans script-src ni style-src.
- connect-src fermé : `'self' ws://{host} wss://{host}` — Host de la
  requête (uvicorn rejette déjà les Host invalides) + filtrage des
  caractères hors base URL. Le `https:` universel (canal d'exfil) et les
  ws:/wss: tout-hôtes disparaissent. Grep négatif : 0 fetch cross-origin
  côté front.
- Google Fonts : entrées CSP mortes (0 référence dans le code) retirées
  de style-src/font-src.
- img-src https: CONSERVÉ volontairement (unfurls YouTube/Vimeo… + tuiles
  OSM inénumérables) — ponytail: commenté dans security.py.

Tests : test_csp_no_cdn_and_vendor (CSP sans CDN/Google, connect-src
exact 'self' ws://testserver wss://testserver, 4 assets vendor 200,
source collections.py sans CDN) + test_view_chart_renders mis à jour
(chemin vendor). Suite complète 1090/1090 (1089 + 1).

Reste A20 : unsafe-eval (Alpine x-data + htmx hx-on/hx-vars = eval)
→ build @alpinejs/csp + couverture E2E des vues d'abord (même logique
que la décision A39).

suite **1090/1090** · ruff OK · docs à jour
2026-10-01 22:49:48 -04:00
bruno 45e59009c3 fix: A21 phase 2c — 190 routes hors loop, 86 % total (v7.26.0)
FlowDeck CI / lint (push) Successful in 1m55s
FlowDeck CI / test (push) Successful in 15m23s
FlowDeck CI / docker (push) Canceled after 0s
4 passes (283 → 93 routes async sur 667 = 86 % hors loop, avant 61 %) :

A. RACINE AUTH — `get_current_user` (auth/session.py) était `async def`
   SANS aucun await (cookie decode = synchrone) ; idem ses clones :
   `agent._current_user_id/_workspace_id/_current_admin` (34 sites) et
   `sso._require_admin` (corps 0 await, 6 sites) → `def` +
   47 `await` supprimés. Piège : 3 call sites passaient par l'alias `gcu`
   (grep littéral aveugle) — 8 tests en échec → corrigés.

B. Re-scan : 19 routes devenues SANS await → `def` (agent 8, sso 5,
   web_clipper 3, projects 2, auth 1…).

C/D. 155 routes dont les seuls awaits = `request.json()` / événements :
   - try/except `body = {}` → `Body(default={})` (même tolérance)
   - try/except `raise HTTPException(400)` → `Body(...)` REQUIS
     (422 FastAPI — aucun test ne couvrait le 400)
   - forme conditionnelle `request.json() if content-type else {}`
     (54 sites) → défaut `{}` (sans corps = `{}` dans les 2 cas)
   - `await fire_*` → `run_event_sync(...)` ; imports `Body` /
     `run_event_sync` ajoutés aux routers convertis

Reste async (93, justifié) : form/upload/file (22), réseau gitea/llm/oidc,
`_json_body` (9), 2 JSON inline en argument, 1 fallback logique
(capture_frontend_error), 1 lecture conditionnelle (web_clipper), mixtes.

suite **1089/1089** · ruff OK · docs à jour
2026-10-01 22:17:48 -04:00
bruno 8d0d69e7b8 fix: A27 lint terminé — eslint 0/0 (285 warnings nettoyés) (v7.25.0)
FlowDeck CI / test (push) Failing after 3h9m51s
FlowDeck CI / lint (push) Successful in 1m54s
FlowDeck CI / docker (push) Skipped
3 familles, 13 fichiers (+153/−167) :

1. no-empty ×70 = TOUS des `catch (x) {}` vides → `catch { /* volontaire */ }`
   (binding optionnel ES2019 + commentaire : passe no-empty ET
   no-unused-vars, zéro changement de comportement).

2. no-unused-vars ×171 :
   - bindings de catch inutilisés retirés (e/err/ex/e2/e3)
   - 24 lignes mortes déterministes, chaque suppression validée par assert
     sur le texte exact (`var self = this` ×8, `var lang`, `var acc`,
     `var today`, `var path/restored/files/resolved/items/clickEl`,
     `uid()`/`propName()` sans un seul appel, `.then` + `resolved++`
     compteurs jamais lus)
   - `/* exported */` sur les 10 fonctions appelées depuis les attributs
     HTML des templates (vérifiées par grep : 1 template chacune) :
     setActiveTab/kanbanBoard/filterSystem/sortSystem/newIssueForm/
     showNewIssue, importWizard, libraryPage, workspacesPage, settingsInit

3. no-undef ×44 = vrais globaux déclarés dans eslint.config.mjs
   (getSvgIcon = script inline de base.html, TextDecoder = API navigateur,
   Prism = CDN) + 2 vrais correctifs :
   - settings.js : `typeof toast === 'function'` = guard TOUJOURS faux
     (pas de toast global) → les toasts timezone/SAML ne s'affichaient
     jamais → `window.showToast` (2 sites)
   - local_workspace.js : `_wsInitData = window._wsInitData`
     (auto-affectation sans effet, global implicite) supprimé

eslint static/js : **0 erreur / 0 warning** (285 → 0) · node --check vert
sur tous les fichiers · suite **1089/1089** · ruff OK · docs à jour
2026-10-01 21:30:37 -04:00
bruno 103bc57418 fix: A27 phase 2c — database_table 1 314 L, extraction A27 terminée (v7.24.0)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
- `_database_table_scripts.html` → `static/js/database_table.js` (1 314 L).
  Le Jinja du bloc était confiné à la construction de l'objet de config
  (4 clés + `{% if collection_data %}`) → config JSON `#db-config`
  null-vs-objet : `new DBInstance(container, PAGE_COLLECTION_ID, DB_CONFIG)`
  remplace les 2 branches Jinja (le `else` était déjà un literal null).
- Loader DB_CONFIG : JSON.parse du bloc, `null` si absent (parité stricte
  avec le else d'origine) ; acrlade try corrigée par node --check avant
  commit.
- 2 tests adaptés (lisaient le template source → static/js/database_table.js)
  ; `FlowDeckDB` / `db-board` / `db-cal-grid` / `db-gallery` plus dans le
  HTML → asserts sur le JS extrait.

BILAN A27 : 11 874 L extraites en 4 phases (4 243 + 2 516 + 3 801 + 1 314),
inline 13 904 → 2 022 L (-85 %), 22 fichiers static/js/*.js, node --check
vert partout, eslint 0 erreur / 285 warnings. Reste : base 1 523 L
structurel ({% block %}/{% for %} — inline par nature), ~500 L de petits
blocs hors cibles, nettoyage des 285 warnings.

suite **1089/1089** · ruff OK · docs à jour
2026-10-01 21:10:16 -04:00
bruno 45917c194d fix: A27 phase 2b — +3 801 L extraits (recette config JSON) (v7.23.0)
FlowDeck CI / lint (push) Successful in 1m55s
FlowDeck CI / docker (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 37m14s
4 blocs interpolés extraits avec la recette de la 2a (config JSON inline +
JS statique, substitutions sur le CORPS du bloc) :
- local_workspace.html → local_workspace.js (2 031 L, lw-config :
  current_folder_id, workspace_id)
- settings.html → settings.js (1 093 L, st-config : avatar, user
  full_name/login/email, is_admin (bool), auth_method — 2 routes rendent ce
  template, expressions « or "" » préservées pour les valeurs Undefined)
- _page_editor_realtime.html → page_editor_realtime.js (531 L, rt-config :
  SELF id/login/full_name/color)
- board.html → board.js (146 L, bd-config : owner/repo/initial_view)

BONUS sécurité : les valeurs passent par |tojson (échappement JSON explicite)
au lieu d'être interpolées dans des strings JS. Tags : config JSON (nonce
conservé) + <script src> ?v={{ asset_version }} ; loaders JSON.parse en tête
(try/catch → {}). Correctif sur le loader (accolade try en trop, caught par
node --check avant tout commit).

Cumul A27 : 10 560 L extraites (13 904 → 3 344 restantes, -76 %).
Reste structurel : base 1 338 ({% block %}/{% for %}) + database_table 1 323
(if/else) + 279 warnings eslint (12 fichiers, 0 erreur).

suite **1089/1089** · ruff OK · node --check ×4 vert · docs à jour
2026-10-01 20:37:07 -04:00
bruno ee1d46e965 fix: A27 phase 2a — éditeur 2 516 L extrait via page-data JSON (v7.22.0)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
- `_page_editor_scripts.html` : le gros bloc interpolé (2 516 L) part vers
  `static/js/page_editor_scripts.js` — recette « config JSON » : les 8
  interpolations Jinja lisent `PD = JSON.parse(#page-data)`, bloc JSON qui
  EXISTAIT DÉJÀ juste avant le script (même ordre d'exécution), garde
  `__fdEditorScriptsLoaded` préservée, node --check vert.
- Route `view_page_root` : page_data enrichi de updated_at, created_at,
  user_id, is_shared (dérivé HOISTÉ : une seule expression sert le ctx ET le
  JSON) et clip_icon (macro fd_icon rendue côté serveur). workspace_key reste
  vide comme avant (jamais défini dans ce ctx → parité stricte).

8 tests adaptés à l'extraction (ils lisaient le template SOURCE) :
- test_ai_writing ×2 (+ helper _read_js), test_pwa_offline,
  test_v511 front_end_wired, test_v55 ×3 → lisent le JS extrait
- test_page_editor_renders_page_is_shared → parsing du JSON #page-data
  (`is_shared is True`) — la valeur sert toujours à la page

Cumul A27 : 6 759 L extraites (13 904 → 7 145 inline). Reste : local_workspace
2 031, base 1 523 (structurel {% for %}/{% block %}), database_table 1 323,
settings 1 093, realtime 531, board 146 ≈ 6 653 L + 120 warnings eslint.

suite **1089/1089** · ruff OK · docs à jour
2026-10-01 16:12:26 -04:00
bruno 587ec8d61b fix: A27 phase 1 — 4 243 L de JS inline extraites + eslint actif (v7.21.0)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
Extraction des 7 templates dont le JS n'est PAS interpolé Jinja → 9 fichiers
static/js/*.js (4 243 lignes, -30 % du JS inline : 13 904 → 9 661) :
- agent_panel_1/_2 (bloc de 1 788 L livré sur CHAQUE page), library (1 039),
  gitea_workspace (626), _icon_picker_1/_2, _ctx_menu, import, workspaces
- UN fichier par bloc : ordre/timing identiques (pas de defer, attributs
  conservés dont data-cfasync), cache-busting via ?v={{ asset_version }}
  (source unique A40), scripts externes = 'self' en CSP (pas de nonce requis)
- garde-fou : le script refuse tout bloc contenant {{ ou {%
- vérifs : node --check vert sur les 9, 0 script inline restant dans les
  cibles, suite complète 1089/1089

Lint (la moitié « ajouter les templates à eslint » de l'audit) :
- eslint.config.mjs existait (flat v9, sans dépendances npm) mais AUCUN
  binaire eslint n'était installé → npm i -g eslint
- `eslint static/js` → 0 erreur, 120 warnings (no-unused-vars 69,
  no-empty 36, no-undef 15) sur 8 fichiers = baseline à nettoyer
- les extraits sont couverts d'office par la config (static/js/**/*.js)

Reste A27 : blocs interpolés Jinja (page_editor 2 517, local_workspace 2 031,
base 1 523, database_table 1 323, settings 1 093, realtime 531 ≈ 9 661 L)
→ extraction en 2 temps (config JSON injectée + script statique).

suite **1089/1089** · ruff OK · docs à jour
2026-10-01 15:55:09 -04:00
bruno 7a38ddd0f6 test: A32 TERMINÉ — 6 routes Gitea stubbées + bug prod fd_icon (v7.20.0)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
Les 6 dernières routes d'A32 (api.py, gitea) avec stub de transport — zéro
réseau réel :

- _stub_gitea() : stubs manuels sur gitea_client.gitea (create_issue,
  update_issue, update_issue_labels, get_issue, get_issue_comments) avec
  ÉTAT MUTABLE PARTAGÉ — le handler PATCH re-fetch l'issue via get_issue,
  un canevas figé aurait masqué la mise à jour.
- POST /issues : carte INSÉRÉE sur le board (board seedé par endpoint) ;
  PATCH : colonne recalculée sans perdre la carte.
- GET /issues JSON + HTML : ?format=html requis (le segment /html ne fixe pas
  le paramètre, le handler le lit dans la query) ; stub qui lève → 404.
- POST /checklists + POST /checklist-items : lignes vérifiées en base,
  404 sans board ; cleanup (items → checklists).

BUG PROD corrigé (trouvé par le smoke HTML) : card_detail.html utilisait la
macro fd_icon SANS l'importer → UndefinedError → 500 systématique sur
GET /api/issues/...?format=html (seul rendu du template dans le code).
Fix : {% from '_icons.html' import fd_icon %}.

A32 COMPLET : plus aucun router « 0 test » (webhooks, notes, sidebar_config,
github_routes, library, api, dashboard, api_v2 tous couverts).

test_smoke_uncovered.py : 52 tests. suite **1089/1089** · ruff OK · docs à jour
2026-10-01 15:39:21 -04:00