# Changelog - FlowDeck ## v7.11.0 (2026-10-01) — Audit : A32 phase 1 (routers à 0 test) ### Tests - `tests/test_smoke_uncovered.py` — **10 smoke tests**, un 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) ; register sans secret → 400 **avant** tout appel réseau ; status avec `gitea.list_webhooks` stubbé (zéro accès réseau réel) · `notes.py` (2/2) : GET HTML + roundtrip POST→GET (upsert persisté, échappement HTML vérifié `<b>`) · `sidebar_config.py` (2/2) : GET défauts, PUT persisté relu depuis `users.sidebar_config`, 400 sans `config`, remise en état en fin de test · `github_routes.py` (2/2) : status `{"linked": False}`, disconnect ok - Reste (phase 2) : quasi nuls — `library.py` 1/10, `api.py` 3/23, `dashboard.py` 17/63, `api_v2.py` 50/115 - Suite complète : **1047/1047** ## v7.10.0 (2026-10-01) — Audit : A21 phase 2b (api_v2 bouclé) ### Changed - **Helper** `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** : bloqué = le worker, jamais la boucle d'event, et la réponse n'est envoyée qu'une fois l'événement terminé (déterministe, équivalent sémantique de l'`await` d'avant). Note `ponytail:` : clients httpx créés à chaque appel partout → aucun lien de boucle ; sinon `run_coroutine_threadsafe` + boucle du lifespan - **A21 (phase 2b)** — 15 routes `api_v2` dont les seuls awaits étaient `request.json` / `_fire_event` / `fire_published` / `fire_unpublished` → paramètre `Body(default={})` + `run_event_sync(...)` + conversion en `def` - **`api_v2` : 111/115 routes hors event loop** — il ne reste que 4 routes async, toutes avec de vrais awaits réseau : `import_csv_v2` (multipart), `project_tree_v2` (gitea), `test_webhook_v2`, `retry_webhook_deliveries` - Repo-wide : **403 routes synchrones (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** en 228 s ## v7.9.0 (2026-10-01) — Audit : A21 phase 2a (api_v2 hors loop) ### Changed - **A21 (phase 2a)** — dans `api_v2`, les 36 routes dont le **seul** `await` était `body = await request.json()` passent à un paramètre FastAPI `body: dict = Body(default={})` (parsing async fait par FastAPI avant l'appel) puis sont converties en `def` → threadpool. Équivalences vérifiées avant-engagement : corps absent → `{}` (identique au `try/except` d'avant), JSON invalide → **422** (avant : traité silencieusement comme `{}`), zéro `body[...] = ` dans le fichier (le défaut partagé n'est jamais muté) - `api_v2` : **96/115 routes hors event loop** (60 en phase 1 + 36 ici) ; il ne reste que **19 routes async** dans ce router (`fire_event`, `request.form`, appels gitea/webhooks — phase 2b) - Bug de transformation évité en cours de route : première version du script supprimait 5 lignes au lieu de 4 (`slice` fermant d'un cran trop loin) — fichier restauré depuis git puis script corrigé, 0 ligne perdue (diff logique : +39/-183 = 36 signatures + import, 4 lignes de try/except × 35) ### Tests - Suite complète **1037/1037** (242 s) ; ciblée sur `test_public_api_v2` + `test_v65` + audit : 62/62 verts avant la passe complète ## v7.8.0 (2026-10-01) — Audit : A21 phase 1 (SQLite hors event loop) ### Changed - **A21 (phase 1)** — **352 routes** `async def` sans aucun `await` converties en `def` : FastAPI les exécute alors dans son threadpool — tout leur travail SQLite (`get_conn()` + `conn.execute`) quitte l'event loop, **sans changer une ligne de logique** (la conversion est sémantiquement neutre : vérifié corps par corps — aucun `await`/`async with`/`async for`/`asyncio` dans les fonctions converties). Répartition : api_v2 60, dashboard 40, collections 25, board 23, workspace 19, wiki 17, permissions 14, api 14, + 35 autres fichiers - **Reste (phase 2)** — les 311 routes qui ont de vrais `await` (`request.json()`, `fire_event`, httpx) : enrouler les blocs DB dans `await anyio.to_thread.run_sync(...)` ; aucun wrapper partagé livré pour l'instant (rien ne l'appellerait) ### Perf - Dernière suite : 233 s (écarts précédents mesurés : 235-359 s) — les handlers SQLite ne saturent plus la boucle pendant les tests ## v7.7.0 (2026-10-01) — Audit : A20 (CSP — nonce, partie 1) ### Security - **A20** — `script-src` : `'unsafe-inline'` remplacé par `'nonce-'`. Le middleware CSP génère le nonce dans une `ContextVar` avant `call_next` (visible des templates via `{{ csp_nonce() }}`) ; **38 tags `