1fedafa808f6280b13bdb0267bafdd63cc6eb1d0
100
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1fedafa808 | fix: sidebar Repository et pages forge — erreurs console, cache assets (v7.70.1) | ||
|
|
d95178156f | feat: sidebar Repository identique au Workspace-local, pages forge unifiees, GC miroirs vides | ||
|
|
9ea0cddf3c |
fix: CI verte — fuite du client HTTP mocké + rate limiter actif dans test_agent
Deux causes indépendantes faisaient échouer le job test de la CI (et seulement en CI : ni .env ni les mêmes ordres de chargement en local) : - test_agent_web_tools.mock_http patche http_client.shared_client pendant sa fenêtre d'exécution. Le premier import d'importers/url_fetch dans cette fenêtre fige la factory moquée dans le namespace du module → tous les appels fetch_url suivants du processus passaient par le handler mocké de l'autre test (« assert '…/post' == '…/page' » dans test_v56_import). url_fetch résout désormais le client à l'appel, et le mock_http restaure la vraie factory (référence figée au chargement du module) y compris sur url_fetch ; - test_agent.py posait RATE_LIMIT_ENABLED=false en env var, sans effet sur le singleton Settings déjà instancié : sous pytest -n auto, le worker dépassait le quota de 60 req/min et 8 tests recevaient des 429 (KeyError 'id' au passage). Le fixture désactive maintenant le limiter sur le singleton, comme les autres fichiers de tests. |
||
|
|
0218d8f5e5 |
fix: /local-workspace — chemin du header + clic sur un dossier du sidebar (v7.69.9)
Corrige deux régressions de /local-workspace :
- le chemin du header restait bloqué sur « Home / <workspace> » quel que
soit le dossier affiché : la route rend désormais breadcrumb_items
(Home / <workspace> / <dossier> / <sous-dossier>, niveaux cliquables,
collapse « … » au-delà de 4) et la navigation sans rechargement recalcule
le chemin via l'event flowdeck:breadcrumb-changed ;
- le clic sur un dossier du sidebar affichait TOUS les composants à la
fois : Alpine.data('wsInitData') retournait le même objet singleton, le
2e montage (navigation partielle) levait « Cannot redefine property:
\ » et initTree abandonnait, laissant tout le contenu au state
brut. La factory retourne désormais une enveloppe fraîche par montage
qui délègue à l'état réactif partagé. #lw-config est aussi relu à chaque
exécution (le 2e montage gardait le folder_id du 1er chargement).
Inclus également le travail en cours de l'arbre : Library (colonnes Last
visited/Source, ordre d'en-tête, favoris à icônes Workspace), Meeting
Notes (bloc, CSS, routes, docs), coloration de code hljs, badges
favori/publié dans l'arbre local-workspace, docs (DATA_MODEL,
architectures) et tests associés.
|
||
|
|
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 |
||
|
|
b9a776b626 |
fix: le favicon est enfin le vrai logo, plus un placeholder (v7.69.2)
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 |
||
|
|
674baf6954 |
fix: og:image et twitter:image en URL absolue (v7.69.1)
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 |
||
|
|
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). |
||
|
|
db6bf03a00 |
fix: icône du bouton « commenter » = bulle Feather + (message-square) (v7.66.0)
Chemin SVG dessiné à la main remplacé par le message-square éprouvé de Feather (viewBox 24) + un « + » centré dans la bulle — même famille de traits que les autres icônes outline de l'éditeur. Le test statique suit. Gates: pytest tests/ -q -n auto → 1386 passed (1 flake connu et non lié test_v55::TestOGParser::test_fetch_og_metadata_success : échoue parfois sous -xdist, passe seul et passe au run complet final) ; ruff OK ; e2e v766 (2) + v764 (1) → 3 passed sur :8081 (image flowdeck:v7.66.0 rebuild + redéployée). |
||
|
|
c031bf67de |
chore: ne pas versionner commit_msg.txt (fichier de message ajouté par erreur en v7.66.0)
Ajouté à .gitignore + retiré de l'index ; le message de commit de v7.66.0
(
|
||
|
|
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) |
||
|
|
033490c464 |
feat: menu contextuel de bloc refondu façon Notion — 10 actions, sous-menus et raccourcis (v7.61.0)
- panneau de gauche : recherche « Search actions... », section « Text », les 10 actions dans l'ordre Notion (Turn into, Color, Copy link Alt+⇧+L, Duplicate Ctrl+D, Move to Ctrl+⇧+P, Delete Del, Comment Ctrl+⇧+M, Suggest edits Ctrl+⇧+Alt+X, Ask AI Ctrl+J, Skills) + pied métadonnées (auteur / heure de dernière édition / mots-caractères du bloc) - panneau de droite au survol (fermeture différée) : Turn into avec ✓ du type actuel + Heading 1-4, listes, To-do, Toggle, Code, Quote, Callout, Block equation, Synced block, 2/3/4/5 columns (info-bulle « Create N columns of blocks ») ; Color avec Last used / Text color / Background color (10 couleurs nommées, ✓ courante, carré de couleur) ; Skills = compétences réelles de l'espace appliquées au bloc via l'IA - turnInto(idx,type,cols) : crée N colonnes, sélecteur de source pour les blocs synchronisés ; setBlockColor mémorise la dernière couleur (Ctrl+⇧+H) - nouvelles actions éditeur : commentBlock (sélection + tiroir), askAI (composeur sans effacer le bloc), suggestEdit / runSkill (IA, insertion sous le bloc), applyLastColor - raccourcis : handler clavier dédié (Ctrl+J, Ctrl+⇧+H, Ctrl+⇧+M, Ctrl+⇧+Alt+X, Alt+⇧+L, Ctrl+⇧+P, Ctrl+D sans double, Del seulement avec le menu ouvert, Échap) — les entrées Copy/Cut/Paste du menu sont retirées (Ctrl+C/X/V conservés) - styles : .bm-group, .bm-foot, .bm-check, .bm-current, .bm-dot, sous-menu 60vh, recherche focalisée à l'ouverture - tests : tests/test_v761_block_menu.py (9 tests, dont node : colonnes) + test_v7593 adapté ; suite complète 1362 passed / 0 failed, ruff 0, eslint 0 - livraison : VERSION + app/main = 7.61.0, OpenAPI 525 chemins, CHANGELOG |
||
|
|
f132885b08 |
feat: bouton + à gauche du handle de bloc — insère un bloc dessous (v7.60.0)
- nouveau bouton .block-insert-btn émis AVANT le handle (6 points) dans le wrapper de chaque bloc de premier niveau : clic → E.addAfter(idx) → addAt(idx+1) → bloc inséré sous le courant et focusé (mécanisme déjà en place, réutilisé tel quel) - position : left:-64px (gouttière .blocks-container = 64px), -56px dans le breakpoint où la gouttière passe à 32px ; apparaît au survol/focus comme le handle ; masqué avec lui sur écran étroit (2 breakpoints) - aria-label + title « Add block below » - tests : tests/test_v760_block_insert_btn.py (2 tests : ordre dans le wrapper, câblage E.addAfter, positions/masquages CSS) - livraison : VERSION + app/main = 7.60.0, OpenAPI 525 chemins, CHANGELOG |
||
|
|
a79bdc1641 |
fix: « Turn into » du menu contextuel ne fonctionnait pas (v7.59.3)
- cause : la garde de turnInto() faisait _BLOCK_TURN_TYPES.indexOf(type) sur
un tableau d'objets {id,name} → toujours -1 → retour immédiat après
l'ouverture du sous-menu, aucun changement de format
- correction : validation par id (_BLOCK_TURN_TYPES.some(t=>t.id===type))
- même fonction : convertir un texte en To-do / Toggle vidait le contenu
(exclusion par erreur) ; seuls les types sans champ texte (image, embed,
meeting, divider, table_of_contents, database, button, columns) le vident
- tests : tests/test_v7593_turn_into.py (4 tests, dont 1 comportemental node
qui exécute la vraie turnInto() extraite de la source)
- livraison : VERSION + app/main = 7.59.3, OpenAPI 525 chemins, CHANGELOG
|
||
|
|
a67bd252aa |
fix: aide (bouton ✕), Insert below qui perdait le format, listes numérotées invisibles (v7.59.2)
- aide : bouton ✕ en haut à droite du panneau (classe partagée settings-close, 44 px mobile) + helpInit.close() (retour en arrière, sinon /workspaces) ; .settings-panel en position:relative pour ancrer hamburger + ✕ au panneau - éditeur « Insert below » (/Write with AI) : insère le markdown source conservé à la génération (AIC._md) au lieu du texte extrait du HTML rendu (_resultText() reste seulement en repli) → titres/listes/gras conservés - listes numérotées : aucun numéro n'était affiché (CSS = retrait seul) ; le rang des blocs numbered_list contigus est calculé au rendu (data-num) et rendu via content: attr(data-num) — /numbered list et l'insertion agent affichent 1, 2, 3 - md2b : cases à cocher - [ ] / - [x] testées avant la branche des puces (aligné sur paste2b) + séparateur 1) accepté (CommonMark) - menu slash : requête tapée dans le bloc (/numbered list) répercutée dans le filtre du menu quand le focus n'a pas rejoint l'entrée - tests : tests/test_v7592_ui_fixes.py (5 tests, dont 1 comportemental node qui exécute le vrai md2b) ; ruff I001 préexistant sur test_agent.py - livraison : VERSION + app/main = 7.59.2, OpenAPI 525 chemins, CHANGELOG |
||
|
|
39d34ac866 |
fix(llm): Mistral 403 tier_not_allowed — défauts tous-plans + sonde chat + migration 36 (v7.59.1)
La clé fonctionne: /v1/models 200 + chat 200 (vérifié live). L'échec venait
du modèle: mistral-large-latest (défaut PROVIDERS + tête de liste) n'est pas
servi par les plans d'abonnement basiques → 403 code 1910 « tier_not_allowed »,
et le Test connection sélectionnait systématiquement ce premier candidat.
- PROVIDERS mistral default → mistral-small-latest (tous plans)
- PROVIDER_MODELS réordonnée: tous-plans d'abord (ordre du test de connexion)
- mistral ∈ _CHAT_VALIDATED_PROVIDERS: le fetch modèles ne garde que les
modèles réellement servis (46 listés → 25 utilisables avec la clé du compte)
- migration 36: default_model bloqué (large/pixtral-large) reset en base
Test live déployé: {ok:true, model:mistral-small-latest, reply:PONG,
verified:true} · tests/test_agent.py 53/53 · nouveau test d'ancrage
test_mistral_defaults_are_tier_safe
|
||
|
|
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) |
||
|
|
21b96afa12 |
merge: origin/develop → main (contenu déjà intégré, arbre identique)
Vérification : merge-tree propre, diff de l'arbre fusionné vs main vide —
les 4 fichiers de
|
||
|
|
16f73fe39e |
feat: Add plugins — catalogue on/off à effet réel (v7.58.0, phase 8/8)
- app/services/plugins.py + migration 35 : table plugins (slug, name,
description, enabled) pré-remplie avec 3 modules câblés — web-tools,
web-clipper, automations ; ligne absente = activé (défaut sûr)
- automations OFF → dépendance FastAPI posée à l'include_router dans main.py
(aucun router touché) → toutes les routes /workspace/automations* refusées +
garde de tick du scheduler en arrière-plan
- web-clipper OFF → GET /extensions et tout /api/v2/web-clipper/* refusés
- web-tools OFF → web_search et fetch_url retirés du schéma ET de execute()
via ToolRegistry._all() : le LLM ne les voit plus
- UI rendue côté serveur : global Jinja plugin_enabled(slug) — nav
« Extensions » / « Automations » en {% if %} (absentes du DOM), sections
conditionnées en x-show dans settings.html
- menu + : l'entrée « Add plugins » devient vivante (fini disabled:true) —
liste des 3 plugins avec bascule, GET/PATCH /api/agent/plugins[/slug]
(slug inconnu → 404, 401 sans session)
- tests : tests/test_v758_plugins.py (10 tests) — routes refusées (302 hors
/api, 404 JSON pour /api*), outils retirés, nav disparue, persistance,
câblage ; assertions disabled:true == 0 dans les tests des phases 1/3/4/5/7
- livraison : VERSION + app/main = 7.58.0, OpenAPI 525 chemins, CHANGELOG,
ROADMAP phase 8 cochée (menu + complet), avenant phase 8 (docs)
|
||
|
|
f8acb906e5 |
feat: connecteurs Discord/Telegram/MCP + outils MCP dynamiques au registre (v7.57.0)
- presets Discord / Telegram : colonnes kind + auth (bearer/bot/none) sur
agent_connectors (migration 34) ; Discord envoie le jeton préfixé dans
l'en-tête d'autorisation, Telegram le place dans l'URL via le substitut
{secret} remplacé à l'appel — jamais stocké en clair
- Teams : scope ChannelMessage.Read.All ajouté aux Graph scopes M365
- app/services/mcp_client.py : handshake initialize → notifications/initialized
→ tools/list → tools/call (JSON-RPC 2.0, réponse JSON ou premier data: d'un
text/event-stream), garde SSRF, outils cachés en base (tools_json) —
le bouton « Tester » rejoue le handshake
- ToolRegistry._all() merge le cache MCP à chaque run : outils exposés au LLM
sous mcp_<serveur>_<outil> avec leur inputSchema, dispatch tools/call ;
serveur désactivé → outil absent du schéma, échec réseau → ToolResult(error)
- menu + : champ Type dans le formulaire connecteur (preset URL + auth),
badge « · N outil(s) » sur la fiche d'un serveur MCP
- tests : tests/test_v757_mcp_discord.py (12 tests, 0 appel réseau réel) ;
suite complète 1331 verts, ruff 0, eslint 0
- livraison : VERSION + app/main = 7.57.0, OpenAPI 523 chemins, CHANGELOG,
ROADMAP phase 7 cochée, avenant phase 7 dans docs/V74_Agent_Plus_Menu.md
|
||
|
|
1d7750bda0 |
feat: connecteurs Google + Microsoft 365 (OAuth2 PKCE) — phase 6/8 (v7.56.0)
- app/services/oauth_connectors.py : flow OAuth2 complet PKCE (S256) pour
2 fournisseurs décrits par 1 dict — Google (Drive/Gmail/Calendar en lecture
seule) et Microsoft 365 (Graph Files.Read / Mail.Read / Calendars.Read) ;
begin() = URL d'autorisation + state + code_verifier, complete() = échange du
code, access_token() = refresh automatique (60 s de marge, refresh_token
conservé si absent de la réponse), api_get() = path absolu refusé + validation
SSRF + borne 20 000 car.
- Tokens chiffrés Fernet en réutilisant calendar_sync._encrypt_tokens (zéro
dépendance) dans la table connector_tokens (migration 33, PK (kind, user_id)).
- 4 routes /api/agent/connectors/oauth/{kind}/… : status, authorize (cookies
d'état HttpOnly 10 min, retour same-origin validé), callback (GET safe, state
comparé en temps constant, tokens stockés puis cookies purgés, redirection
?oauth=connected / ?oauth_error=), disconnect. OpenAPI 523 chemins.
- Config + .env.example : GOOGLE_CLIENT_ID/SECRET, MS_CLIENT_ID/SECRET (vidés =
« non configuré »), redirect URI dérivé d'APP_BASE_URL.
- Catalogue : google/ms365 en natifs avec badge connecté/non connecté ;
connector_fetch et Tester passent par l'API du fournisseur avec le token de
l'utilisateur (user_id transmis par l'outil LLM).
- Menu + : « Se connecter » / « Déconnecter » sur la fiche, toast au retour du
flux (URL nettoyée par history.replaceState). État dans la fiche du menu
plutôt qu'une page dédiée.
- Tests : tests/test_v756_oauth_connectors.py (13), 0 appel réseau réel
(_post_form / _api_get monkeypatchés) — state forgé refusé sans échange,
tokens chiffrés en base, refresh, URL absolue refusée, 401/404, câblage menu ;
test_v755 adapté (5 natifs). Suite complète 1319 verts (-n auto), ruff 0,
eslint 0 erreur (19 warnings préexistants hors fichiers touchés).
|
||
|
|
18c72d77fe |
feat: menu + — Connecteurs (socle) — phase 5/8 (v7.55.0)
- Table agent_connectors (migration 32) : colonnes plates name, url, secret_encrypted, enabled, status, detail — config_json remis (YAGNI, les scopes OAuth des phases 6-7 ajouteront le leur). - app/services/connectors.py : 1 fichier au lieu du package connectors/ — 3 natifs (gitea, github, web) servis à la volée avec statut sans réseau, CRUD des personnels (URL validée par _validate_url = garde SSRF, clé chiffrée Fernet et jamais renvoyée — seul has_secret), probe() qui persiste status/detail, connector_fetch() borné à 20 000 car. - API /api/agent/connectors : GET, POST (400 URL privée), PATCH, DELETE, POST /connectors/probe — 401 sans session, CSRF global. OpenAPI 519 chemins. - Outil LLM connector_fetch (26e outil) : un seul outil qui dispatche vers Gitea/GitHub/web/personnalisé (id ou nom ou kind + path + query) au lieu d'un outil par connecteur ; désactivé/inconnu = erreur outil, jamais de run cassé. - Menu + : section « Connecteurs » (catalogue avec badge ✓/✗/⚠/?), fiche par connecteur (Tester, Activer/Désactiver, Supprimer — masqués pour les natifs), formulaire « Ajouter un connecteur personnalisé » (clé en type=password). - Tests : tests/test_v755_connectors.py (13) — 3 natifs, roundtrip Fernet de la clé, 5 URLs refusées (localhost, 127.0.0.1, metadata cloud, ftp:, file:), toggle persisté, 401, probe OK/erreur, outil complet, câblage menu ; test_v751/v753/v754 adaptés (1 seule section « bientôt » = plugins). Suite complète 1306 verts (-n auto), ruff 0, eslint 0 problème. |
||
|
|
64c85caffc |
feat: menu + — Mémoire de l'agent — phase 4/8 (v7.54.0)
- Le moteur n'envoie jamais l'historique (system + objectif courant seulement) : chaque run repartait de zéro. Nouveau service app/services/agent_memory.py — une ligne résumé par conversation (table agent_memory, migration 31), écrite en fin de run et ré-injectée au contexte du run suivant. - context_for() : bloc « ## Mémoire de la conversation », budget 4000 car. (troncature gauche + marqueur). remember() : 1 note/échange (180 → 700 car.), 20 notes max, upsert, erreurs journalisées — un run ne peut pas échouer à cause de la mémoire. Toggle OFF = ni lecture ni écriture. - Toggle « Mémoire » dans le menu + : entrée racine à l'état réel (🧠/💭 + libellé), PATCH /api/agent/conversations/{id} avec memory_enabled ajouté au whitelist, état lu à l'ouverture et à la création, défaut AGENT_MEMORY_DEFAULT (settings.agent_memory_default) + memory_enabled dans la réponse de création. - Décision : mémoire d'espace (lignes workspace_id NULL) remise — YAGNI, rien ne l'écrivait ; noté ponytail: dans le service avec le chemin de montée. - Tests : tests/test_v754_memory.py (8) dont 2 runs réels du moteur (FakeLLM capture le prompt : mémoire présente ON / absente OFF) ; test_v751 et test_v753 adaptés (2 sections « bientôt » restantes). Suite complète 1293 verts (-n auto, 2e run ; 1 échec intermittent sur test_v56_import URL au 1er run, vert isolé + vert au run suivant = flake préexistant), ruff 0, eslint 0 problème. OpenAPI 516 chemins / 7.54.0. |
||
|
|
a68e8848ea |
feat: menu + — Design System – Canevas — phase 3/8 (v7.53.0)
- Section « Design System – Canevas » : liste des canevas (intégrés +
personnels, GET /board/api/page-templates) puis page de détail avec
« Créer une page à partir du canevas », « Insérer dans le document ouvert »
(désactivé sans document ouvert) et « Enregistrer le document ouvert comme
canevas ».
- GET /board/api/page-templates/{id}/blocks (nouvelle route) : blocs d'un
canevas — intégré via template_id=0&key=, sinon id ; 404 clé/id inconnu.
Insertion côté front via ensureBlockIds() global + concat dans E.blocks
+ autoSave/render.
- Canevas « Design System » ajouté aux builtins (callout tokens, TOC, toggles
composants, grille, checklist revue UI, citation).
- Fix : le front du panneau appelait /api/page-templates (404 silencieux) —
le router est sous prefix=/board → /board/api/page-templates.
- Piège : pour /use la clé builtin se passe DANS LE BODY (body.key), pas en
query string — test garde-fou ajouté.
- Tests : tests/test_v753_canvases.py (5) + test_v751 adapté. Suite complète
1285 verts (-n auto), ruff 0, eslint 0 problème. OpenAPI 516 chemins.
|
||
|
|
18a2d45d46 |
feat: menu + — Compétences-skills « Gérer » + « Parcourir » — phase 2/8 (v7.52.0)
- Sous-menu « Compétences-skills » : 11 skills builtin + skills enregistrés,
libellés affichés « Deep research » (/research) et « Skill-creator »
(/create-new-skill) via FD_SKILL_ALIAS (slugs intacts) ; le clic épingle le
skill au contexte, même chemin que la palette /.
- « Gérer les compétences » : section du menu + formulaire intégré (nom,
description, prompt) — création, édition, suppression (chip épinglé nettoyé),
export JSON téléchargé, import fichier avec overwrite (pas de doublon).
- « Parcourir les compétences » : section galerie filtrable (nom/description/
slug), installation en 1 clic (installGallerySkill réutilisé).
- PATCH /api/agent/skills/{skill_id} créé : édition partielle, whitelist de
colonnes, 400/401/404/409. OpenAPI régénéré : 515 chemins, version 7.52.0.
- En-tête ← + titre de section dans le menu (navigation à 3 niveaux).
- Fix : épingler un skill enregistré n'efface plus le message déjà tapé
(_pinDbSkill ne vide le composer que s'il est vide).
- Tests : tests/test_v752_skills_menu.py (7) + test_v751 adapté. Suite
complète 1280 verts (-n auto), ruff 0, eslint 0 problème.
|
||
|
|
f271ac9b7a |
feat: menu + de l'assistant en hub de contexte — phase 1/8 (v7.51.0)
- Bouton + : menu à sections (fichiers/répertoires, compétences-skills,
connecteurs, Design System – Canevas, Add plugins, Mémoire on/off).
Sections pas encore livrées affichées « bientôt (phase N) » mais désactivées,
navigation clavier ↑/↓/Entrée/Échap, focus visible, la frappe referme le menu.
- Parcours « Parcourir… » : un niveau par appel via GET /api/nav/menu
(contrat : dossier = icon 'folder'), fil d'Ariane cliquable, épingle de dossier
via la ligne « 📌 Épingler le dossier ».
- Jeton folder:<id> résolu par ContextBuilder._single_folder() : titre du dossier
+ documents directs, budget ~12k caractères (marqueur « tronqué »), enfants
directs seulement.
- « Rechercher… » conserve l'ancien sélecteur de mentions (@ inline + recherche
par nom) : régression zéro sur le chemin existant.
- Tests : tests/test_v751_plus_menu.py (7 tests). Suite complète 1273 verts
(-n auto), ruff check app tests propre, eslint static/js 0 erreur.
- Docs : CHANGELOG, ROADMAP (phase 1 cochée), docs/V74_Agent_Plus_Menu.md statut.
|
||
|
|
f006880394 |
docs: plan par phases du menu + de l'assistant + design V74
- ROADMAP : section « v7.51.0 → v7.58.0 — Menu + de l'assistant » (8 phases : menu hub/fichiers-répertoires, compétences, canevas, mémoire, connecteurs socle, Google+M365, Discord/Telegram/MCP, plugins), état des lieux lu dans le code, aucun case cochée. - docs/V74_Agent_Plus_Menu.md : maquette, découpage front/back, modèle de données, sécurité (SSRF, secrets Fernet, CSRF), décisions D1-D7, pièges repo, critères d'acceptation par phase. |
||
|
|
81746d9440 |
fix: peek de Library nu comme local-workspace - iframe pleine (v7.50.2)
.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. |
||
|
|
f53205bc45 |
fix: peek des bases hydrate FlowDeckDB + Open de ligne, tableau pleine largeur (v7.50.1)
- 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. |
||
|
|
3706689eca |
fix: side peek = document seul en edition + bouton Open calendrier/Kanban (v7.50.0)
- 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.
|
||
|
|
7dbaefb381 |
fix: page de base - titre au-dessus du tableau, tableau au ras du sidebar (v7.49.2)
- .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.
|
||
|
|
15b6b163f4 |
fix: tableau des bases aligne sur le titre (v7.49.1)
.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. |
||
|
|
2e5efcfe07 | chore: retire commit_msg.txt embarque par erreur | ||
|
|
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. |
||
|
|
b2ea8ec537 | docs: suivis v7.45.3 listés au ROADMAP (CLS library, fenêtre settings, topbar swappée, fd-navigating morte) | ||
|
|
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
|
||
|
|
de751ffe35 |
feat: A20 TERMINÉ — Alpine en build CSP, unsafe-eval retiré de la CSP (v7.43.0)
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). |
||
|
|
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 |
||
|
|
3a74ea8bbd |
fix: A35 TERMINÉ — drift Python 3.12→3.13 aligné, rebuild validé (v7.35.0)
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 |
||
|
|
13dc8fdaad |
fix: A38 phase 2 — 0 doublon de fonction globale + garde-fou (v7.34.0)
- 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 |
||
|
|
770fdc2b68 |
fix: A43 TERMINÉ + A38 phase 1 — CSRF rendu côté serveur, helper unique (v7.33.0)
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
|
||
|
|
0bc74ad728 |
refactor: A28 TERMINÉ — board.py (2 101 L) → package 14 fichiers (v7.32.0)
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 |
||
|
|
adf56a2dd8 |
refactor: A28 lot 3 — collections.py (2 622 L) → package 13 fichiers (v7.31.0)
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 |
||
|
|
c0925e511b |
refactor: A28 lot 2 — dashboard.py (2 735 L) → package 10 fichiers (v7.30.0)
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 |
||
|
|
6a5fe0524a |
refactor: A28 lot 1 — api_v2.py (2 110 L) → package 14 fichiers (v7.29.0)
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 |
||
|
|
3bb8e87ef2 |
fix: A42 terminé — client httpx partagé par boucle (v7.28.0)
- `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 |
||
|
|
069c438aae |
fix: A20 phase 2 — chart/leaflet vendorisés + connect-src fermé (v7.27.0)
- 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
|
||
|
|
45e59009c3 |
fix: A21 phase 2c — 190 routes hors loop, 86 % total (v7.26.0)
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
|
||
|
|
8d0d69e7b8 |
fix: A27 lint terminé — eslint 0/0 (285 warnings nettoyés) (v7.25.0)
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
|
||
|
|
103bc57418 |
fix: A27 phase 2c — database_table 1 314 L, extraction A27 terminée (v7.24.0)
- `_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
|
||
|
|
45917c194d |
fix: A27 phase 2b — +3 801 L extraits (recette config JSON) (v7.23.0)
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
|
||
|
|
ee1d46e965 |
fix: A27 phase 2a — éditeur 2 516 L extrait via page-data JSON (v7.22.0)
- `_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
|
||
|
|
587ec8d61b |
fix: A27 phase 1 — 4 243 L de JS inline extraites + eslint actif (v7.21.0)
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
|
||
|
|
7a38ddd0f6 |
test: A32 TERMINÉ — 6 routes Gitea stubbées + bug prod fd_icon (v7.20.0)
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
|
||
|
|
113374e499 |
test: A32 phase 2h — dashboard bloqué : 44/44 routes à 0 ref (v7.19.0)
+5 routes dashboard (fichier test_smoke_uncovered.py à 49 tests) :
- Members POST/PUT/DELETE : invitation de soi-même dans un workspace dédié
(_own_workspace), rôle admin relu en base, membre supprimé (COUNT=0).
Quirk documenté : les retours tuple des routes (`{"error": ...}, 400`)
sont sérialisés FastAPI en tableau + 200 → assert sur `[0]["error"]`.
- upload-folder : validations SEULES (structure absente → 400 « No
structure provided », JSON cassé → 400 « Invalid structure JSON ») —
zéro fichier écrit, workspace dédié nettoyé.
- convert-to-database : collection + propriété title + vue table + page en
content_format='collection' VÉRIFIÉS en base, 404 page inconnue,
cleanup dans l'ordre FK (pages avant collections — IntegrityError corrigée).
Recoupement final : scan des 44 routes strictement à 0 ref de dashboard.py →
TOUTES exercées. Les 19 résidus du scan sont des faux positifs (paths en
f-string dans les tests : /api/workspace/1/…, f"/api/pages/{id}/…", …)
rapprochés manuellement un par un.
Reste A32 : les 6 routes Gitea d'api.py (issues ×4, créations checklists)
→ stub de transport httpx (effort S).
suite **1086/1086** · `ruff check app tests` OK · docs à jour
|
||
|
|
0cb476e336 |
test: A32 phase 2g — dashboard +13 routes, cycles items/tags (v7.18.0)
Cumul dashboard : 43 → 56 des 63 routes. 4 nouveaux tests (fichier à 46) :
- GET /gitea-workspace : page HTML (200 ou redirection propre)
- workspace/projects GET+POST : shape {builtin, gitea, github} avec
github == [] ; projet créé RETROUVÉ dans builtin ; quirk « error » sans nom
; nettoyage (DELETE page)
- Cycle items local-workspace (5 routes) : POST création (titre relu),
PUT rename (relu en base), PUT move, DELETE soft-delete (deleted_at relu),
POST restore (deleted_at NULL relu) — nettoyage finally
- Cycle tags d'item (5 routes) : POST (urgenta32 lowercasé), tags de l'item,
liste workspace, search (shape), suppression vérifiée. Utilisateur DÉDIÉ +
workspace créé dans le test (le endpoint /api/local-workspace/tags exige un
workspace actif : fallback « premier workspace du user » — on n'attache pas
ce workspace à l'utilisateur fixture partagé), tout est nettoyé.
Reste A32 : dashboard 7 routes (members invite/role/unsubscribe,
upload-folder, convert-to-database) + 6 routes Gitea d'api.py (stub httpx).
suite **1083/1083** · `ruff check app tests` OK · docs à jour
|
||
|
|
2339fa2586 |
test: A32 phase 2f — dashboard +7 routes, garde-fous A16 (v7.17.0)
Cumul dashboard : 36 → 43 des 63 routes. 4 nouveaux tests (fichier à 42),
centrés sur les garde-fous A16 :
- GET /api/files/{ws}/{path} : traversal encodé %2e%2e%2f → 403
« Path traversal denied » (décodé par Starlette puis bloqué par resolve) ;
inexistant → 404 ; vrai fichier écrit dans le data_dir de test →
200 + octets exacts, nettoyé en finally
- GET /api/pages/{id}/download : page markdown → 404 « No downloadable file »
(pas de 500) ; page « file » avec chemin ../ sortant de la racine →
jamais 200 ; file-content → 404/415 sans fuite
- GET /api/local-workspace/page-content/{id} : contenu + format relus,
404 sur id inconnu
- GET /api/avatar/{id} : 302 + Location avec follow_redirects=False
(AUCUNE requête réelle vers l'URL externe), 404 sans avatar
- GET/POST /api/collections/{id}/table-data|pages : 404 inconnu, shape,
ligne créée retrouvée dans table-data, nettoyage finally
Reste A32 : dashboard 20 routes (upload/local-workspace items/members/
projects/HTML gitea) + 6 routes Gitea d'api.py (stub transport httpx).
suite **1079/1079** · `ruff check app tests` OK · docs à jour
|
||
|
|
8b48dbdd4b |
test: A32 phase 2e — dashboard +9 routes, comptes A2/A3 (v7.16.0)
Cumul dashboard : 27 → 36 des 63 routes. 6 nouveaux tests (fichier à 38) :
- /accounts + /accounts/settings : 200 HTML et « password_hash » ABSENT du
rendu (whitelist A2 vérifiée côté page)
- PUT /api/user/profile : persistance relue en base, restauration finally
- PUT /api/user/password : 403 « current password is incorrect » (A3 — la
session seule ne change pas le mdp) + quirk assumé documenté : la longueur
est validée AVANT l'auth et répond 200 + message
- POST /api/user/token : format fd_ + 64 hex ; ligne user_tokens nettoyée
- DELETE /api/user/forge/{provider} : {"status": "ok"}
- PUT /api/settings/account : full_name/email persistés + 400 sur mdp court,
restauration finally
- POST /api/workspaces/1/select : Set-Cookie flowdeck_workspace vérifié ;
GET /api/local-workspace/breadcrumb : shape liste
Reste A32 : dashboard 27 routes (fichiers/avatars/local-workspace/collections)
+ 6 routes Gitea d'api.py (stub transport httpx).
suite **1075/1075** · `ruff check app tests` OK · docs à jour
|
||
|
|
360c705fd4 |
test: A32 phase 2d — dashboard +10 routes couvertes (v7.15.0)
Scan strict dashboard.py : 44 routes à 0 référence stricte sur 63. 10
couvertes cette passe (test_smoke_uncovered.py → 32 tests) :
- Tags CRUD complet : POST (nom lowercasé `SmokeTag` → `smoketag`), GET all
(présent), PUT couleur (relue), DELETE (absente de la liste ensuite)
- Vie d'une page : GET /api/pages/{id}/content (contenu seedé relu) →
PUT rename (ok + **400 titre vide** + titre relu en base) →
POST trash (parent_section='Trash' + deleted_at RELUS en base) ;
nettoyage en finally
- GET /api/sidebar/workspace-tree : 200 HTML, fragment « No pages yet »
(pas de cookie workspace)
- POST /api/settings/avatar-color : couleur relue SUR L'UTILISATEUR DE LA
SESSION (pas LIMIT 1), avatar_color/avatar_url d'origine restaurés
- GET /api/workspace/1/members : shape {"members": [...]}
Helper _seed_page : les colonnes par défaut sont surchargeables (content=)
pour les seeds à contenu.
Reste A32 : dashboard 34 routes à 0 ref (fichiers/avatars/imports…) +
6 routes Gitea d'api.py (stub transport httpx).
suite **1069/1069** · `ruff check app tests` OK · docs à jour
|
||
|
|
0698645dbd |
test: A32 phase 2c — api_v2 : les 5 routes à 0 ref couvertes (v7.14.0)
Scan strict des 115 routes api_v2.py contre tous les tests (chaîne de chemin littérale) → 5 routes sans AUCUNE référence, toutes couvertes maintenant : - POST /properties/evaluate-formula : 200 + shape, 400 sans expression. Le moteur renvoie « 1 + 2 » tel quel aujourd'hui → le smoke valide le câble (bearer, Body param, parse), pas le moteur (réalm de ses propres tests). - POST /properties/compute-rollup : 400 « collection_id required », 401 sans bearer. - GET /admin/audit-logs : portail admin VÉRIFIÉ — l'attendu est calculé depuis /users/me (le tout premier utilisateur d'un worker est admin : état non contrôlable depuis un test isolé), + token scope admin → 200 + logs. - GET /webhooks/events : catalogue non vide + wildcards * / page.*. - POST /webhooks/verify-signature : valid=True avec sign_payload() (le même helper que le serveur), False avec signature bidon. test_smoke_uncovered.py : 27 tests. Reste A32 : dashboard 17/63 + 6 routes gitea d'api.py (stub transport). suite **1064/1064** · `ruff check app tests` OK · docs à jour |
||
|
|
b2e38aece7 |
test: A32 phase 2b — api.py 3 → 16/22 routes couvertes (v7.13.0)
+6 smokes dans test_smoke_uncovered.py (22 tests au total dans le fichier) : - board-config GET/POST : défauts 5 colonnes sans board, création puis relecture du roundtrip (seed via l'endpoint lui-même, pas de SQL brut) - col-mapping POST/DELETE : 404 sans board, upsert label vérifié, suppression - card POST : 404 sans board, ok avec - collaborators GET : gitea.get_collaborators STUBBÉ (zéro accès réseau réel) - frontend-error(s) : capture, JSON invalide → ignored, DÉDUP d'une erreur répétée (count=2), lecture qui purge (cleared=true puis 0) - checklist mutations : PATCH item (checked/content relus EN BASE), DELETE item, DELETE checklist (COUNT=0) — seed + cleanup en finally Reste api.py : 6 routes Gitea (issues ×4 + créations checklists owner/repo) → stub de transport httpx (phase suivante). Reste global : dashboard 17/63, api_v2 50/115. suite **1059/1059** (236 s) · `ruff check app tests` OK · docs à jour |
||
|
|
df9a269d76 |
test: A32 phase 2a — library 10/10 + 2 routes fantômes supprimées (v7.12.0)
- tests/test_smoke_uncovered.py : +6 tests pour library.py (1/10 → 8 routes
couvertes) : les 5 listes en boucle (recents/favorites/published/private/
workspace → 200 + items), /private avec page seedée retrouvée, /children/{id}
avec parent/enfant seedés, /repository vide ET clé (string, aucun réseau),
non-régression 404 sur les routes supprimées.
- DÉCOUVERTE (les smokes l'ont prouvé) : `/api/library/local-workspace-children`
renvoyait un 500 systématique (test vert → ASGI double response.start) et
`/api/library/local-workspace` 500 dès qu'un workspace existe — les deux
lisaient `local_workspace_items`, table AUCUNEMENT créée dans le codebase
(0 CREATE TABLE) et sans 1 seule référence front. Supprimés avec
`library._format_size` devenu mort (la copie de dashboard.py est inchangée).
`local_workspace_items` : 0 occurrence restante dans app/.
- helper `_seed_page` minimal (workspace NOT NULL inclus) + nettoyage en
finally (pas de pollution des autres tests).
suite **1053/1053** (229 s) · `ruff check app tests` OK · docs à jour
restent phase 2b : api.py 3/23, dashboard.py 17/63, api_v2.py 50/115
|
||
|
|
da7326ffde |
test: A32 phase 1 — 4 routers à 0 test couverts (10 smokes) (v7.11.0)
tests/test_smoke_uncovered.py — un smoke par route des 4 routers qui n'avaient
AUCUN test :
- webhooks.py 3/3 : réception sans secret → {"status":"ok"} ; HMAC faux → 401
(secret piloté par monkeypatch, déterministe quel que soit le .env) ;
register sans secret → 400 AVANT tout appel réseau ; status avec
gitea.list_webhooks stubbé → {"registered": False} (zéro réseau réel)
- notes.py 2/2 : GET HTML + roundtrip POST→GET (upsert persisté en base,
échappement HTML vérifié : <b> et non <b>)
- sidebar_config.py 2/2 : GET défauts ; PUT persisté puis RELU depuis
users.sidebar_config ; 400 sans config ; remise en état en fin de test
- github_routes.py 2/2 : status {"linked": False} ; disconnect {"status": "ok"}
Reste (A32 phase 2) : quasi nuls — library 1/10, api 3/23, dashboard 17/63,
api_v2 50/115 → même recette, fixture client existante.
suite **1047/1047** · `ruff check app tests` OK · docs à jour
|
||
|
|
3a1276596c |
fix: A21 phase 2b — run_event_sync + 15 routes api_v2 en def (v7.10.0)
- `run_event_sync(coro)` (app/services/automations.py) : exécute une coroutine
d'événement depuis un handler synchrone — `asyncio.run` sur une boucle
dédiée dans le worker threadpool : le worker est bloqué, JAMAIS la boucle
d'event, et la réponse n'est produite qu'une fois l'événement terminé
(déterministe, équivalent sémantique de l'await). ponytail: les clients
httpx sont créés à chaque appel partout → aucun lien de boucle ; sinon
run_coroutine_threadsafe + boucle du lifespan.
- 15 routes api_v2 dont les SEULS awaits étaient `request.json`,
`_fire_event`, `fire_published`, `fire_unpublished` →
`Body(default={})` + `run_event_sync(...)` + conversion en `def` (script
: wrapping par appariement de parenthèses chaîne-aware, assert de flip
« plus aucun await »).
- api_v2 : **111/115 routes hors loop**. Les 4 restantes ont de vrais awaits
réseau et restent async volontairement : import_csv_v2 (multipart),
project_tree_v2 (gitea), test_webhook_v2 (delivery), retry_webhook_deliveries.
- Repo-wide : 403 routes sync (hors loop) / 260 async (phase 2c).
tests : ciblé public_api_v2 + v65 + webhooks_v2 + audit = 90/90 (les webhooks
prouvent la détermination de run_event_sync) ; suite complète **1037/1037**
(228 s) · `ruff check app tests` OK · docs à jour
|
||
|
|
07904f05e5 |
fix: A21 phase 2a — api_v2 : body JSON en paramètre, 36 routes hors loop (v7.9.0)
- Les 36 routes d'api_v2 dont le SEUL `await` était
`body = await request.json()` (bloc try/except uniforme) → paramètre FastAPI
`body: dict = Body(default={})` + conversion en `def` → threadpool :
toute leur séquence SQLite quitte l'event loop.
- Équivalences vérifiées avant engament (probe FastAPI) :
· corps absent → `{}` (identique à l'ancien try/except)
· JSON invalide → 422 (avant : avalé comme `{}` — 422 est plus juste)
· zéro `body[...]=` / setdefault / update dans api_v2 → défaut partagé
jamais muté
- verify_webhook_signature (signature multi-ligne) traitée à la main.
- Piège courant évité : première version du script supprimait 5 lignes au
lieu de 4 (slice m-1:m+4) → fichier restauré depuis git, slice corrigée,
0 ligne perdue (diff logique +39/-183).
api_v2 : 96/115 routes hors loop (60 phase 1 + 36 ici) ; 19 async restantes
(fire_event, request.form, gitea/webhooks) = phase 2b.
suite **1037/1037** (242 s) · `ruff check app tests` OK · docs à jour
|
||
|
|
224bda74d5 |
fix: A21 phase 1 — 352 routes async sans await → threadpool (v7.8.0)
- Conversion `async def` → `def` de TOUTES les routes dont le corps ne contient ni `await`, ni `async with`, ni `async for`, ni `asyncio` (scan automatique corps par corps sur app/ : 352 converties, 0 dangereuses, vérifié `asyncio`/`run_coroutine`/`.result()` absents). FastAPI exécute ces handlers dans son threadpool → tout leur SQLite (`get_conn()` + `conn.execute`) quitte l'event loop, sans changer une ligne de logique. - Répartition : api_v2 60, dashboard 40, collections 25, board 23, workspace 19, wiki 17, permissions 14, api 14, main.py 6, + 35 fichiers. - Les 4 routers prioritaires de l'audit sont couverts par ce lot : api_v2 60 + dashboard 40 + collections 25 + board 23 = 148 conversions (le reste de leurs routes attend la phase 2 : elles ont de vrais `await`). - Reste (phase 2) : les 311 routes avec de vrais `await` → enrouler les blocs DB dans `await anyio.to_thread.run_sync(...)` ; pas de wrapper partagé livré (rien ne l'appellerait — YAGNI jusqu'au premier usage). suite **1037/1037** (233 s) · `ruff check app tests` OK · docs à jour |
||
|
|
c718fe06de |
fix: A20 (partiel) — CSP nonce par requête, unsafe-inline sort de script-src (v7.7.0)
- ContentSecurityPolicyMiddleware : nonce aléatoire par requête dans la
ContextVar `CSP_NONCE` (posée avant `call_next` → visible des templates),
`script-src 'self' 'unsafe-eval' 'nonce-…'` — plus aucun script inline
sans nonce ne tourne (fin des XSS injectés en JS)
- 38 tags `<script>` des templates : `nonce="{{ csp_nonce() }}"` (passage
scripté, vérifié : 0 restant) ; `LOCAL_LOGIN_HTML` (constante de module) :
helper `_with_nonce()` au rendu ; collections.py : 3 scripts Python
(chart/form/map) noncés
- `<meta name="htmx-config" content='{"inlineScriptNonce": …}'>` dans base.html
: htmx ré-injecte les <script> des réponses boostées avec le bon nonce
- `script-src-attr 'unsafe-inline'` : les 74 handlers `onclick=` inline
restent couverts (le nonce les aurait désactivés aussi)
- chart.js (cdn.jsdelivr.net) et leaflet (unpkg) ajoutés à script-src/style-src
: vues chart/map déjà BLOQUÉES par la CSP depuis toujours
(commentaire ponytail: upgrade = vendoriser puis retirer les hôtes)
- reste d'A20 : unsafe-eval (Alpine x-data → @alpinejs/csp), externalisation
JS (A27), resserrer img-src/connect-src
test : test_csp_nonce_per_request (page base.html + page hors template,
nonce unique par requête)
suite **1037/1037** · `ruff check app tests` OK · docs à jour
|
||
|
|
f706424f90 |
fix: A31 — transaction par migration + helper columns() (v7.6.0)
- `_apply_one()` : BEGIN explicite → `fn(conn)` → marque `schema_version` →
commit ; rollback complet à l'échec. Avant le DDL sortait en autocommit
(isolation_level legacy) : un échec au milieu laissait un schéma partiel
commité SANS ligne de version, et la reprise rejouait un DDL déjà appliqué.
Si une transaction englobante subsiste (init_db commit juste avant), on la
vide d'abord plutôt que de l'englober.
- Helper unique `columns(conn, table)` (valide l'identifiant, ValueError sinon)
: 25 copies de `{r[1] for r in conn.execute("PRAGMA table_info(...)")}`
éliminées dans migrations.py (21 littéraux + 3 f-string + 1 variante row).
`table_exists`/`column_exists` préconisés par l'audit NON livrés : aucune
migration n'interroge sqlite_master, un contrôle unitaire se lit dans le set.
- Smoke : DB fraîche → 28 migrations → version 29, ré-apply idempotent.
tests : test_migration_transaction_rolls_back (DDL partiel annulé + zéro marque
de version), test_columns_helper_validates_table_name
suite **1036/1036** · `ruff check app tests` OK · OpenAPI 511 chemins / 7.6.0
docs (ROADMAP/CHANGELOG/WORKLOAD/VERSION) à jour
|
||
|
|
7be96f0618 |
fix: A29 + A42(partiel) — publish partagé, fuite password_hash, data_dir (v7.5.0)
- A29 — `app/services/publish.py` : slugify titré unique (fallback aléatoire),
404 si la page n'existe pas, événements centralisés. Les 3 paires
publish/unpublish déléguent (sharing = front, board, v2) :
· board : mise à jour aveugle → 404 + contrôle de session ajouté
· board : perd `share_mode='anyone'` en bonus, v2 : perd `is_shared=1` —
le share dialog reste l'unique propriétaire de ces drapeaux
· v2 : slug fourni conservé, slug vidé aussi à la dépublication (avant : laissé)
· `/users/me` ×2 et listings collections ×3 = contrats versionnés distincts,
décision documentée (on garde)
- Byproduct sécurité — `GET /api/users/me` (v1) et le contexte de `/accounts`
faisaient `SELECT *` sur users → password_hash / login_attempts / locked_until
exposés → colonnes whitelistées (liste v2)
- A42 (partiel) — 9 copies de `Path(os.environ.get("FLOWDECK_DATA_DIR", "/data"))`
→ `settings.data_dir` (property : lecture à chaque accès, les tests
monkeypatchent l'env) ; cache Gitea : évacuation des entrées expirées à chaque
écriture. Reste : client httpx partagé (52 créations, cache par event loop)
tests : test_publish_service_shared_and_safe, test_users_me_no_secret_columns,
test_gitea_cache_evicts_expired
suite **1034/1034** · `ruff check app tests` OK · OpenAPI 511 chemins / 7.5.0
docs (ROADMAP/CHANGELOG/WORKLOAD/VERSION) à jour
|
||
|
|
937ecfc2e0 |
fix: A30 + A37 + A39 + A40 + A41 — fin du P2/P3 XS/S (v7.4.0)
- A30 — `require_scope()` câblé : 69 sites stricts de api_v2.py passent par la factory (Bearer + scope en 1 appel, contrôle manuel supprimé) ; sémantique alignée sur celle des handlers (pas de default "read" → 0 changement de comportement) ; 12 top-level morts supprimés (0 ref app ET tests) : unsync_block, find_referring, _b64url, strip_markdown, format_number, get_auto_property_value, get_next_unique_id, local_date_in_tz, verify_device_token, _get_dynamic_groups, _require_user_gitea, validate_upload_request - A37 — CORS sans `*` : origines = app_base_url + allow_origin_regex (localhost/dev, origines d'extension pour le Web Clipper), méthodes et entêtes minutées, allow_credentials explicite + test test_cors_no_star - A39 — htmx : décision « rien » documentée (32 attributs hx-* réels sur 6 templates, conversion = refonte du view-switching sans test E2E) - A40 — version d'assets à source unique : ENV.globals["asset_version"] lu au boot depuis le fichier VERSION ; littéraux `?v=` de base.html éliminés ; test test_asset_version_single_source - A41 — app.css : 91 règles mortes purgées (-10 274 octets, 121 618 → 111 344), scan templates/JS/CSS/Python à 0 référence suite **1031/1031** · `ruff check app tests` OK · OpenAPI 511 chemins / 7.4.0 docs (ROADMAP/CHANGELOG/WORKLOAD/VERSION) à jour |
||
|
|
998b5c630c |
docs(roadmap): A43 marque partiel — placeholder CSRF et palette restent ouverts
Les deux sous-items JS de A43 ne sont pas traits (utcnow et health log le sont). |
||
|
|
cb47f5c7f4 |
fix: A26 + A33 + A34 + A35 + A36 + A43 — bande S/XS du P2 (v7.3.9)
- A26 — champ `standalone` mort supprimé (0 lecteur) ; `sqlite:////data/…` ne produit plus de chemin UNC sous Windows ; `.env.example` ne promet plus PostgreSQL ; raise au boot si `APP_SECRET_KEY` vaut la valeur par défaut - A33 — rate limit : préfixes manquants (`/scim/v2/`, `/workspace/`, `/db/` + non-GET sur `/s/` et `/f/`), limite lue dans `settings.rate_limit_requests` (60 annoncés / 100 codés en dur), clé `X-Forwarded-For` seulement derrière un proxy local (anti-spoof), `_store` épuré (mémoire bornée) + test dédié - A34 — `_spawn()` : les 10 schedulers loggent leur exception et redémarrent après 10 s au lieu de mourir en silence ; 2 `logger.debug` de scheduler → warning - A35 — OpenAPI régénéré 439 → 511 chemins (`info.version 7.3.9`), README (était v6.7.0), compteur API_GUIDE, titre dupliqué retiré du ROADMAP ; le drift Python 3.12/3.13 reste noté (rebuild d'image à valider) - A36 — 4 dépendances mortes purgées de requirements.txt (aiosqlite, slowapi, loguru, packaging = 0 import) ; pyproject reste sans [project] : Docker et la CI installent requirements.txt, dupliquer les 22 deps créerait 2 sources - A43 — 15 `datetime.utcnow()` → `now(UTC).replace(tzinfo=None)` (format ISO naïf identique, warnings de dépréciation divisés : 2374 → 926) suite **1029/1029** · `ruff check app tests` OK · docs/ROADMAP/CHANGELOG/WORKLOAD à jour |
||
|
|
ffa1fa89ab |
fix: A25 + A21 (partiel) — plus d'exception muque, transaction protégée (v7.3.8)
- A25 — 84 `except Exception: pass/…` → `logger.exception("<fonction>")`
(19 fichiers : api_v2 30, dashboard 10, board 7, sites 5, workspace 5,
api_v2_helpers 5, …) ; `logger` ajouté là où il manquait (api_v2_helpers,
sites + `import logging`)
- A25 critique — les `try` autour de `materialize_properties` supprimés dans
`create_collection_v2` ET `apply_db_template_v2` : un échec interrompt la
transaction au lieu de commiter une collection sans schéma
- test `test_collection_rollback_when_materialize_fails` (Bearer v2, monkeypatch
qui lève, assertions : RuntimeError + 0 ligne commitée)
- A21 partiel — `PRAGMA busy_timeout=5000` dans `get_conn()` (point d'entrée
unique) ; commentaire `ponytail:` : le wrapper async + les 510 call sites
restent à migrer module par module
- suite **1028/1028** · `ruff check app tests` OK
|
||
|
|
3ad2605c9e |
fix: A14 — fin du fallback « row admin » sur l'agent (v7.3.7)
- `_current_user_id` : 401 sans session (24 sites) au lieu de retomber sur `SELECT id FROM users WHERE login='admin'` - `_current_admin` : suppression du même fallback — `PATCH /api/agent/providers` et `POST /api/agent/providers/test` (donc `LLMClient.ping(api_base=…)`) exigent une session admin : 401 sans session, 403 non-admin - `_check_api_base()` sur les 2 routes : scheme http(s), pas d'identifiants dans l'URL (400) ; hôtes privés maintenus — Ollama `localhost:11434` est le provider par défaut du produit (commentaire `ponytail:` pour la fermeture) - +1 test de non-régression → suite **1027/1027**, `ruff check app tests` OK |
||
|
|
1f705ce512 |
fix: A19 terminé — plus aucun préfixe cookie-auth exempt du CSRF (v7.3.6)
- 46 appels non-GET équipés de `X-CSRF-Token` (expression cookie en ligne, portée indifférente) : agent_panel (9), settings (12), local_workspace (15), gitea_workspace (4), workspace (2), workspaces (5), library (2), welcome (2 en v7.3.5) - 5 derniers préfixes sortis d'`EXCLUDED_PATHS` : /api/workspace (+/api/workspaces), /api/local-workspace, /api/settings, /api/gitea, /api/agent - il ne reste exempté que : Bearer (/api/webhook, /api/v1, /api/v2, /scim/v2), callbacks /auth/*, pages publiques (/s/, /f/), /api/csrf-token et /api/frontend-error - vérif : `node --check` des <script> des 39 templates → 0 échec (avant et après) - tests : `anon_csrf()` sur les 5 tests d'anonymat devenus CSRF-first - suite **1026/1026** · `ruff check app tests` OK |
||
|
|
cf76e00f12 | docs(roadmap): A19 — 49 fetch restants (compte exact) au lieu de 51 | ||
|
|
0861f1fdbf |
fix: A19 (partiel) — CSRF réservé aux vrais cas d'exemption (v7.3.5)
- Scan de tous les appels `fetch` non-GET du front par préfixe : 12 préfixes n'ont AUCUN call site sans `X-CSRF-Token` → sortis de `EXCLUDED_PATHS` (`/db/`, `/workspace`, `/workspace/automations`, `/board/api/favorites`, `/api/pages`, `/api/recents`, `/api/notifications`, `/api/comments`, `/api/automations`, `/api/github`, `/api/admin`, `/api/onboarding`) - `welcome.html` : les 2 POST `/api/onboarding/*` reçoivent le header (`adminFetch` prouve que `/api/admin` était déjà couvert) - reste A19 (front à équiper) : `/api/workspace` (+`/api/workspaces`), `/api/local-workspace`, `/api/settings`, `/api/gitea`, `/api/agent` — 49 fetch - tests : helper `anon_csrf()` (anonyme + CSRF valide) pour isoler les 401 de route du 403 middleware — 4 tests d'anonymat ajustés - suite **1026/1026** · `ruff check app tests` OK |