feat: A20 phase 3 LOT 3b — gitea + agent + éditeur verts en CSP (v7.41.0)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s

Changed :
- gitea_workspace : x-data="giteaWorkspace" → appel giteaWorkspace(),
  new Date(…) → fmtGwDate(pp), x-html icône arbre → bindGwIcon (x-init +
  Alpine.effect).
- agent_panel : x-html markdown → bindMarkdown($el, m) (effet reactif).
- page_editor : les 12 sites window.E du topbar right_actions →
  délégués appState (edCall('…') x6, edTimeAgo, edCommentCount, edShared,
  bindStar — les 2 branches du ternaire favorited étaient identiques) ;
  + 3 sites dans _page_editor_content (edCall commentOnSelection,
  openBacklink, fmtImportSize, bindIconHtml). Garde Jinja : quotes \' dans
  le set délimité par ' (quote nue = 500).
- Gate éditeur (csp_preview) : création collection → /pages/{id},
  délégués + editorState liés, filet 0-erreur.

Fixed :
- x-html iconHtml() du contenu éditeur = directive INTERDITE sous build
  CSP (attrapé par le filet) → x-init + Alpine.effect.

⚠️ BUG pre-existant identifie (pas introduit ici) : les right_actions du
topbar sont servis ÉCHAPPÉS sur TOUTES les pages (entities "/< —
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
This commit is contained in:
2026-10-02 15:15:26 -04:00
parent d7d9966edf
commit 6914780f24
17 changed files with 213 additions and 16 deletions
+3 -1
View File
@@ -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**.*