# 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 - **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 - **Gate `csp_preview` « surfaces simples »** : `/welcome`, `/trash`, `/accounts`, `/workspace`, `/import` — **0 modification de code nĂ©cessaire** sur les 4 premiĂšres (scan statique 0 expression/x-html + registres Alpine.data posĂ©s au lot 1). **8 surfaces couvertes** au total (base shell, library, settings, local workspace + celles-ci). ### Fixed - **Bug prĂ©-existant sur `/import` (visible sous les DEUX builds)** : `x-text="'🔗 '+report.relations
"` Ă©valuĂ© alors que `report = null` (le `x-show` parent ne masque pas, il initialise quand mĂȘme) → pageerror « Cannot read property 
 'relations' » — garde `report && report.relations`. (Le probe montrait aussi un 401 console prĂ©-existant sur la page — hors pĂ©rimĂštre, non touchĂ©.) ### Notes - Reste ph3 : page_editor (12 sites `window.E` dans `right_actions`), gitea_workspace (`new Date`), agent_panel (site `x-text` markdown + 1 x-html), board + table_view/teamload/card_detail (gabarits liĂ©s au contexte Gitea, scan statique propre) → bascule rĂ©elle ensuite. ## v7.39.0 (2026-10-01) — A20 phase 3 LOT 2 : settings + local workspace verts ### Added - **2 gates `csp_preview` de plus** : `settings` (composant liĂ©, overlay visible, 0 erreur) et `local workspace` (recherche focalisĂ©e via `Alpine.nextTick`, chips filtre en SVG via `bindSvg`, 0 erreur). **3 surfaces vertes** sous build CSP : library, settings, local workspace. ### Changed - **settings** : `window.history.back()`/`new Date(
)` → mĂ©thodes `historyBack`/`fmtLastLogin`/`fmtAuditDate` ; `?.` → ternaires. - **local workspace** : `x-data="_wsInitData"` → registre `wsInitData()` (le build CSP ne rĂ©sout que le registre) ; 14 `x-html` → `x-init` + `Alpine.effect` (`bindSvg`/`bindFileIcon`/`bindNodeIcon`/`bindChildren`/ `bindPreview`) ; `$nextTick`+`$refs` arrow → `toggleSearch()` ; `window.FlowDeck.*` → `createPageAt`/`createFolderAt` ; `?.` → ternaires ; `@contextmenu="_wsInitData.
"` → appel de mĂ©thode. - **Partage d'Ă©tat JS↔Alpine (piĂšge du build CSP)** : `ji` = snapshot des **valeurs** de toutes les propriĂ©tĂ©s `globalThis` au boot → l'objet mis sur `window` avant Alpine est **banni** (« Accessing global variables is prohibited »). Fix : objet portĂ© par une **const lexicale** (non propriĂ©tĂ© `globalThis`) + factory Alpine.data qui le retourne → **mĂȘme objet** partagĂ©, rĂ©activitĂ© intacte (une copie `Object.assign` aurait coupĂ© les mises Ă  jour JS : preview, uploads, isDragging). - **Bloc preview hors div racine** (structure prĂ©-existante : le parseur referme la racine avant, masquĂ©e par le fallback window d'Alpine standard) → composant `wsPreview` **dĂ©lĂ©guant** vers `_wsInitData` (`Alpine.reactive` pour la rĂ©activitĂ© ; wrapper = objet unique, les magics `$nextTick` ne sont redĂ©finissables qu'une fois — deuxiĂšme montage du mĂȘme objet = « Cannot redefine property »). - **`.env` local : `RATE_LIMIT_REQUESTS=600`** — les rafales E2E Playwright (5 tests × ~25 requĂȘtes) butaient sur le 60/min par IP ; dĂ©faut produit inchangĂ©. ### Notes - Reste ph3 : page_editor, board, agent_panel, import, gitea_workspace, welcome/accounts/trash/team_load/workspace/table_view/card_detail → puis bascule rĂ©elle (retrait `unsafe-eval`). ## v7.38.0 (2026-10-01) — A20 phase 3 LOT 1 : shell + library migres ### Added - **`e2e/csp_preview.spec.js`** — aperçu CSP strict SANS dĂ©ployer : le build `@alpinejs/csp` (fichier officiel, `e2e/fixtures/alpine.csp.js`) est servi **Ă  la place** de `alpine.min.js` par interception Playwright ; tout Ă©chec du parseur maison = `pageerror` (filet). **PremiĂšre surface verte : library** (composant liĂ©, icĂŽnes SVG rendues via `Alpine.effect`, recherche ouverte + focalisĂ©e, 0 erreur console/page). ### Changed - **Composants `x-data="fn()"` → registre `Alpine.data(...)`** (15 + `appState` + `libraryPage`) : le build CSP ne rĂ©sout que le registre (probe : globale window → `Undefined variable`) — scripts/classiques chargĂ©s pendant le parsing = `alpine:init` toujours joint Ă  temps. - **base.html (shell) migrĂ©** : `x-effect document.*` → `syncSidebarClass()`, `$nextTick(arrow)` → `initSidebarSort()`, `window.FlowDeck.*` → `fdCreatePage/fdCreateFolder/fdGwRefresh`, `Object.keys`/`Math.min`/ `window.innerWidth` dans `x-for`/`:style` → `sidebarSections()`/ `sectionMenuPos()` — toutes les formes = simple appel de mĂ©thode. - **x-html restants du shell** → `x-init` + `Alpine.effect` : icĂŽne agent (`bindAgentIcon`), carte projet (`bindProjectIcon`), library ×3 (`bindHtmlIcon`/`bindHtmlItem`), recherche library (`toggleSearch` avec `Alpine.nextTick`), `openMoveSelected` (library). - **eslint : 70 warnings → 0/0** : `getCsrf` (helper A38 ph1) dĂ©clarĂ© dans les globals du config, `/* exported openCardDetail */` + `/* global owner, repo */` (app.js), 3 `;;` rĂ©siduels de la conversion A38 supprimĂ©s. ### Notes - Portes A20 ph3 : surfaces restantes = settings, local_workspace, gitea_workspace, page_editor, board (partiels), agent_panel, import, welcome, accounts, trash, team_load, workspace, table_view, card_detail ; **bascule rĂ©elle** (retrait `unsafe-eval`) = quand csp_preview est vert sur toutes les pages principales. ## v7.37.0 (2026-10-01) — A20 : htmx allowEval off + plan Alpine CSP (phase 3) ### Changed - **htmx `allowEval: false`** dans le `` : htmx ne peut plus Ă©valuer de JS (`hx-on`/`hx-vars`/`hx-vals`) — grep = **0 usage** dans les templates, donc zĂ©ro rĂ©gression possible. `unsafe-eval` reste **uniquement** pour Alpine standard. - **Gate E20 renforcĂ©** : le smoke vĂ©rifie dĂ©sormais que `Alpine.$data()` lie un vrai composant `[x-data]` de la page (le lien composant = le cƓur de tout basculement CSP). ### Notes — A20 phase 3 (unsafe-eval, scopĂ© par probes) Le build `@alpinejs/csp` a Ă©tĂ© **testĂ© empiriquement** (fichier 72 Ko, **0 `eval`/`new Function`**, parseur d'expressions maison) : il tourne sous CSP strict, mais **bloquĂ© sur FlowDeck** par deux familles d'usages : - **13 expressions non parsables** par la grammaire restreinte : arrows (`$nextTick(() => 
)` ×2), `typeof` ×1, `new Date(
)` ×4, optional-chaining `?.` ×6 (base, library, local_workspace, settings, gitea_workspace) ; - **24 `x-html` rĂ©actifs** (icĂŽnes SVG `getSvgIcon`/`_fileIcon`, markdown agent, preview) — **interdits par le build CSP** (innerHTML) : nĂ©cessitent de reposer les icĂŽnes sur des composants `Alpine.data`. - PortĂ©e : 0 variable globale/`document` accessible dans les expressions du build CSP (scope = donnĂ©es du composant + magics) → chaque site devient une mĂ©thode de composant enregistrĂ©e via `Alpine.data`. **Plan phase 3** : migrer composant par composant (library → settings → local_workspace → gitea → base) avec gate E2E dĂ©diĂ©e, puis retirer `unsafe-eval`. En attendant : `unsafe-eval` conservĂ© (Alpine standard). ## v7.36.0 (2026-10-01) — Fondations E2E + 2 bugs trouvĂ©s au passage ### Added - **`e2e/smoke.spec.js`** — 2 gates vert contre l'instance de test : · **gate A39** : bascule de vues d'une collection (clic onglet Calendar → `?view_type=calendar`, grille `.calendar` + `.cal-header` rendue, collection créée puis **supprimĂ©e** = rĂ©pĂ©table) · **gate A20** : palette Ctrl+K (ouverture Alpine `.open`, recherche GET rend `.cmd-palette-item`, fermeture Échap) · **filet console** : 0 erreur JS/CSP (les violations atterrissent ici ; le bruit `Failed to load resource` 401/403 est filtrĂ©) · **Service Workers bloquĂ©s** dans le smoke : `/sw.js` sert sa page « hors ligne » sur les navigations redirigĂ©es (`redirect:'manual'`) — bruit PWA hors sujet, `pwa_offline.spec.js` couvre le SW · bootstrap autonome : login OU crĂ©ation du compte e2e documentĂ©, workspace si absent — lecture seule sur les donnĂ©es existantes - Commande : `cd e2e && node node_modules/@playwright/test/cli.js test smoke.spec.js` ### Fixed (trouvĂ©s par les gates) - **Bascule de vues standalone jamais fonctionnelle** : les onglets Ă©mettaient `?view=
` mais la route lit `view_type` (FastAPI) → l'onglet restait sur Table quel que soit le clic (bug prĂ©-existant, non introduit par A28). Onglets → `?view_type=` + assertion comportementale ajoutĂ©e Ă  `test_all_view_tabs_present` - **Inter bloquĂ© par la CSP depuis v7.27** : `app.css` importait encore Google Fonts (`@import` ratĂ© par le grep de v7.27) → violation `style-src` sur chaque page + police tombĂ©e en fallback. Inter **auto-hĂ©bergĂ©** : 2 faces variables (100-900, latin + latin-ext) dans `static/fonts/`, `@import` supprimĂ© ### Notes - Suite complĂšte : **1093/1093** · ruff OK · E2E **2/2** - Portes : A20 (unsafe-eval) attaquable avec ce filet ; A39 couvre la bascule collection (pas la bascule htmx board → dĂ©cision « rien » maintenue) ; A38 twins reste conditionnĂ© Ă  une couverture Ă©largie ## v7.35.0 (2026-10-01) — Audit : A35 TERMINÉ (Python 3.13 alignĂ© + rebuild) ### Changed - **Drift Python rĂ©solu** (dernier reliquat A35) : tout le projet est en **3.13** — `Dockerfile` (`python:3.13-slim` builder + runtime), CI Gitea (`python-version: '3.13'` ×2), `README.md` ×2, ruff `target-version = "py313"` (0 nouvelle remarque ruff). Restait 3.12 aux 6 endroits alors que `uv.lock` = `requires-python >=3.13` et le venv = 3.13.14. ZĂ©ro rĂ©fĂ©rence 3.12 rĂ©siduelle. ### Validation (le point que l'audit laissait « Ă  faire par rebuild ») - `docker build` **vert** sur `python:3.13-slim` (image `flowdeck:a35-py313`) - dans le conteneur : `python -V` = **3.13.16**, `import app.main` OK (version 7.35.0) → wheels `requirements.txt` construits et importables sur 3.13 - Suite locale complĂšte : **1093/1093** · ruff OK (target py313) ### Notes - A35 = **TERMINÉ** (OpenAPI/README/titre faits en 7.3.9 + drift aujourd'hui) ## v7.34.0 (2026-10-01) — Audit : A38 phase 2 (0 doublon de fonction globale) ### Changed - **Inventaire exhaustif de la duplication de fonctions** : scan de profondeur de brace sur les 13 noms dĂ©finis 2+ fois (templates + static/js, strings/comments/backticks gĂ©rĂ©s) → **12 sont dĂ©jĂ  scopĂ©s** dans des IIFEs depuis A27 (aucun conflit de page possible) - **Seul doublon global = `openCardDetail`** (corps byte-identiques ×2 dans `board_fragment` + `detailed_board`, fragments de vues mutuellement exclusifs) → dĂ©dupliquĂ©e vers `static/js/app.js`, les 2 copies supprimĂ©es (les onclick/@click des deux fragments appellent la mĂȘme dĂ©finition) ### Tests - `test_no_duplicate_global_functions` : garde-fou — 0 `function NAME` globale dĂ©finie 2+ fois entre templates et static/js (scanner naĂŻf, plafond `ponytail:` commentĂ© : un faux positif se lit au nom signalĂ©) - Suite complĂšte : **1092/1092** · ruff OK · node --check vert ### Notes - Reste A38 : mĂ©thodes jumelles library/local_workspace (~9-21 noms communs, corps divergents) → fusion `workspace-tree.js` reportĂ©e (rĂ©conciliation sans E2E — mĂȘme logique que A39/A20) ## v7.33.0 (2026-10-01) — Audit : A43 TERMINÉ + A38 phase 1 (CSRF unifiĂ©) ### Fixed - **A43-1 — plus de `__CSRF_PLACEHOLDER__` servi** : ContextVar `CSRF_TOKEN` posĂ©e par `CSRFMiddleware` **avant** `call_next` (mĂȘme mĂ©canique que le nonce CSP), global `{{ csrf_token() }}` ajoutĂ© Ă  templating ; `base.html` rend `{"X-CSRF-Token":{{ csrf_token()|tojson }}}` — vide si le cookie est absent sur cette 1Êłá”‰ requĂȘte, mais `htmx:configRequest` re-lit le cookie Ă  chaque appel → jamais de jeton factice. Test `test_csrf_server_rendered_no_placeholder` (pas de placeholder + token == cookie) - **A43-2 — palette : plus de re-parse par frappe** : `fetch('/api/search
')` sans header (GET ∈ `SAFE_METHODS` → le CSRF ne s'applique pas) — le `JSON.parse(document.body.getAttribute('hx-headers'))` par frappe disparaĂźt ### Changed - **A38 phase 1 — helper CSRF unique** : `window.getCsrf()` dĂ©fini dans le `` de `base.html` (le plus tĂŽt possible) ; **76 lectures brutes du cookie → `getCsrf()`** dans 13 fichiers (47 formes `(
||[])[1]||''`, 25 dĂ©clarations `const X = match(
)` + leurs usages `X?X[1]:''` → `X`, 4 formes espacĂ©es) ; dĂ©finitions dupliquĂ©es supprimĂ©es (`card_detail`, `database_table`) ; les 3 variantes de `base.html` (IIFE + 2 `getCsrfToken`) unifiĂ©es sur `return getCsrf()` ; `welcome.html` garde sa lecture locale (page autonome documentĂ©e) ### Notes - A43 = **TERMINÉ** (4/4 : utcnow dĂ©prĂ©ciĂ© = 0 dans app/**.py, health loggĂ©, placeholder, palette) - A38 reste : 12 fonctions dupliquĂ©es entre templates (wrappeur) + 21 mĂ©thodes jumelles library/local_workspace - Suite complĂšte : **1092/1092** · ruff OK · node --check vert ## v7.32.0 (2026-10-01) — Audit : A28 TERMINÉ (board, dernier lot) ### Changed - **A28 lot 4** : `app/routers/board.py` (**2 101 lignes, 53 routes**) devient le package `app/routers/board/` : · 12 modules : `pages` 271 L (7), `page_api` 229 (5), `board_views` 223 (8), `page_ops` 176 (3), `sharing` 175 (10), `synced` 144 (8), `page_media` 122 (4), `import_` 85 (2), `wiki` 76 (2), `library` 66 (1), `embed` 64 (2), `sync` 51 (1) · `_common.py` (878 L) : les **23 helpers dont 4 async** + les 4 constantes (STATUS_COLORS, STATUS_LABELS, AI_KEYWORD_COLORS, _REPO_REF_RE) · `__init__.py` : `__all__` complet — importateurs **inchangĂ©s** (api.py ×4 top-level, webhooks top-level, dashboard ×5 lazy, tests ×4) - Preuve contractuelle : **`docs/openapi-v2.json` rĂ©gĂ©nĂ©rĂ© = identique byte-Ă -byte** ### Fixed - Constantes d'Ă©tat oubliĂ©es dans `_common` Ă  la 1Êłá”‰ passe (F821 + ImportError au chargement) → rĂ©-insĂ©rĂ©es avec leurs valeurs exactes (git show) - Docstring du header copiĂ© → F404 (`from __future__` aprĂšs un statement) → slice `[1:21]` ; helpers **async** non dĂ©tectĂ©s par `def ` seul ### Notes - **A28 TERMINÉ en 4 lots** : api_v2 (7.29.0), dashboard (7.30.0), collections (7.31.0), board (7.32.0) — **0 changement d'URL** sur les 4 - Suite complĂšte : **1091/1091** · ruff OK ## v7.31.0 (2026-10-01) — Audit : A28 lot 3 (collections → package 13 fichiers) ### Changed - **A28 lot 3** : `app/routers/collections.py` (**2 622 lignes, 53 endpoints / 52 fonctions**) devient le package `app/routers/collections/` : · 10 modules de routes : `crud` 337 L (6), `properties` 322 (8), `linked` 286 (7), `structure` 267 (8), `dashboard_views` 214 (3), `meta` 197 (5), `views` 187 (6), `pages` 184 (4), `data_api` 122 (2), `boards` 61 (3) · `_common.py` (220 L) : 8 helpers auth/permissions/validation · `_renderers.py` (667 L) : **15 rendus HTML des vues** (_render_chart, _render_map, 
) + `CHART_MAX_GROUPS` · `__init__.py` : rĂ©-exports connus — `_validate_page_properties` (automations), `_chart_values`/`_chart_aggregate`/`_fmt_number`/ `_render_chart` (tests) - Preuve contractuelle : **`docs/openapi-v2.json` rĂ©gĂ©nĂ©rĂ© = identique byte-Ă -byte** ### Fixed (piĂšges de la dĂ©coupe) - docstring d'origine gardĂ©e dans le header copiĂ© → **F404** (`from __future__` aprĂšs un statement) : slice `[1:30]` - dĂ©corateurs **empilĂ©s** (`view_collection` ×2) : segment sans `def` → skip du 2ᔉ dĂ©corateur (53 endpoints = 52 unitĂ©s) - `CHART_MAX_GROUPS` hors dĂ©tection des helpers (F821 dans `dashboard_views`) → import `._renderers` ajoutĂ© - `test_csp_no_cdn_and_vendor` lisait `collections.py` → balayage du package ### Notes - Reste A28 : `board.py` 2 101 L (lot 4) - Suite complĂšte : **1091/1091** · ruff OK ## v7.30.0 (2026-10-01) — Audit : A28 lot 2 (dashboard → package 10 fichiers) ### Changed - **A28 lot 2** : `app/routers/dashboard.py` (**2 735 lignes, 63 routes**) devient le package `app/routers/dashboard/` : · `local_workspace` 559 L (15 routes), `pages_html` 485 (6), `account_settings` 439 (16), `workspace` 321 (9), `pages_api` 240 (6), `workspaces` 131 (6), `public` 78 (1), `account_api` 77 (4) · `_common.py` (774 L) : les **15 helpers top-level intercalĂ©s** dans l'ancien fichier + Ă©tat (`logger`, `_VERSION`, `WORKSPACE_COOKIE`) · `__init__.py` : re-export complet (les 7 importateurs existants — main, board ×3, my_tasks, web_clipper, wiki, sites, tests — n'ont rien changĂ©) + `__all__` - Preuve contractuelle : **`docs/openapi-v2.json` rĂ©gĂ©nĂ©rĂ© = identique byte-Ă -byte** (ordre d'enregistrement des routes prĂ©servĂ©) ### Fixed (piĂšges de la dĂ©coupe, rattrapĂ©s) - segment dĂ©corateur sans sa fonction (`def`) → corps perdus en silence : assert `def in seg` ajoutĂ©e + rĂ©cupĂ©ration depuis git - collision `settings` : la section `settings.py` entrait en conflit avec `from app.config import settings` du header (`hasattr` du fromlist de Python → la section Ă©tait ignorĂ©e) → renommĂ©e `account_settings` - `WORKSPACE_COOKIE` utilisĂ© dans `workspaces.py` sans import (F821) → import `._common` ajoutĂ© - script `__all__` tronquant la fin du fichier → `__init__.py` réécrit ### Notes - Reste A28 : `collections.py` 2 622 L, `board.py` 2 101 L - Suite complĂšte : **1091/1091** · ruff OK ## v7.29.0 (2026-10-01) — Audit : A28 lot 1 (api_v2 → package 14 fichiers) ### Changed - **A28 lot 1** : `app/routers/api_v2.py` (**2 110 lignes, 115 routes**) devient le package `app/routers/api_v2/` — un module par concern : · `collections` 566 L (23 routes), `engagement` 338 (21), `workspaces` 230 (9), `templates_io` 205 (9), `webhooks` 195 (8), `identity` 195 (7), `views` 164 (8), `sharing` 160 (8), `properties` 151 (7), `planning` 148 (7), `projects` 93 (4), `admin` 91 (4) · `_common.py` : helpers partagĂ©s (`_hash`, `_v2_rate_check`) · `__init__.py` : `router = APIRouter(prefix="/api/v2")` + agrĂ©gat `include_router` (section routers sans prefix, tags `api-v2`) - Preuve contractuelle : **`docs/openapi-v2.json` rĂ©gĂ©nĂ©rĂ© = identique byte-Ă -byte** (0 changement de chemin, tag ni operation_id) - Seul importateur (`app/main.py` → `from app.routers.api_v2 import router`) fonctionne via le package ; en-tĂȘte d'imports copiĂ© par module, Ă©mondĂ© par `ruff --fix` (143 imports morts supprimĂ©s automatiquement) ### Notes - Reste A28 : `dashboard.py` 2 735 L, `collections.py` 2 622 L, `board.py` 2 101 L (mĂȘme recette, lots suivants) - Suite complĂšte : **1091/1091** · ruff OK ## 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Ă©) + `