From 3a74ea8bbde0fc64d3faec340fbc9e2f834c0f45 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Fri, 2 Oct 2026 09:39:02 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20A35=20TERMIN=C3=89=20=E2=80=94=20drift?= =?UTF-8?q?=20Python=203.12=E2=86=923.13=20align=C3=A9,=20rebuild=20valid?= =?UTF-8?q?=C3=A9=20(v7.35.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dernier reliquat de l'A35 (docs/périmètre dérivés) : - Dockerfile : python:3.12-slim → python:3.13-slim (builder + runtime) - .gitea/workflows/ci.yml : python-version '3.12' → '3.13' (lint + test) - README.md : « Python 3.12 » et « python:3.12-slim » → 3.13 (×2) - pyproject.toml : ruff target-version py312 → py313 (0 nouvelle remarque ruff) - zéro référence 3.12 résiduelle ; uv.lock (requires-python >=3.13) et le venv (3.13.14) étaient déjà bons Validation (le point laissé « à faire par un rebuild d'image ») : - docker build VERT sur python:3.13-slim → image flowdeck:a35-py313 - dans le conteneur : python -V = 3.13.16, `import app.main` OK (v7.35.0) → wheels requirements.txt construits + importables sur 3.13 A35 = TERMINÉ (OpenAPI/README/titre dupliqué faits en 7.3.9 + drift). suite **1093/1093** · ruff OK (target py313) · docs à jour --- .gitea/workflows/ci.yml | 4 ++-- CHANGELOG.md | 23 +++++++++++++++++++++++ Dockerfile | 4 ++-- README.md | 4 ++-- ROADMAP.md | 4 ++-- VERSION | 2 +- WORKLOAD.md | 2 +- app/main.py | 2 +- docs/openapi-v2.json | 2 +- pyproject.toml | 2 +- 10 files changed, 36 insertions(+), 13 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index f5d75f9..f8aae8c 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: '3.13' - name: Install lint tools run: pip install -r requirements-dev.txt - name: Ruff (Python) @@ -31,7 +31,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: '3.13' - name: Install system dependencies (WeasyPrint / emoji fonts) run: |- SUDO="" diff --git a/CHANGELOG.md b/CHANGELOG.md index ac808c2..be4addc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog - FlowDeck +## 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 diff --git a/Dockerfile b/Dockerfile index 32eff4f..789a6db 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ # Stage 1 "builder": build Python wheels once. # Stage 2 "runtime": minimal image with WeasyPrint system libs. # ═══════════════════════════════════════════════════════════ -FROM python:3.12-slim AS builder +FROM python:3.13-slim AS builder WORKDIR /app @@ -11,7 +11,7 @@ COPY requirements.txt . RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt # ── runtime stage ─────────────────────────────────────────── -FROM python:3.12-slim AS runtime +FROM python:3.13-slim AS runtime WORKDIR /app diff --git a/README.md b/README.md index a53cc90..7595806 100644 --- a/README.md +++ b/README.md @@ -73,9 +73,9 @@ docker compose up -d | Couche | Techno | |--------|--------| | Frontend | Jinja2 + HTMX + Alpine.js + SortableJS + CSS | -| Backend | Python 3.12 + FastAPI + httpx | +| Backend | Python 3.13 + FastAPI + httpx | | BDD | SQLite (WAL, 21 tables) — `/data/flowdeck.db` | -| Déploiement | Docker (python:3.12-slim), docker-compose | +| Déploiement | Docker (python:3.13-slim), docker-compose | ## Configuration diff --git a/ROADMAP.md b/ROADMAP.md index 066c399..4550ac3 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1155,7 +1155,7 @@ Quality DB views, Agent IA Palette → Realtime + E - [x] **A32 — Couverture de tests par trou — TERMINÉ 2026-10-01** : routers à **0 test** : `webhooks.py` (0/3), `notes.py` (0/2), `sidebar_config.py` (0/2), `github_routes.py` (0/2) ; quasi nuls : `library.py` 1/10, `api.py` 3/23 (move, col-mapping, board-config, CRUD issues), `dashboard.py` 17/63, `api_v2.py` 50/115. Points positifs vérifiés : 1 002 tests, **aucun sans `assert`**, aucun qui touche le réseau réel. *Fix : 1 smoke test par route non couverte (fixture TestClient existante).* — **phase 1 faite 2026-10-01** : les **4 routers à 0 test sont couverts** — `tests/test_smoke_uncovered.py`, 10 tests : webhooks (receive ok sans secret, 401 HMAC via monkeypatch, register 400 sans appel réseau, status avec `gitea.list_webhooks` stubbé), notes (GET HTML + roundtrip upsert avec échappement), sidebar_config (GET, PUT persisté relu en base, 400 sans config + remise en état), github_routes (status/disconnect) — **0 accès réseau réel**. **Phase 2a faite 2026-10-01** : `library.py` **1/10 → 8 routes couvertes** (5 listes en 1 test + private seedé, children avec parent/enfant seedé, repository vide/clé, + test de non-régression post-suppression). **Découverte** : `/api/library/local-workspace-children/{id}` (toujours 500) et `/api/library/local-workspace` (500 dès qu'un workspace existe) lisaient `local_workspace_items` — **une table inexistante nulle part dans le codebase**, zéro référence front → **2 routes + `_format_size` supprimés** (test 404 anti-réintroduction). **Phase 2b faite 2026-10-01 (partielle — api.py)** : **3/23 → 16/22 routes couvertes** (mesure sur les `@router` du fichier) : board-config (défauts + roundtrip création), col-mapping (404 sans board, upsert + suppression), card (404 + ok), collaborators (**`gitea.get_collaborators` stubbé**, zéro réseau), frontend-error (capture, JSON invalide → ignored, dédup `count=2`, lecture qui purge), checklist-items PATCH/DELETE + checklists DELETE (mutations **vérifiées en base** avec seed + cleanup). **Reste api.py** : 6 routes **Gitea** (issues ×4, checklists/checklist-items création avec owner/repo) → stub de transport httpx + formes de réponse par endpoint. **Phase 2c faite 2026-10-01 (api_v2)** : scan strict des 115 routes vs tests → **5 routes à 0 référence** couvertes : `evaluate-formula` (200 + 400 sans expression ; le moteur renvoie `1 + 2` tel quel — on teste le câble, pas le moteur), `compute-rollup` (400 `collection_id required` + 401 sans bearer), `admin/audit-logs` (**portail vérifié** : attendu calculé depuis `/users/me` car le tout premier utilisateur du worker est admin, + token scope `admin` → 200), `webhooks/events` (catalogue + wildcards), `webhooks/verify-signature` (**valid True avec `sign_payload`, False avec signature bidon**). **Phase 2d faite 2026-10-01 (dashboard, partiel)** : scan strict → 44 routes à 0 ref stricte sur 63 ; **10 couvertes** cette passe : tags CRUD complet (création lowercasée, liste, couleur relue, suppression), page content→rename(+400 titre vide)→trash (title/parent_section/deleted_at **relus en base**), sidebar workspace-tree (fragment HTML « No pages yet »), avatar-color (couleur **relue sur l'utilisateur de la session** puis restaurée), workspace members (shape). **Phase 2e faite 2026-10-01** : +**9 routes** (cumul dashboard : 27→36 des 63) : pages `/accounts` + `/accounts/settings` (HTML, « password_hash » **absent** du rendu — whitelist A2), `PUT /api/user/profile` (persistance relue + restauration), `PUT /api/user/password` (**403 sans mdp actuel correct** = A3 ; quirk assumé validé : longueur avant auth en 200+erreur), `POST /api/user/token` (format `fd_`+64 hex, `user_tokens` nettoyé), `DELETE /api/user/forge/{provider}`, `PUT /api/settings/account` (persistance + **400 mdp court**, restauration), `POST /api/workspaces/{id}/select` (cookie `Set-Cookie` vérifié), `GET /api/local-workspace/breadcrumb`. **Phase 2f faite 2026-10-01** : +**7 routes** (cumul dashboard **36→43/63**) centrées sur les **garde-fous A16** : `/api/files/{ws}/{path}` — traversal `%2e%2e%2f` → **403 « Path traversal denied »**, 404 inexistant, 200 + octets pour un vrai fichier écrit dans le data_dir de test (nettoyé) ; `download` (page markdown → 404 propre) et `file-content` (page « file » dont le chemin sort de la racine → **jamais 200**, 404/415) ; `local-workspace/page-content` (contenu relu + 404) ; `/api/avatar/{id}` — **302 avec `follow_redirects=False`** (aucune requête réelle vers l'URL externe) + 404 ; `collections/{id}/table-data` (404 + shape) et `POST collections/{id}/pages` (ligne créée **retrouvée dans table-data**). **Phase 2g faite 2026-10-01** : +**13 routes** (cumul dashboard **43→56/63**) : `/gitea-workspace` (HTML), projects GET/POST (shape + création retrouvée dans `builtin` + quirk `error` sans nom), **cycle complet des items local-workspace** (POST → rename relu → move → soft-delete `deleted_at` relu → restore `NULL` relu = 5 routes), **cycle des tags d'item** (POST lowercasé → lecture item → liste workspace → search → suppression, avec utilisateur + workspace **créés et nettoyés dans le test** : `/api/local-workspace/tags` exige un workspace actif, fallback « premier workspace du user »). **Phase 2h faite 2026-10-01 → DASHBOARD BLOQUÉ** : +5 routes (members invite **avec quirk tuple FastAPI documenté** → rôle invalides/emails inconnus répondent 200+tableau, role relu en base, membre supprimé ; upload-folder **validations seules** via structure absente/cassée = 400, zéro fichier écrit ; convert-to-database = collection + propriété title + vue Table + page en `content_format='collection'` vérifiés, cleanup dans l'ordre FK). **Recoupement final** : les 44 routes strictement à 0 ref de `dashboard.py` sont TOUTES exercées — le scan résiduel affiche 19 faux positifs (paths en f-string dans les tests : `/api/workspace/1/…`, `f"/api/pages/{id}/…"`, etc.) rapprochés manuellement un par un. **TERMINÉ 2026-10-01** : les **6 routes Gitea** d'`api.py` stubbées à la main (`gitea_client.gitea.create_issue/update_issue/get_issue/get_issue_comments` → canevas **mutable partagé** car le PATCH re-fetch via `get_issue`) : create avec **carte insérée sur le board**, PATCH avec colonne recalculée sans perdre la carte, détail JSON + **HTML** (`?format=html` — le segment `/html` ne fixe pas le paramètre, le handler le lit dans la query), 404 quand le stub lève, checklists POST ×2 vérifiées en base + 404 sans board. **BUG PROD TROUVÉ ET CORRIGÉ** : `card_detail.html` utilisait `fd_icon` sans l'importer → **500 systématique** sur `GET /api/issues/...?format=html` (route unique à rendre ce template) → `{% from '_icons.html' import fd_icon %}` ajouté. **A32 complet** : 0-router-à-0-test restant (webhooks/notes/sidebar/github/library/api/dashboard/api_v2 tous couverts). - [x] **A33 — Rate limit incomplet et mal câblé** : `security.py:98` ne couvre que `/api/`, `/board/api/`, `/auth/` — pas `/scim/v2`, `/workspace`, `/db/`, `/s/{slug}/auth` (brute force du mot de passe de site, `sites.py:599`), ni `/f/` ; `max_requests=100` codé en dur alors que `settings.rate_limit_requests=60` n'est **jamais lu** ; clé = `request.client.host` (tous les users derrière 1 proxy = 1 seau) ; `_store` (`security.py:113,134-145`) **jamais épuré** → croissance mémoire par IP. `config.py:44` ment donc sur la valeur. *Fix : lire le settings, ajouter les préfixes, épurage, `X-Forwarded-For`. Effort : **S**.* - [x] **A34 — 10 schedulers sans observabilité** : `main.py:90-124` — les boucles **ont** bien un `try` interne (vérifié), mais **aucun `add_done_callback` ni restart** : une exception hors `try` tue la tâche en silence ; `calendar_sync.py:469` et `automations.py:484` loggent leurs échecs en `logger.debug` (invisibles à `LOG_LEVEL=INFO`) ; le `finally` (`main.py:136-139`) ne catch que `CancelledError` → un task mort re-raise à l'arrêt. *Fix : helper `spawn()` avec `add_done_callback` (log + recreé) + passer les 2 debug en warning. Effort : **S**.* -- [x] **A35 — Docs/périmètre dérivés** : `docs/openapi-v2.json` = `info.version 6.7.0`, **439 chemins vs 511 réels** (v6.8→v7.3 non documentés) · `README.md:5` = v6.7.0 alors que `VERSION=7.3.0` · `API_GUIDE_V6.md:8` = « 427 chemins » · **ROADMAP titre dupliqué** `## 🎯 Ordre de priorité (état 2026-09-28)` aux lignes 1052 (vide) et 1065 · drift Python : Dockerfile/CI/README = 3.12, venv local = 3.13, `uv.lock` ≥3.13, ruff target py312. *Fix : régénérer l'OpenAPI à chaque bump (`app.openapi()`), une passe README, dédoublonner la section, aligner 3.13 partout. Effort : **S**. — **fait 2026-10-01** : OpenAPI 511 chemins / 7.3.9, README, API_GUIDE, titre dupliqué retiré ; **reste le drift Python** (Docker/CI/README 3.12 vs venv 3.13 : alignement à valider par un rebuild d'image).* +- [x] **A35 — Docs/périmètre dérivés** : `docs/openapi-v2.json` = `info.version 6.7.0`, **439 chemins vs 511 réels** (v6.8→v7.3 non documentés) · `README.md:5` = v6.7.0 alors que `VERSION=7.3.0` · `API_GUIDE_V6.md:8` = « 427 chemins » · **ROADMAP titre dupliqué** `## 🎯 Ordre de priorité (état 2026-09-28)` aux lignes 1052 (vide) et 1065 · drift Python : Dockerfile/CI/README = 3.12, venv local = 3.13, `uv.lock` ≥3.13, ruff target py312. *Fix : régénérer l'OpenAPI à chaque bump (`app.openapi()`), une passe README, dédoublonner la section, aligner 3.13 partout. Effort : **S**. — **fait 2026-10-01** : OpenAPI 511 chemins / 7.3.9, README, API_GUIDE, titre dupliqué retiré ; **reste le drift Python** (Docker/CI/README 3.12 vs venv 3.13 : alignement à valider par un rebuild d'image).* — **drift aligné + rebuild validé 2026-10-01 → A35 TERMINÉ** : `python:3.13-slim` (builder + runtime), CI `python-version: '3.13'` ×2, README ×2, ruff `target-version = "py313"` (0 retour ruff) — plus aucune référence 3.12 hors uv.lock (`requires-python >=3.13` déjà bon) ; **validation réelle** : `docker build` vert sur `python:3.13-slim` (image `flowdeck:a35-py313`) + `python -V` = **3.13.16** dans le conteneur + `import app.main` OK (v7.35.0) → les wheels de `requirements.txt` se construisent bien sur 3.13. → suite 1093/1093, version 7.35.0. - [x] **A36 — Chaîne de dépendances cassée** : `pyproject.toml` **sans `[project]` ni `dependencies`** (35 lignes, que pytest+ruff), `uv.lock` gitignoré (`.gitignore:19`) et réduit à 3 lignes → aucun verrouillage reproductible ; deps mortes dans `requirements.txt` : **`aiosqlite`, `slowapi`, `loguru`, `packaging` = 0 import** (le rate-limit maison a remplacé slowapi). *Fix : purger les 4 mortes, soit `[project].dependencies`, soit un lock réel. Effort : **S**.* - [x] **A37 — CORS `allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]`** (`main.py:154`) alors que l'auth est cookie de session (avec A19 qui désactive le CSRF sur la plupart des routes) — et `allow_credentials` n'est pas posé. *Fix : origines explicites (`app_base_url` + frontends connus). Effort : **XS**.* @@ -1181,4 +1181,4 @@ Quality DB views, Agent IA Palette → Realtime + E → Puis **A3–A8** (le bloc « fallback admin ») d'un seul tenant, puis **A10** (autoescape) qui débloque A18/A20. *Audit produit le 2026-09-30 · 43 items · aucun code modifié ( ROADMAP seul ).* -→ **A1–A9 corrigés le 2026-09-30** : deps réinstallées (`pyotp`/`webauthn`/`cbor2`), rebinding de `settings` supprimé dans `test_v54.py` → **suite 1016/1016 verts**, cycle committé (`1706ad1`) + tag `v7.3.0` poussé, `.db`/fichiers de test désindexés, `APP_SECRET_KEY` roté dans `.env` (sessions révoquées) · **A3–A8 : 401 sans session sur les routes de compte (mdp actuel exigé), tokens `/api/v1` + `/api/user` sans session → 401, CRUD membres d'espace sous session+role admin, `_require_view`/`_require_edit` sans session → 404/401, création/lecture de page sous session, `/board/api/pages` + `/api/user` sortis du CSRF exempt, seed admin sans mdp en dur (aléatoire ou `FLOWDECK_ADMIN_PASSWORD`). Tests : client connecte par defaut (`_TestSessionAuth`), helper `anon()` sur les 40 tests d'anonymat → suite 1016/1016 + ruff OK, commit `d125eb3` · **A10 : `app/templating.py` (ENV partagé + autoescape `select_autoescape(["html"])`) remplace les 29 instantiations, `|safe` retriés (corps d'issue/commentaires echappes, `sidebar_config` en `|tojson`) → suite 1016/1016, version 7.3.1 · **A11 (traversal avatar) + A18 (vue publique : 404 restricted/private + html.escape)** : `tests/test_audit_p0_fixes.py`, suite 1019/1019, version 7.3.2 · **A12/A13/A15/A17/A22/A23/A24** : garde SSRF par hop, routers automations + /api sous session/Bearer, webhooks admin+URL publique, uploads validés, N+1 en GROUP BY/executemany, 2 doublons de routes supprimés → suite 1025/1025, version 7.3.3 · **A16** : exports + pièces jointes sous session + `can_view_page` → suite 1026/1026, version 7.3.4 · **A19 (partiel)** : 12 préfixes CSRF retirés après scan front (12 préfixes = tous ceux dont les appels portent déjà le header), reste 5 préfixes / 49 fetchs → suite 1026/1026, version 7.3.5 · **A19 terminé** : 46 call sites front équipés, plus aucun préfixe cookie-auth exempté → suite 1026/1026, version 7.3.6 · **A14** : fallback `admin` supprimé de `_current_user_id`/`_current_admin` (401 sans session), `api_base` validé scheme http(s) sans identifiants (hôtes privés acceptés = Ollama localhost, commentaire `ponytail:`) → suite 1027/1027, version 7.3.7 · **A25** : 84 `except Exception: pass` remplacés par `logger.exception(fn)` (19 fichiers, +`logger` manquants), try supprimé sur `materialize_properties` dans `create_collection_v2` et `apply_db_template_v2` (rollback au lieu d'un commit sans schéma), test de rollback · **A21 (partiel)** : `busy_timeout=5000` dans `get_conn()` → suite 1028/1028, version 7.3.8 · **A26/A33/A34/A35/A36/A43** : secret par défaut refusé au boot, rate limit (préfixes + settings + XFF + épurage), `_spawn()` pour les 10 schedulers, OpenAPI 511 chemins + README, 4 deps mortes purgées, 15 `utcnow()` → `now(UTC)` naïf → suite 1028/1028, version 7.3.9. · **A30/A37/A39/A40/A41** : `require_scope` câblé sur 69 sites + 12 fonctions mortes supprimées, CORS sans `*` (origines de `app_base_url` + regex dev/extensions), assets versionnés depuis `VERSION` (source unique), `app.css` -10,2 Ko de règles mortes, htmx = décision « rien » documentée → suite 1031/1031, version 7.4.0. · **A29/A42** : `services/publish.py` partagé (3 routers déléguent, 404 partout, board sous session), fuite `password_hash` corrigée sur `GET /api/users/me` v1 + contexte `/accounts`, `settings.data_dir` remplace les 9 copies d'env, cache Gitea évacue les expirés ; `/users/me` ×2 + collections ×3 = contrats versionnés, on garde ; reste A42 = client httpx partagé → suite 1034/1034, version 7.5.0. · **A31** : transaction par migration (`_apply_one`, rollback tout-ou-rien du DDL) + helper `columns()` remplaçant 25 copies de `PRAGMA table_info` (1 helper au lieu de 3 — les 2 autres seraient mort-nés) → suite 1036/1036, version 7.6.0. · **A20 (partiel)** : CSP nonce par requête — `unsafe-inline` retiré de `script-src`, 38 scripts templates + login constant + 3 scripts Python noncés, meta `htmx-config` pour htmx, `script-src-attr` pour les 74 `onclick=`, CDN chart/leaflet débloqués (déjà cassés avant) → suite 1037/1037, version 7.7.0. · **A21 phase 1** : 352 routes `async def` sans `await` → `def` (threadpool FastAPI, SQLite hors loop, zéro changement de logique — scan corps par corps) ; reste phase 2 = 311 routes avec `await` → `anyio.to_thread.run_sync` par bloc DB → suite 1037/1037, version 7.8.0. **Phase 2c faite 2026-10-01** : **+190 routes hors loop** (283 → 93 async, **86 % des 667 routes**) en 4 passes : (A) **racine auth** — `get_current_user` (session.py) était `async def` SANS aucun await (cookie decode = synchrone) + ses clones async (`agent._current_user_id/_workspace_id/_current_admin`, `sso._require_admin`) → `def`, **47 `await` supprimés** (dont 3 via l'alias `gcu`) ; (B) re-scan → 19 routes sans await flipées ; (C/D) **155 routes** json/événements → `Body(default={})` (formes : try/except `body = {}`, try/except `raise HTTPException(400)` → `Body(...)` requis (422 FastAPI, aucun test ne couvrait le 400), forme conditionnelle content-type → défaut `{}`) + `run_event_sync`. **Reste async (93, justifié)** : `request.form`/`upload.read`/`file.read` (14+5+3, corps de requête réellement asynchrone), gitea/llm/oidc (réseau), `_json_body` 9 (wrapper de validation), 2 JSON inline en argument, 1 fallback à logique (capture_frontend_error), 1 lecture conditionnelle dans web_clipper. Échecs : 3 seeds d'aliases `gcu` attrapés par la suite → corrigés → suite 1089/1089, version 7.26.0. · **A21 phase 2a** : `body` JSON → paramètre `Body(default={})` sur les 36 routes api_v2 dont c'était le seul `await` → conversion en `def` → api_v2 à 96/115 hors loop (19 async restantes : fire_event/form/gitea) → suite 1037/1037, version 7.9.0. · **A21 phase 2b** : `run_event_sync()` (asyncio.run dans le worker, événement attendu = déterministe) + les 15 routes json/événements en `def` → api_v2 bouclé à 111/115 (4 async = vrais awaits réseau) ; repo-wide 403 sync / 260 async → suite 1037/1037, version 7.10.0. · **A32 phase 1** : les 4 routers à 0 test couverts (webhooks/notes/sidebar_config/github_routes = 10 smokes, 0 réseau réel, échappement notes vérifié) ; reste quasi nuls library/api/dashboard/api_v2 → suite 1047/1047, version 7.11.0. · **A32 phase 2a** : library 1/10 → 8 routes couvertes ; découverte = 2 routes lisant la table fantôme `local_workspace_items` (500 systématique, 0 ref front) supprimées + `_format_size` mort → suite 1053/1053, version 7.12.0. · **A32 phase 2b** : api.py 3 → 16/22 routes couvertes (board-config, col-mapping, card, collaborators stubbé, frontend-error dédup, mutations checklist vérifiées en base) ; reste 6 routes gitea + dashboard/api_v2 → suite 1059/1059, version 7.13.0. · **A32 phase 2c** : api_v2 scan strict → 5 routes à 0 ref couvertes (formula, rollup, audit-logs avec portail vérifié, webhooks/events, verify-signature signé) → suite 1064/1064, version 7.14.0. · **A32 phase 2d** : dashboard scan strict = 44 routes à 0 ref, 10 couvertes (tags CRUD, page content→rename→trash vérifiés en base, tree HTML, avatar-color restauré, members) → suite 1069/1069, version 7.15.0. · **A32 phase 2e** : +9 routes dashboard (pages comptes sans hash, profile/password A3 403, token fd_+64hex, forge, settings/account 400, select cookie, breadcrumb) → suite 1075/1075, version 7.16.0. · **A32 phase 2f** : +7 routes dashboard centrées A16 (files traversal 403, download/file-content sans fuite, avatar 302 sans réseau, table-data + création de ligne) → suite 1079/1079, version 7.17.0. · **A32 phase 2g** : +13 routes dashboard (gitea-workspace HTML, projects CRUD, cycle items 5 routes, cycle tags d'item 5 routes avec ws dédié) → suite 1083/1083, version 7.18.0. · **A32 phase 2h → dashboard bloqué** : members (cycle complet + quirk tuple), upload-folder (validations seules), convert-to-database (vérifié en base, ordre FK) ; les 44 routes à 0 ref sont toutes exercées (faux positifs f-string rapprochés) → suite 1086/1086, version 7.19.0. · **A32 TERMINÉ** : 6 routes Gitea stubbées (canevas mutable, carte board, HTML `?format=html`, 404) + **bug prod `card_detail.html`/`fd_icon` corrigé** (500 garanti avant) → suite 1089/1089, version 7.20.0. · **A27 phase 1** : 7 templates sans Jinja → 9 fichiers static/js (4 243 L, -30 % du inline, node --check vert), eslint installé globalement → `eslint static/js` 0 erreur/120 warnings ; reste = blocs interpolés Jinja (~9 661 L) → suite 1089/1089, version 7.21.0. · **A27 phase 2a** : éditeur 2 516 L extrait via `#page-data` (PD.*), dérivé `is_shared` hoisté, 8 tests adaptés (source → static js / parsing JSON) → suite 1089/1089, version 7.22.0. · **A27 phase 2b** : local_workspace 2031 + settings 1093 + realtime 531 + board 146 extraits (recette config JSON, tojson au lieu d'interpolation JS) → suite 1089/1089, version 7.23.0. · **A27 phase 2c → extraction TERMINÉE** : database_table 1314 L (config JSON null-vs-objet, 2 branches Jinja remplacées par `DB_CONFIG`), bilan **11 874 L extraites, inline -85 % (13 904 → 2 022)** → suite 1089/1089, version 7.24.0. · **A27 lint TERMINÉ** : eslint 285 warnings → **0/0** (catch vides → binding optionnel, 24 lignes mortes, 10 fonctions `/* exported */` appelées par le HTML, 3 globaux réels en config) + 2 vrais correctifs (toasts settings jamais affichés → `window.showToast`, `_wsInitData` sans effet supprimé) → suite 1089/1089, version 7.25.0. · **A21 phase 2c → A21 TERMINÉ** : auth racine sync (47 await retirés, alias gcu rattrapé) + 155 routes json/événements → Body/run_event_sync + 19 flips → **283→93 async (86 % hors loop)** → suite 1089/1089, version 7.26.0. · **A20 phase 2** : chart/leaflet vendorisés (static/js/vendor, 8 fichiers), CSP sans aucun hôte CDN, connect-src `'self' ws://{host} wss://{host}` (exfil fermé), Google Fonts morts retirés, img-src https: gardé (unfurls) → suite 1090/1090, version 7.27.0 — reste A20 = unsafe-eval (Alpine+htmx, E2E d'abord). · **A42 TERMINÉ** : `shared_client` (cache par boucle+kwargs, WeakKeyDictionary) remplace 49 créations httpx dans 14 fichiers, 2 sites laissés (transport injecté / own_client), stub webhooks étendu à la fabrique → suite 1091/1091, version 7.28.0. · **A28 lot 1** : api_v2.py (2 110 L, 115 routes) → package de 14 fichiers par concern, openapi JSON IDENTIQUE byte-à-byte (0 changement d'URL) → suite 1091/1091, version 7.29.0 · **A28 lot 2** : dashboard.py (2 735 L, 63 routes) → package de 10 fichiers (15 helpers dans _common, re-exports intacts, collision `settings`→`account_settings`), openapi IDENTIQUE byte-à-byte → suite 1091/1091, version 7.30.0 · **A28 lot 3** : collections.py (2 622 L, 53 endpoints) → package de 13 fichiers (_renderers = 15 rendus, décorateurs empilés gérés, F404 docstring corrigé), openapi IDENTIQUE byte-à-byte → suite 1091/1091, version 7.31.0. · **A28 TERMINÉ (lot 4 : board)** : 2 101 L → package de 14 fichiers (23 helpers dont 4 async dans _common, constantes ré-insérées), openapi IDENTIQUE byte-à-byte → suite 1091/1091, version 7.32.0 — les 4 god files du audit sont découpés. · **A43 TERMINÉ + A38 phase 1** : CSRF rendu côté serveur (ContextVar, plus de placeholder), palette sans re-parse par frappe, 76 lectures cookie → `getCsrf()` global head, 2 déf dupliquées supprimées → suite 1092/1092, version 7.33.0 · **A38 phase 2** : 13 noms dupliqués audités (12 scopés IIFE), seul doublon global openCardDetail dédupliqué dans app.js + test garde-fou 0-doublon → suite 1092/1092, version 7.34.0 — reste : méthodes jumelles (E2E d'abord). +→ **A1–A9 corrigés le 2026-09-30** : deps réinstallées (`pyotp`/`webauthn`/`cbor2`), rebinding de `settings` supprimé dans `test_v54.py` → **suite 1016/1016 verts**, cycle committé (`1706ad1`) + tag `v7.3.0` poussé, `.db`/fichiers de test désindexés, `APP_SECRET_KEY` roté dans `.env` (sessions révoquées) · **A3–A8 : 401 sans session sur les routes de compte (mdp actuel exigé), tokens `/api/v1` + `/api/user` sans session → 401, CRUD membres d'espace sous session+role admin, `_require_view`/`_require_edit` sans session → 404/401, création/lecture de page sous session, `/board/api/pages` + `/api/user` sortis du CSRF exempt, seed admin sans mdp en dur (aléatoire ou `FLOWDECK_ADMIN_PASSWORD`). Tests : client connecte par defaut (`_TestSessionAuth`), helper `anon()` sur les 40 tests d'anonymat → suite 1016/1016 + ruff OK, commit `d125eb3` · **A10 : `app/templating.py` (ENV partagé + autoescape `select_autoescape(["html"])`) remplace les 29 instantiations, `|safe` retriés (corps d'issue/commentaires echappes, `sidebar_config` en `|tojson`) → suite 1016/1016, version 7.3.1 · **A11 (traversal avatar) + A18 (vue publique : 404 restricted/private + html.escape)** : `tests/test_audit_p0_fixes.py`, suite 1019/1019, version 7.3.2 · **A12/A13/A15/A17/A22/A23/A24** : garde SSRF par hop, routers automations + /api sous session/Bearer, webhooks admin+URL publique, uploads validés, N+1 en GROUP BY/executemany, 2 doublons de routes supprimés → suite 1025/1025, version 7.3.3 · **A16** : exports + pièces jointes sous session + `can_view_page` → suite 1026/1026, version 7.3.4 · **A19 (partiel)** : 12 préfixes CSRF retirés après scan front (12 préfixes = tous ceux dont les appels portent déjà le header), reste 5 préfixes / 49 fetchs → suite 1026/1026, version 7.3.5 · **A19 terminé** : 46 call sites front équipés, plus aucun préfixe cookie-auth exempté → suite 1026/1026, version 7.3.6 · **A14** : fallback `admin` supprimé de `_current_user_id`/`_current_admin` (401 sans session), `api_base` validé scheme http(s) sans identifiants (hôtes privés acceptés = Ollama localhost, commentaire `ponytail:`) → suite 1027/1027, version 7.3.7 · **A25** : 84 `except Exception: pass` remplacés par `logger.exception(fn)` (19 fichiers, +`logger` manquants), try supprimé sur `materialize_properties` dans `create_collection_v2` et `apply_db_template_v2` (rollback au lieu d'un commit sans schéma), test de rollback · **A21 (partiel)** : `busy_timeout=5000` dans `get_conn()` → suite 1028/1028, version 7.3.8 · **A26/A33/A34/A35/A36/A43** : secret par défaut refusé au boot, rate limit (préfixes + settings + XFF + épurage), `_spawn()` pour les 10 schedulers, OpenAPI 511 chemins + README, 4 deps mortes purgées, 15 `utcnow()` → `now(UTC)` naïf → suite 1028/1028, version 7.3.9. · **A30/A37/A39/A40/A41** : `require_scope` câblé sur 69 sites + 12 fonctions mortes supprimées, CORS sans `*` (origines de `app_base_url` + regex dev/extensions), assets versionnés depuis `VERSION` (source unique), `app.css` -10,2 Ko de règles mortes, htmx = décision « rien » documentée → suite 1031/1031, version 7.4.0. · **A29/A42** : `services/publish.py` partagé (3 routers déléguent, 404 partout, board sous session), fuite `password_hash` corrigée sur `GET /api/users/me` v1 + contexte `/accounts`, `settings.data_dir` remplace les 9 copies d'env, cache Gitea évacue les expirés ; `/users/me` ×2 + collections ×3 = contrats versionnés, on garde ; reste A42 = client httpx partagé → suite 1034/1034, version 7.5.0. · **A31** : transaction par migration (`_apply_one`, rollback tout-ou-rien du DDL) + helper `columns()` remplaçant 25 copies de `PRAGMA table_info` (1 helper au lieu de 3 — les 2 autres seraient mort-nés) → suite 1036/1036, version 7.6.0. · **A20 (partiel)** : CSP nonce par requête — `unsafe-inline` retiré de `script-src`, 38 scripts templates + login constant + 3 scripts Python noncés, meta `htmx-config` pour htmx, `script-src-attr` pour les 74 `onclick=`, CDN chart/leaflet débloqués (déjà cassés avant) → suite 1037/1037, version 7.7.0. · **A21 phase 1** : 352 routes `async def` sans `await` → `def` (threadpool FastAPI, SQLite hors loop, zéro changement de logique — scan corps par corps) ; reste phase 2 = 311 routes avec `await` → `anyio.to_thread.run_sync` par bloc DB → suite 1037/1037, version 7.8.0. **Phase 2c faite 2026-10-01** : **+190 routes hors loop** (283 → 93 async, **86 % des 667 routes**) en 4 passes : (A) **racine auth** — `get_current_user` (session.py) était `async def` SANS aucun await (cookie decode = synchrone) + ses clones async (`agent._current_user_id/_workspace_id/_current_admin`, `sso._require_admin`) → `def`, **47 `await` supprimés** (dont 3 via l'alias `gcu`) ; (B) re-scan → 19 routes sans await flipées ; (C/D) **155 routes** json/événements → `Body(default={})` (formes : try/except `body = {}`, try/except `raise HTTPException(400)` → `Body(...)` requis (422 FastAPI, aucun test ne couvrait le 400), forme conditionnelle content-type → défaut `{}`) + `run_event_sync`. **Reste async (93, justifié)** : `request.form`/`upload.read`/`file.read` (14+5+3, corps de requête réellement asynchrone), gitea/llm/oidc (réseau), `_json_body` 9 (wrapper de validation), 2 JSON inline en argument, 1 fallback à logique (capture_frontend_error), 1 lecture conditionnelle dans web_clipper. Échecs : 3 seeds d'aliases `gcu` attrapés par la suite → corrigés → suite 1089/1089, version 7.26.0. · **A21 phase 2a** : `body` JSON → paramètre `Body(default={})` sur les 36 routes api_v2 dont c'était le seul `await` → conversion en `def` → api_v2 à 96/115 hors loop (19 async restantes : fire_event/form/gitea) → suite 1037/1037, version 7.9.0. · **A21 phase 2b** : `run_event_sync()` (asyncio.run dans le worker, événement attendu = déterministe) + les 15 routes json/événements en `def` → api_v2 bouclé à 111/115 (4 async = vrais awaits réseau) ; repo-wide 403 sync / 260 async → suite 1037/1037, version 7.10.0. · **A32 phase 1** : les 4 routers à 0 test couverts (webhooks/notes/sidebar_config/github_routes = 10 smokes, 0 réseau réel, échappement notes vérifié) ; reste quasi nuls library/api/dashboard/api_v2 → suite 1047/1047, version 7.11.0. · **A32 phase 2a** : library 1/10 → 8 routes couvertes ; découverte = 2 routes lisant la table fantôme `local_workspace_items` (500 systématique, 0 ref front) supprimées + `_format_size` mort → suite 1053/1053, version 7.12.0. · **A32 phase 2b** : api.py 3 → 16/22 routes couvertes (board-config, col-mapping, card, collaborators stubbé, frontend-error dédup, mutations checklist vérifiées en base) ; reste 6 routes gitea + dashboard/api_v2 → suite 1059/1059, version 7.13.0. · **A32 phase 2c** : api_v2 scan strict → 5 routes à 0 ref couvertes (formula, rollup, audit-logs avec portail vérifié, webhooks/events, verify-signature signé) → suite 1064/1064, version 7.14.0. · **A32 phase 2d** : dashboard scan strict = 44 routes à 0 ref, 10 couvertes (tags CRUD, page content→rename→trash vérifiés en base, tree HTML, avatar-color restauré, members) → suite 1069/1069, version 7.15.0. · **A32 phase 2e** : +9 routes dashboard (pages comptes sans hash, profile/password A3 403, token fd_+64hex, forge, settings/account 400, select cookie, breadcrumb) → suite 1075/1075, version 7.16.0. · **A32 phase 2f** : +7 routes dashboard centrées A16 (files traversal 403, download/file-content sans fuite, avatar 302 sans réseau, table-data + création de ligne) → suite 1079/1079, version 7.17.0. · **A32 phase 2g** : +13 routes dashboard (gitea-workspace HTML, projects CRUD, cycle items 5 routes, cycle tags d'item 5 routes avec ws dédié) → suite 1083/1083, version 7.18.0. · **A32 phase 2h → dashboard bloqué** : members (cycle complet + quirk tuple), upload-folder (validations seules), convert-to-database (vérifié en base, ordre FK) ; les 44 routes à 0 ref sont toutes exercées (faux positifs f-string rapprochés) → suite 1086/1086, version 7.19.0. · **A32 TERMINÉ** : 6 routes Gitea stubbées (canevas mutable, carte board, HTML `?format=html`, 404) + **bug prod `card_detail.html`/`fd_icon` corrigé** (500 garanti avant) → suite 1089/1089, version 7.20.0. · **A27 phase 1** : 7 templates sans Jinja → 9 fichiers static/js (4 243 L, -30 % du inline, node --check vert), eslint installé globalement → `eslint static/js` 0 erreur/120 warnings ; reste = blocs interpolés Jinja (~9 661 L) → suite 1089/1089, version 7.21.0. · **A27 phase 2a** : éditeur 2 516 L extrait via `#page-data` (PD.*), dérivé `is_shared` hoisté, 8 tests adaptés (source → static js / parsing JSON) → suite 1089/1089, version 7.22.0. · **A27 phase 2b** : local_workspace 2031 + settings 1093 + realtime 531 + board 146 extraits (recette config JSON, tojson au lieu d'interpolation JS) → suite 1089/1089, version 7.23.0. · **A27 phase 2c → extraction TERMINÉE** : database_table 1314 L (config JSON null-vs-objet, 2 branches Jinja remplacées par `DB_CONFIG`), bilan **11 874 L extraites, inline -85 % (13 904 → 2 022)** → suite 1089/1089, version 7.24.0. · **A27 lint TERMINÉ** : eslint 285 warnings → **0/0** (catch vides → binding optionnel, 24 lignes mortes, 10 fonctions `/* exported */` appelées par le HTML, 3 globaux réels en config) + 2 vrais correctifs (toasts settings jamais affichés → `window.showToast`, `_wsInitData` sans effet supprimé) → suite 1089/1089, version 7.25.0. · **A21 phase 2c → A21 TERMINÉ** : auth racine sync (47 await retirés, alias gcu rattrapé) + 155 routes json/événements → Body/run_event_sync + 19 flips → **283→93 async (86 % hors loop)** → suite 1089/1089, version 7.26.0. · **A20 phase 2** : chart/leaflet vendorisés (static/js/vendor, 8 fichiers), CSP sans aucun hôte CDN, connect-src `'self' ws://{host} wss://{host}` (exfil fermé), Google Fonts morts retirés, img-src https: gardé (unfurls) → suite 1090/1090, version 7.27.0 — reste A20 = unsafe-eval (Alpine+htmx, E2E d'abord). · **A42 TERMINÉ** : `shared_client` (cache par boucle+kwargs, WeakKeyDictionary) remplace 49 créations httpx dans 14 fichiers, 2 sites laissés (transport injecté / own_client), stub webhooks étendu à la fabrique → suite 1091/1091, version 7.28.0. · **A28 lot 1** : api_v2.py (2 110 L, 115 routes) → package de 14 fichiers par concern, openapi JSON IDENTIQUE byte-à-byte (0 changement d'URL) → suite 1091/1091, version 7.29.0 · **A28 lot 2** : dashboard.py (2 735 L, 63 routes) → package de 10 fichiers (15 helpers dans _common, re-exports intacts, collision `settings`→`account_settings`), openapi IDENTIQUE byte-à-byte → suite 1091/1091, version 7.30.0 · **A28 lot 3** : collections.py (2 622 L, 53 endpoints) → package de 13 fichiers (_renderers = 15 rendus, décorateurs empilés gérés, F404 docstring corrigé), openapi IDENTIQUE byte-à-byte → suite 1091/1091, version 7.31.0. · **A28 TERMINÉ (lot 4 : board)** : 2 101 L → package de 14 fichiers (23 helpers dont 4 async dans _common, constantes ré-insérées), openapi IDENTIQUE byte-à-byte → suite 1091/1091, version 7.32.0 — les 4 god files du audit sont découpés. · **A43 TERMINÉ + A38 phase 1** : CSRF rendu côté serveur (ContextVar, plus de placeholder), palette sans re-parse par frappe, 76 lectures cookie → `getCsrf()` global head, 2 déf dupliquées supprimées → suite 1092/1092, version 7.33.0 · **A38 phase 2** : 13 noms dupliqués audités (12 scopés IIFE), seul doublon global openCardDetail dédupliqué dans app.js + test garde-fou 0-doublon → suite 1092/1092, version 7.34.0 — reste : méthodes jumelles (E2E d'abord). · **A35 TERMINÉ** : drift 3.12→3.13 aligné (Dockerfile/CI/README/ruff py313), rebuild image validé (python 3.13.16 + import app OK dans le conteneur) → suite 1093/1093, version 7.35.0. diff --git a/VERSION b/VERSION index 1680386..e319f92 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.34.0 +7.35.0 diff --git a/WORKLOAD.md b/WORKLOAD.md index db37398..b228b72 100644 --- a/WORKLOAD.md +++ b/WORKLOAD.md @@ -1,6 +1,6 @@ # WORKLOAD — FlowDeck Notion Clone -> **Début**: 2026-07-08 | **Version**: v7.34.0 (audit — A38 phase 2 : 0 doublon de fonction globale, garde-fou testé | **Statut**: EN COURS 🔄 +> **Début**: 2026-07-08 | **Version**: v7.35.0 (audit — A35 TERMINÉ : Python 3.13 aligné + rebuild image validé | **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 4645419..b5b118e 100644 --- a/app/main.py +++ b/app/main.py @@ -185,7 +185,7 @@ async def lifespan(_app: FastAPI): app = FastAPI( title="FlowDeck", - version="7.34.0", + version="7.35.0", docs_url="/docs", redoc_url="/redoc", lifespan=lifespan, diff --git a/docs/openapi-v2.json b/docs/openapi-v2.json index a154ef8..4f930a5 100644 --- a/docs/openapi-v2.json +++ b/docs/openapi-v2.json @@ -2,7 +2,7 @@ "openapi": "3.1.0", "info": { "title": "FlowDeck", - "version": "7.34.0" + "version": "7.35.0" }, "paths": { "/auth/register": { diff --git a/pyproject.toml b/pyproject.toml index 1fb0159..df279ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ pythonpath = ["."] [tool.ruff] -target-version = "py312" +target-version = "py313" line-length = 110 exclude = [".venv", ".venv311", ".pytest_cache", "static/js/vendor"]