diff --git a/CHANGELOG.md b/CHANGELOG.md index be4addc..d142f94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,44 @@ # Changelog - FlowDeck +## 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 diff --git a/ROADMAP.md b/ROADMAP.md index 4550ac3..3ba75c4 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1155,7 +1155,23 @@ 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).* — **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] **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) + +> **2026-10-01 — Fondations E2E posées** (`e2e/smoke.spec.js`, 2 gates verts +> contre l'instance de test : `cd e2e && node node_modules/@playwright/test/cli.js +> test smoke.spec.js`) — **2 bugs réels trouvés et corrigés au passage** : +> (1) les onglets de vues standalone émettaient `?view=` alors que la route +> lit `view_type` → **la bascule de vues ne marchait jamais** (l'onglet +> restait sur Table) → `?view_type=` + assertion comportementale dans +> `test_all_view_tabs_present` ; (2) `app.css` importait encore Google +> Fonts (`@import` Inter) **bloqué par la CSP** depuis v7.27 → Inter +> **auto-hébergé** (`static/fonts/`, 2 faces variables 100-900, +> latin+latin-ext). Le SW est bloqué dans le smoke (sa page « hors ligne » +> sert les navigations redirigées — `pwa_offline.spec.js` couvre le SW). +> **Portes** : A20 (unsafe-eval) attaquable avec ce filet ; A39 couvre la +> bascule collection standalone (la bascule htmx des vues board reste non +> couverte → décision « rien » maintenue) ; A38 twins reste conditionné à +> une couverture élargie.` 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**.* diff --git a/VERSION b/VERSION index e319f92..0391f8e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.35.0 +7.36.0 diff --git a/WORKLOAD.md b/WORKLOAD.md index b228b72..8a41d8c 100644 --- a/WORKLOAD.md +++ b/WORKLOAD.md @@ -1,6 +1,6 @@ # WORKLOAD — FlowDeck Notion Clone -> **Début**: 2026-07-08 | **Version**: v7.35.0 (audit — A35 TERMINÉ : Python 3.13 aligné + rebuild image validé | **Statut**: EN COURS 🔄 +> **Début**: 2026-07-08 | **Version**: v7.36.0 (E2E fondations : smoke A39/A20 vert + 2 bugs trouvés (onglets ?view=, Inter CSP) | **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 b5b118e..f6fdb1f 100644 --- a/app/main.py +++ b/app/main.py @@ -185,7 +185,7 @@ async def lifespan(_app: FastAPI): app = FastAPI( title="FlowDeck", - version="7.35.0", + version="7.36.0", docs_url="/docs", redoc_url="/redoc", lifespan=lifespan, diff --git a/app/routers/collections/_renderers.py b/app/routers/collections/_renderers.py index b38d1c5..18b4cc9 100644 --- a/app/routers/collections/_renderers.py +++ b/app/routers/collections/_renderers.py @@ -55,17 +55,17 @@ h1{{font-size:24px;margin:0 0 8px}} p.desc{{color:#A0A0A0;margin:0 0 20px}}