# Changelog - FlowDeck ## v7.28.0 (2026-10-01) — Audit : A42 TERMINÉ (client httpx partagé) ### Changed - **`app/services/http_client.py`** : `async with shared_client(timeout=15) as client:` remplace les **49 créations `async with httpx.AsyncClient(`** réparties dans 14 fichiers (gitea ×21, providers ×11, calendar ×4, automations ×3, …) — le pool de connexions est réutilisé au lieu d'être recréé à chaque appel - Cache **par (boucle d'event, kwargs)** en `WeakKeyDictionary` : un `AsyncClient` n'est jamais partagé entre deux loops (le piège des tests : « Event loop is closed ») — une boucle par test = un client propre, collecté avec elle. Clé = kwargs triés, `repr()` pour les valeurs non hashables (`headers=` dict) - Context manager no-op à la sortie (pas de fermeture du client partagé) ; `ponytail:` documenté : pas d'`aclose` explicite, plafond = pools non fermés à la main (GC des sockets), upgrade = lifespan - **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), cloisonnement par kwargs, cloisonnement par boucle (2× `asyncio.run`) - Stub webhooks : patch étendu à la fabrique `http_client.httpx` + purge du cache (les tests patchaient `webhook_outbound.httpx`, contourné par la fabrique partagée) - Suite complète : **1091/1091** ## v7.27.0 (2026-10-01) — Audit : A20 phase 2 (CDN retiré, connect-src fermé) ### Changed - **Vendorisation** : chart.js 4.5.1 + leaflet 1.9 (js, css, 5 images marker/layer) téléchargés vers `static/js/vendor/` ; 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, caractères hors base URL filtrés) — le `https:` universel (canal d'exfil JS) 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… et 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 servis (200), source `collections.py` sans référence CDN - `test_view_chart_renders` : assert sur le chemin vendor - Suite complète : **1090/1090** ### Notes - 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) ## v7.26.0 (2026-10-01) — Audit : A21 phase 2c (190 routes hors loop) ### Changed - **A21 phase 2c** — **283 → 93 routes async** (**86 % des 667 routes** en threadpool, avant : 61 %) en 4 passes : · **Racine auth** : `get_current_user` (session.py) était `async def` **sans aucun await** (cookie decode = synchrone) ; idem ses clones (`agent._current_user_id/_workspace_id/_current_admin`, `sso._require_admin` au corps 0 await) → `def` + **47 `await` supprimés** (dont 3 via l'alias `gcu` — le piège : le grep littéral ne les voyait pas, la suite les a attrapés) · Re-scan : 19 routes devenues sans await → `def` · **155 routes** dont les seuls awaits étaient `request.json()` / événements → `Body(default={})` + `run_event_sync(...)` puis `def` : - try/except `body = {}` → défaut `{}` (même tolérance, laissée intacte) - try/except `raise HTTPException(400)` → `Body(...)` **requis** (422 FastAPI — **aucun test ne couvrait le 400**, aucun call front n'envoie de JSON invalide) - forme conditionnelle `request.json() if content-type else {}` (54 sites) → défaut `{}` : sans corps = `{}` dans les 2 cas - Import `Body`/`run_event_sync` ajoutés aux routers convertis ### Notes - **Reste async (93, justifié)** : `request.form`/`upload.read`/`file.read` (corps réellement asynchrone), gitea/llm/oidc (réseau), `_json_body` (9 — wrapper de validation), 2 JSON inline en argument d'appel, 1 fallback à logique (`capture_frontend_error`), 1 lecture conditionnelle de taille (web_clipper) - Suite complète : **1089/1089** · ruff OK ## v7.25.0 (2026-10-01) — Audit : A27 CLOSED (eslint 0/0) ### Fixed - **Toasts settings jamais affichés** : `settings.js` appelait `typeof toast === 'function'` — guard **toujours faux** (le `toast` global n'existait pas) → les retours « Timezone saved » / « SP metadata URL copiée » ne s'affichaient jamais. → `window.showToast` (global `base.html`) - `_wsInitData = window._wsInitData` (auto-affectation d'un nom non déclaré, sans effet observable car `window._wsInitData` existe depuis L4) supprimé ### Changed - **A27 lint terminé : `eslint static/js` = 0 erreur / 0 warning** (285 → 0, 22 fichiers) : · no-empty ×70 = `catch (x) {}` vides → `catch { /* volontaire */ }` (binding optionnel ES2019 ; zéro changement de comportement) · no-unused-vars ×171 = bindings de catch retirés + 24 lignes mortes déterministes (assert sur texte exact avant suppression) + `/* exported */` sur les 10 fonctions appelées depuis les attributs HTML des templates (vérifiées par grep) · no-undef ×44 = vrais globaux déclarés dans `eslint.config.mjs` (getSvgIcon = inline base.html, TextDecoder = API, Prism = CDN) ### Notes - `node --check` vert sur tous les fichiers ; suite complète **1089/1089** - A27 reste ouvert uniquement pour : `base` 1 523 L structurel (inline par nature, décision assumée) + ~500 L de petits blocs hors cibles ## v7.24.0 (2026-10-01) — Audit : A27 extraction TERMINÉE (bilan -85 %) ### Changed - **A27 (phase 2c)** — `_database_table_scripts.html` : **1 314 L extraites** vers `static/js/database_table.js`. 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 - 2 tests adaptés (lisaient le template source → `database_table.js`) ; `FlowDeckDB` plus dans le HTML → assert sur le JS ### Notes - **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 %}` — reste inline par nature, décision assumée), ~500 L de petits blocs hors cibles, nettoyage des 285 warnings - Suite complète : **1089/1089** ## v7.23.0 (2026-10-01) — Audit : A27 phase 2b (+3 801 L) ### Changed - **A27 (phase 2b)** — 4 blocs interpolés extraits avec la recette config JSON (identique à la 2a) : · `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_url, avatar_color, user full_name/login/email, is_admin, auth_method — **2 routes rendent ce template**, les expressions `or ""` sont préservées pour les valeurs Undefined, `is_admin` reste un booléen) · `_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 inline (nonce conservé) + `