From 48b5551b93f69ba7c835a99cecbefa03cb4176da Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Fri, 2 Oct 2026 15:39:57 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20BUG=20TOPBAR=20=E2=80=94=20boutons=20du?= =?UTF-8?q?=20header=20servis=20=C3=A9chapp=C3=A9s=20sur=20toutes=20les=20?= =?UTF-8?q?pages=20(v7.42.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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É) --- CHANGELOG.md | 29 +++++++++++++++++++++++ ROADMAP.md | 2 +- VERSION | 2 +- WORKLOAD.md | 2 +- app/main.py | 2 +- app/templates/gitea_workspace.html | 2 +- app/templates/page_editor.html | 2 +- app/templates/page_editor_collection.html | 2 +- app/templates/workspace.html | 2 +- app/templates/workspaces.html | 2 +- docs/openapi-v2.json | 2 +- e2e/csp_preview.spec.js | 7 +++--- tests/test_topbar_right_actions.py | 22 +++++++++++++++++ 13 files changed, 64 insertions(+), 14 deletions(-) create mode 100644 tests/test_topbar_right_actions.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 71caf1b..88fbdf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,34 @@ # Changelog - FlowDeck +## v7.42.0 (2026-10-01) — 🐛 BUG TOPBAR CORRIGÉ (boutons du header en texte brut) + +### Fixed + +- **Les `right_actions` du topbar étaient servis ÉCHAPPÉS sur TOUTES les + pages** (entities `"`/`<` → boutons Share/Star/Settings/Login en + texte brut). **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 ; le `|safe` de `_header:141` est then no-op sur un Markup déjà + échappé. Découvert en cherchant l'échec du gate éditeur CSP (v7.41.0), + reproductible partout (`curl /workspaces`). +- **Fix racine (5 templates)** : conversion en **block-set** + `{% set right_actions %}…{{ fd_icon(…) }}…{% endset %}` (source = brute, + interpolation = Markup brut — la forme idiomatique Jinja) : + `gitea_workspace`, `page_editor`, `page_editor_collection`, `workspace`, + `workspaces`. Piège du script : regex greedy multi-lignes = set avalé → + matcher **une ligne**. +- **Test permanent** `tests/test_topbar_right_actions.py` : `/workspaces` + doit contenir `class="topbar-btn"` parsé et ZÉRIE entité `"`. +- Gate éditeur : l'assertion `.star-btn` **ré-ajoutée** (les boutons + rendent à nouveau). + +### Notes + +- Probable régression depuis A10 (activation d'autoescape) : les `~` + étaient des no-op avant, Markup.__radd__ échappait déjà… les + `|safe` devenaient nécessaires et ne pouvaient plus réparer. + ## v7.41.0 (2026-10-01) — A20 phase 3 LOT 3b : gitea + agent + éditeur verts ### Changed diff --git a/ROADMAP.md b/ROADMAP.md index 4d70f77..df851c1 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1143,7 +1143,7 @@ Quality DB views, Agent IA Palette → Realtime + E **LOT 3a fait 2026-10-01 (v7.40.0)** : gate « surfaces simples » = **welcome, trash, accounts, workspace, import VERTS** (4 sans aucune modification — scan propre + registres lot 1) + **fix bug pré-existant `/import`** (x-text `report.relations` non garde → pageerror sous les 2 builds). **8 surfaces couvertes** au total. **LOT 3b fait 2026-10-01 (v7.41.0)** : **gitea_workspace + agent_panel + page_editor VERTS** (éditeur = 15 sites : 12 `window.E` → délégués `edCall`/`edTimeAgo`/`edCommentCount`/`edShared`/`bindStar` dans appState + 3 dans `_page_editor_content` → `openBacklink`/`fmtImportSize`/`bindIconHtml` ; piège Jinja : quotes `\'` dans le `set`). **12 surfaces vertes** au total (7 gates). **Reste ph3** : board/table_view/teamload/card_detail (scan propre, gates liés au contexte Gitea — gitea actuellement down) → csp_preview vert partout, puis bascule réelle (retrait unsafe-eval). -**🐛 BUG pré-existant à analyser (trouvé v7.41.0)** : **`right_actions` du topbar servi ÉCHAPPÉ sur TOUTES les pages** (entities `"`/`<`, boutons Share/Star/Settings en texte brut — repro `curl /workspaces`); `_header:141` a bien `|safe`, l'ENV est standard, rendu local du même motif = PARSED → cause serveur à cerner (piste : valeur échappée à la construction du set). Effort : **M**. `img-src` : si un proxy d'images local arrive. Effort : **L** (reste : L, plan ci-dessus). +**🐛 BUG TOPBAR — CORRIGÉ 2026-10-01 (v7.42.0)** : cause racine = `'literal' ~ fd_icon(…)` — `fd_icon` = macro → `Markup`, et `Markup.__radd__/__add__` **échappe les segments `str` littéraux** → boutons en entities partout ; `|safe` no-op sur Markup déjà échappé. Fix : **block-set** `{% set right_actions %}…{{ fd_icon() }}…{% endset %}` dans les 5 templates (gitea, page_editor ×2, workspace, workspaces) + **test permanent** `tests/test_topbar_right_actions.py` + assertion `.star-btn` rétablie dans le gate éditeur. Probable régression depuis A10 (autoescape). `img-src` : si un proxy d'images local arrive. Effort : **L** (reste : L, plan ci-dessus). - [x] **A21 — `sqlite3` synchrone sur l'event loop — PARTIEL 2026-09-30 → phase 1 le 2026-10-01 : `PRAGMA busy_timeout=5000` ajouté au point d'entrée unique `get_conn()` (db.py)** ; reste le wrapper async `anyio.to_thread` + la migration des 510 call sites : `get_conn()` (`db.py:833-843`) est synchrone et **510 des 689 `async def` de routes** l'appellent (805 occurrences au total ; 0 `run_in_threadpool`, 1 seul `asyncio.to_thread` dans tout le dépôt : `semantic_search.py:262`) ; connexion neuve par requête (`connect` + 2 PRAGMA), **aucun `busy_timeout`**. Chaque requête bloque la boucle. *Fix : wrapper async (`anyio.to_thread.run_sync`) partagé, migrer d'abord `api_v2`/`dashboard`/`collections`/`board` + `PRAGMA busy_timeout=5000`.* — **phase 1 faite 2026-10-01** : **352 routes `async def` SANS aucun `await`** converties en `def` (scan corps par corps : ni `await`/`async with`/`async for`, ni `asyncio`) → FastAPI les exécute dans son threadpool, donc tout leur travail SQLite quitte l'event loop, sans changer une ligne de logique (api_v2 : 60, dashboard : 40, collections : 25, board : 23, + main.py : 6 ; aucune occurrence `asyncio`/`run_coroutine` dans les corps convertis). **Phase 2a faite 2026-10-01 (api_v2)** : les routes dont le SEUL await était `body = await request.json()` (36) → paramètre FastAPI `body: dict = Body(default={})` (parsing fait par FastAPI avant l'appel, équivalences vérifiées : corps absent → `{}` comme le try/except d'avant, JSON invalide → 422 au lieu d'un silencieux `{}`) + conversion en `def` → **api_v2 passe à 96/115 routes hors loop**. **Phase 2b faite 2026-10-01 (api_v2 bouclé)** : helper `run_event_sync(coro)` (automations.py) — `asyncio.run` sur une boucle dédiée dans le worker threadpool : l'événement est EXÉCUTÉ ET ATTENDU avant la réponse (déterministe comme l'await) mais ne bloque jamais la boucle ; les 15 routes dont les seuls awaits étaient `json`/`_fire_event`/`fire_published`/`fire_unpublished` passent en `def`. **api_v2 : 111/115 routes hors loop**, les 4 restantes ont de vrais awaits réseau (`import_csv_v2` multipart, `project_tree_v2` (gitea), `test_webhook_v2`, `retry_webhook_deliveries`). **Phase 2c faite 2026-10-01 (repo-wide)** : **283 → 93 routes async** (**86 % des 667 routes hors loop**, avant 61 %) en 4 passes — (A) racine auth : `get_current_user` (session.py) + ses clones async (`agent._current_user_id/_workspace_id/_current_admin`, `sso._require_admin`) étaient `async` **sans aucun await** → `def`, **47 `await` supprimés** dont 3 via l'alias `gcu` (piège : grep littéral aveugle, rattrapé par la suite) ; (B) re-scan → 19 routes flipées ; (C/D) **155 routes** `request.json`/événements → `Body(default={})` (3 formes : try/except `body = {}` intact, try/except `raise HTTPException(400)` → `Body(...)` requis (422 FastAPI, **0 test sur le 400**), forme conditionnelle content-type ×54 → défaut `{}`) + `run_event_sync` → `def`. **Reste async (93, justifié)** : form/upload/file (22), gitea/llm/oidc réseau (~25), `_json_body` 9, 2 JSON inline en argument, 1 fallback logique, 1 lecture conditionnelle web_clipper, + mixtes json+réseau. **A21 : fait** (sauf l'idée initiale d'`anyio.to_thread` par bloc DB — **inutile** : les routes sont DÉJÀ hors loop, le SQLite synchrone n'est plus sur la boucle). Effort : **L** (fait). - [x] **A22 — Validateur d'upload = code mort** : `validate_upload()` + `ALLOWED_EXTENSIONS` + `MAX_UPLOAD_SIZE` (`security.py:33-40`) n'ont **aucun appelant** ; `upload_local_workspace_file` (`dashboard.py:1507-1574`) fait `file_path.write_bytes(content)` après seulement `Path(filename).name` — ni taille, ni extension, ni auth, et `/api/local-workspace` est exclu du CSRF. Chemin aussi codé en dur `Path(f"/data/uploads/...")` au lieu de `FLOWDECK_DATA_DIR`. *Fix : appeler `validate_upload()` avant `read()` + session. Effort : **S**.* - [x] **A23 — N+1 avérés** : `dashboard.py:905` (`COUNT(*) FROM pages` par page dans une boucle de 20), `board.py:2141-2146` (INSERT + `_extract_ai_keywords` par issue), `collections.py:378` (INSERT par propriété en boucle). *Fix : une agrégation `GROUP BY` / un `executemany`. Effort : **S**.* diff --git a/VERSION b/VERSION index 8d82fb5..fa52423 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.41.0 +7.42.0 diff --git a/WORKLOAD.md b/WORKLOAD.md index 7c5110e..495cd4b 100644 --- a/WORKLOAD.md +++ b/WORKLOAD.md @@ -1,6 +1,6 @@ # WORKLOAD — FlowDeck Notion Clone -> **Début**: 2026-07-08 | **Version**: v7.41.0 (A20 ph3 LOT 3b : gitea + agent + éditeur verts — 12 sites window.E délégués; BUG pré-existant : right_actions servi échappé partout) | **Statut**: EN COURS 🔄 +> **Début**: 2026-07-08 | **Version**: v7.42.0 (BUG TOPBAR CORRIGÉ : « 'literal' ~ fd_icon » échappait les boutons → block-set ×5 + test permanent) | **Statut**: EN COURS 🔄 > **Cible**: parité Notion + intégration forge · **Follow-ups v7.3 livrés**: sidebar teamspaces, notif `page.updated`, charts `number` + dashboards multi-DB, unfurl forge, UI Settings → Audit — voir `ROADMAP.md § v7.3.0` ## Avancement Global diff --git a/app/main.py b/app/main.py index 5862206..4456b6c 100644 --- a/app/main.py +++ b/app/main.py @@ -185,7 +185,7 @@ async def lifespan(_app: FastAPI): app = FastAPI( title="FlowDeck", - version="7.41.0", + version="7.42.0", docs_url="/docs", redoc_url="/redoc", lifespan=lifespan, diff --git a/app/templates/gitea_workspace.html b/app/templates/gitea_workspace.html index fe6f006..5303690 100644 --- a/app/templates/gitea_workspace.html +++ b/app/templates/gitea_workspace.html @@ -6,7 +6,7 @@ {% block topbar %} {% set breadcrumb_items = [{"label": owner ~ "/" ~ repo, "url": None}] %} {% set page_icon = "link" %} -{% set right_actions = '' ~ fd_icon("settings",16) ~ '' %} +{% set right_actions %}{{ fd_icon("settings",16) }}{% endset %} {% include '_header.html' %} {% endblock %} diff --git a/app/templates/page_editor.html b/app/templates/page_editor.html index a9ae77f..a5ca267 100644 --- a/app/templates/page_editor.html +++ b/app/templates/page_editor.html @@ -3,7 +3,7 @@ page_title %}{{ page.title }}{% endblock %} {% block topbar %} {% set page_icon = "file" if page.content_format != 'file' else "paperclip" %} {% set page_title = page.title %} {% set nav_page_id = page.id %} -{% set right_actions = 'Edited ▾' %} +{% set right_actions %}Edited ▾{% endset %} {% include '_header.html' %} {% endblock %} {% block content %} {% include "_page_editor_content.html" %} diff --git a/app/templates/page_editor_collection.html b/app/templates/page_editor_collection.html index ab58a3e..072c6c0 100644 --- a/app/templates/page_editor_collection.html +++ b/app/templates/page_editor_collection.html @@ -2,7 +2,7 @@ page_title %}{{ page.title }}{% endblock %} {% block topbar %} {% set page_icon = "file" %} {% set page_title = page.title %} -{% set right_actions = 'Edited ▾' %} +{% set right_actions %}Edited ▾{% endset %} {% include '_header.html' %} {% endblock %} {% block content %} {% include "_database_table.html" %} diff --git a/app/templates/workspace.html b/app/templates/workspace.html index 8f2f398..7408e7f 100644 --- a/app/templates/workspace.html +++ b/app/templates/workspace.html @@ -6,7 +6,7 @@ {% block topbar %} {% set page_icon = "home" %} {% set page_title = "Workspace — Projects" %} -{% set right_actions = '' ~ fd_icon("key",16) ~ '' ~ fd_icon("settings",16) ~ '' %} +{% set right_actions %}{{ fd_icon("key",16) }}{{ fd_icon("settings",16) }}{% endset %} {% include '_header.html' %} {% endblock %} diff --git a/app/templates/workspaces.html b/app/templates/workspaces.html index e62819f..3163ecb 100644 --- a/app/templates/workspaces.html +++ b/app/templates/workspaces.html @@ -6,7 +6,7 @@ {% block topbar %} {% set breadcrumb_items = [{"label": "Workspaces", "url": None}] %} {% set page_icon = "home" %} -{% set right_actions = '' ~ fd_icon("key",16) ~ '' ~ fd_icon("settings",16) ~ '' %} +{% set right_actions %}{{ fd_icon("key",16) }}{{ fd_icon("settings",16) }}{% endset %} {% include '_header.html' %} {% endblock %} diff --git a/docs/openapi-v2.json b/docs/openapi-v2.json index 0b6107e..e90823b 100644 --- a/docs/openapi-v2.json +++ b/docs/openapi-v2.json @@ -2,7 +2,7 @@ "openapi": "3.1.0", "info": { "title": "FlowDeck", - "version": "7.41.0" + "version": "7.42.0" }, "paths": { "/auth/register": { diff --git a/e2e/csp_preview.spec.js b/e2e/csp_preview.spec.js index 3f45b0e..f87669d 100644 --- a/e2e/csp_preview.spec.js +++ b/e2e/csp_preview.spec.js @@ -134,10 +134,9 @@ test('A20-ph3 : éditeur de page (right_actions) sous build CSP', async ({ page }); expect(editor.delegates).toBe('ok'); expect(editor.ed).toBe('ok'); - // (le rendu des boutons right_actions est cassé côté SERVEUR — - // entities " sur TOUTES les pages, régression pré-existante - // documentée au ROADMAP — donc on n'asserte pas leur présence ; - // le filet 0-erreur = les expressions évaluées sont parsables.) + // les boutons du topbar sont réellement servis (bug A10 « ~ + Markup » + // corrigé : block-set) ET leurs expressions passent le filet CSP + await expect(page.locator('.star-btn').first()).toBeAttached(); } finally { await page.evaluate(async (id) => { const csrf = (document.cookie.match(/csrf_token=([^;]+)/) || [])[1] || ''; diff --git a/tests/test_topbar_right_actions.py b/tests/test_topbar_right_actions.py new file mode 100644 index 0000000..436a3b4 --- /dev/null +++ b/tests/test_topbar_right_actions.py @@ -0,0 +1,22 @@ +"""Les `right_actions` du topbar doivent être servis PARSED (A10). + +Regresssion historique : `'literal' ~ fd_icon(...)` — fd_icon = macro → +Markup, et `Markup.__radd__/__add__` échappe les segments littéraux → +boutons livrés en entities (`"`) sur TOUTES les pages. Fix : block-set +`{% set x %}…{{ fd_icon() }}…{% endset %}` (5 templates). +""" +from tests.conftest import login_test_client + + +def test_right_actions_parsed(client): + login_test_client(client) + r = client.get("/workspaces") + assert r.status_code == 200 + body = r.text + parsed = 'class="topbar-btn"' in body + escaped = ""topbar-btn"" in body + print("PARSE:", parsed, "| ESCAPED:", escaped) + print("DBG present:", "DBGCLS" in body) + k = body.find("DBGCLS=") + print("CTX:", body[max(0, k - 80) : k + 320].replace("\n", " ") if k >= 0 else "pas de DBG") + assert parsed and not escaped, "right_actions servi échappé"