From 6914780f2453b0f7efdccf9379ece5813ad9ff2a Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Fri, 2 Oct 2026 15:15:26 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20A20=20phase=203=20LOT=203b=20=E2=80=94?= =?UTF-8?q?=20gitea=20+=20agent=20+=20=C3=A9diteur=20verts=20en=20CSP=20(v?= =?UTF-8?q?7.41.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CHANGELOG.md | 39 ++++++++++++++ ROADMAP.md | 4 +- VERSION | 2 +- WORKLOAD.md | 2 +- app/main.py | 2 +- app/templates/_page_editor_content.html | 8 +-- app/templates/agent_panel.html | 2 +- app/templates/base.html | 13 +++++ app/templates/gitea_workspace.html | 6 +-- app/templates/page_editor.html | 2 +- app/templates/page_editor_collection.html | 2 +- docs/openapi-v2.json | 2 +- e2e/_probe_edit.js | 57 ++++++++++++++++++++ e2e/csp_preview.spec.js | 66 ++++++++++++++++++++++- static/js/agent_panel_2.js | 4 ++ static/js/gitea_workspace.js | 9 ++++ static/js/page_editor_scripts.js | 9 ++++ 17 files changed, 213 insertions(+), 16 deletions(-) create mode 100644 e2e/_probe_edit.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fd3f45..71caf1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,44 @@ # Changelog - FlowDeck +## v7.41.0 (2026-10-01) — A20 phase 3 LOT 3b : gitea + agent + éditeur verts + +### Changed + +- **gitea_workspace** : `x-data="giteaWorkspace"` → appel `giteaWorkspace()`, + `new Date(…)` → `fmtGwDate(pp)`, x-html icône d'arbre → `bindGwIcon` + (x-init + `Alpine.effect`). +- **agent_panel** : x-html markdown → `bindMarkdown($el, m)` (effet réactif + sur `m.content`). +- **page_editor (les 12 sites `window.E` du topbar `right_actions`)** → + délégués `appState` : `edCall('…')` (6 appels, arg littéral = seule forme + parsable), `edTimeAgo`, `edCommentCount`, `edShared`, `bindStar` (les 2 + branches du ternaire favorited étaient identiques → rendu 1×) — les deux + templates `page_editor.html` + `page_editor_collection.html` + garde + Jinja : les quotes insérées doivent être `\'` (le `set` est délimité par + `'`, une quote nue casse le template = 500). +- **_page_editor_content** : +3 sites (`window.E.commentOnSelection` → + `edCall`, `backlinksOpen…location.href` → `openBacklink(b)`, + `Math.round(importFile…)` → `fmtImportSize(f)`) + x-html icône de page → + `bindIconHtml` (effet sur `iconHtml()`). +- **Gate éditeur** (`csp_preview`) : création collection → `/pages/{id}`, + délégués `appState` liés + `editorState` lié + filet 0-erreur. + +### Fixed + +- x-html `iconHtml()` du contenu éditeur : directive **interdite** sous + build CSP (le filet l'a attrapé) → `x-init` + `Alpine.effect`. + +### ⚠️ Nouveau bug pré-existant identifié (PAS introdui par ce lot) + +- **Les `right_actions` du topbar sont servi ÉCHAPPÉS sur TOUTES les + pages** (entities `"`/`<` — les boutons Share/Star/Settings/… + s'affichent en texte brut). Reproductible : `curl /workspaces` → + `"topbar-btn"`. Le `{{ right_actions|safe … }}` de `_header:141` + EST présent, l'ENV Jinja est standard, un rendu local du même motif sort + PARSED — la cause exacte côté serveur reste à cerner (piste : valeur déjà + échappée à la construction). Roadmap = suivi dédié ; le gate éditeur + n'asserte donc pas la présence des boutons. + ## v7.40.0 (2026-10-01) — A20 phase 3 LOT 3a : 5 surfaces de plus vertes ### Added diff --git a/ROADMAP.md b/ROADMAP.md index 03c14fe..4d70f77 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1141,7 +1141,9 @@ Quality DB views, Agent IA Palette → Realtime + E **LOT 1 fait 2026-10-01 (v7.38.0)** : (1) **harnais `e2e/csp_preview.spec.js`** — sert le build CSP à la place de `alpine.min.js` par interception Playwright (0 déploiement, tout parse-error = pageerror) ; (2) **les 16 composants `x-data="fn()"` non enregistrés → `Alpine.data`** (registre = seule résolution du build CSP) ; (3) **shell base.html migré** : x-effect document → `syncSidebarClass()`, $nextTick arrow → `initSidebarSort()`, `window.FlowDeck.*` → méthodes, Object/Math/window dans x-for/:style → `sidebarSections()`/`sectionMenuPos()` ; (4) **x-html du shell migrés** → `x-init`+`Alpine.effect` (agent, carte projet, library ×3) + recherche library (`toggleSearch`) ; (5) **eslint 0/0 restauré** (globals `getCsrf` A38 + `;;` résiduels) ; **gate library VERT** (0 pageerror, icônes + recherche vérifiées) → 1093/1093. **LOT 2 fait 2026-10-01 (v7.39.0)** : **settings + local_workspace VERTS** en csp_preview (3/5 surfaces) — voir CHANGELOG pour les 2 pièges structurels résolus (snapshot `ji` = objet `window` pré-boot banni → const lexicale partagée ; bloc preview hors racine → composant `wsPreview` déléguant, wrapper unique à cause des magics) + `RATE_LIMIT_REQUESTS=600` en `.env` local (rafales E2E vs 60/min). **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. -**Reste ph3** : page_editor (12 sites `window.E` dans `right_actions` Jinja), gitea_workspace (`new Date`), agent_panel (1 x-text + 1 x-html markdown), board/table_view/teamload/card_detail (scan propre, liés au contexte Gitea pour les gates) → csp_preview vert partout, puis bascule réelle (retrait unsafe-eval). `img-src` : si un proxy d'images local arrive. Effort : **L** (reste : L, plan ci-dessus). +**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). - [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 cd66467..8d82fb5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.40.0 +7.41.0 diff --git a/WORKLOAD.md b/WORKLOAD.md index 4549a6f..7c5110e 100644 --- a/WORKLOAD.md +++ b/WORKLOAD.md @@ -1,6 +1,6 @@ # WORKLOAD — FlowDeck Notion Clone -> **Début**: 2026-07-08 | **Version**: v7.40.0 (A20 ph3 LOT 3a : welcome/trash/accounts/workspace/import verts — 8 surfaces CSP couvertes, bug x-text import corrigé) | **Statut**: EN COURS 🔄 +> **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 🔄 > **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 bb85921..5862206 100644 --- a/app/main.py +++ b/app/main.py @@ -185,7 +185,7 @@ async def lifespan(_app: FastAPI): app = FastAPI( title="FlowDeck", - version="7.40.0", + version="7.41.0", docs_url="/docs", redoc_url="/redoc", lifespan=lifespan, diff --git a/app/templates/_page_editor_content.html b/app/templates/_page_editor_content.html index ae4d4f1..89a72cc 100644 --- a/app/templates/_page_editor_content.html +++ b/app/templates/_page_editor_content.html @@ -546,7 +546,7 @@
- +