Commit Graph
32 Commits
Author SHA1 Message Date
bruno 2339fa2586 test: A32 phase 2f — dashboard +7 routes, garde-fous A16 (v7.17.0)
FlowDeck CI / lint (push) Successful in 1m52s
FlowDeck CI / docker (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 5m54s
Cumul dashboard : 36 → 43 des 63 routes. 4 nouveaux tests (fichier à 42),
centrés sur les garde-fous A16 :

- GET /api/files/{ws}/{path} : traversal encodé %2e%2e%2f → 403
  « Path traversal denied » (décodé par Starlette puis bloqué par resolve) ;
  inexistant → 404 ; vrai fichier écrit dans le data_dir de test →
  200 + octets exacts, nettoyé en finally
- GET /api/pages/{id}/download : page markdown → 404 « No downloadable file »
  (pas de 500) ; page « file » avec chemin ../ sortant de la racine →
  jamais 200 ; file-content → 404/415 sans fuite
- GET /api/local-workspace/page-content/{id} : contenu + format relus,
  404 sur id inconnu
- GET /api/avatar/{id} : 302 + Location avec follow_redirects=False
  (AUCUNE requête réelle vers l'URL externe), 404 sans avatar
- GET/POST /api/collections/{id}/table-data|pages : 404 inconnu, shape,
  ligne créée retrouvée dans table-data, nettoyage finally

Reste A32 : dashboard 20 routes (upload/local-workspace items/members/
projects/HTML gitea) + 6 routes Gitea d'api.py (stub transport httpx).

suite **1079/1079** · `ruff check app tests` OK · docs à jour
2026-10-01 14:40:06 -04:00
bruno 8b48dbdd4b test: A32 phase 2e — dashboard +9 routes, comptes A2/A3 (v7.16.0)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
Cumul dashboard : 27 → 36 des 63 routes. 6 nouveaux tests (fichier à 38) :

- /accounts + /accounts/settings : 200 HTML et « password_hash » ABSENT du
  rendu (whitelist A2 vérifiée côté page)
- PUT /api/user/profile : persistance relue en base, restauration finally
- PUT /api/user/password : 403 « current password is incorrect » (A3 — la
  session seule ne change pas le mdp) + quirk assumé documenté : la longueur
  est validée AVANT l'auth et répond 200 + message
- POST /api/user/token : format fd_ + 64 hex ; ligne user_tokens nettoyée
- DELETE /api/user/forge/{provider} : {"status": "ok"}
- PUT /api/settings/account : full_name/email persistés + 400 sur mdp court,
  restauration finally
- POST /api/workspaces/1/select : Set-Cookie flowdeck_workspace vérifié ;
  GET /api/local-workspace/breadcrumb : shape liste

Reste A32 : dashboard 27 routes (fichiers/avatars/local-workspace/collections)
+ 6 routes Gitea d'api.py (stub transport httpx).

suite **1075/1075** · `ruff check app tests` OK · docs à jour
2026-10-01 13:59:00 -04:00
bruno 360c705fd4 test: A32 phase 2d — dashboard +10 routes couvertes (v7.15.0)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
Scan strict dashboard.py : 44 routes à 0 référence stricte sur 63. 10
couvertes cette passe (test_smoke_uncovered.py → 32 tests) :

- Tags CRUD complet : POST (nom lowercasé `SmokeTag` → `smoketag`), GET all
  (présent), PUT couleur (relue), DELETE (absente de la liste ensuite)
- Vie d'une page : GET /api/pages/{id}/content (contenu seedé relu) →
  PUT rename (ok + **400 titre vide** + titre relu en base) →
  POST trash (parent_section='Trash' + deleted_at RELUS en base) ;
  nettoyage en finally
- GET /api/sidebar/workspace-tree : 200 HTML, fragment « No pages yet »
  (pas de cookie workspace)
- POST /api/settings/avatar-color : couleur relue SUR L'UTILISATEUR DE LA
  SESSION (pas LIMIT 1), avatar_color/avatar_url d'origine restaurés
- GET /api/workspace/1/members : shape {"members": [...]}

Helper _seed_page : les colonnes par défaut sont surchargeables (content=)
pour les seeds à contenu.

Reste A32 : dashboard 34 routes à 0 ref (fichiers/avatars/imports…) +
6 routes Gitea d'api.py (stub transport httpx).

suite **1069/1069** · `ruff check app tests` OK · docs à jour
2026-10-01 13:36:56 -04:00
bruno 0698645dbd test: A32 phase 2c — api_v2 : les 5 routes à 0 ref couvertes (v7.14.0)
FlowDeck CI / lint (push) Successful in 1m52s
FlowDeck CI / docker (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 34m59s
Scan strict des 115 routes api_v2.py contre tous les tests (chaîne de chemin
littérale) → 5 routes sans AUCUNE référence, toutes couvertes maintenant :

- POST /properties/evaluate-formula : 200 + shape, 400 sans expression.
  Le moteur renvoie « 1 + 2 » tel quel aujourd'hui → le smoke valide le câble
  (bearer, Body param, parse), pas le moteur (réalm de ses propres tests).
- POST /properties/compute-rollup : 400 « collection_id required »,
  401 sans bearer.
- GET /admin/audit-logs : portail admin VÉRIFIÉ — l'attendu est calculé
  depuis /users/me (le tout premier utilisateur d'un worker est admin :
  état non contrôlable depuis un test isolé), + token scope admin → 200 + logs.
- GET /webhooks/events : catalogue non vide + wildcards * / page.*.
- POST /webhooks/verify-signature : valid=True avec sign_payload() (le même
  helper que le serveur), False avec signature bidon.

test_smoke_uncovered.py : 27 tests. Reste A32 : dashboard 17/63 + 6 routes
gitea d'api.py (stub transport).

suite **1064/1064** · `ruff check app tests` OK · docs à jour
2026-10-01 13:09:32 -04:00
bruno b2e38aece7 test: A32 phase 2b — api.py 3 → 16/22 routes couvertes (v7.13.0)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
+6 smokes dans test_smoke_uncovered.py (22 tests au total dans le fichier) :
- board-config GET/POST : défauts 5 colonnes sans board, création puis
  relecture du roundtrip (seed via l'endpoint lui-même, pas de SQL brut)
- col-mapping POST/DELETE : 404 sans board, upsert label vérifié, suppression
- card POST : 404 sans board, ok avec
- collaborators GET : gitea.get_collaborators STUBBÉ (zéro accès réseau réel)
- frontend-error(s) : capture, JSON invalide → ignored, DÉDUP d'une erreur
  répétée (count=2), lecture qui purge (cleared=true puis 0)
- checklist mutations : PATCH item (checked/content relus EN BASE), DELETE
  item, DELETE checklist (COUNT=0) — seed + cleanup en finally

Reste api.py : 6 routes Gitea (issues ×4 + créations checklists owner/repo) →
stub de transport httpx (phase suivante). Reste global : dashboard 17/63,
api_v2 50/115.

suite **1059/1059** (236 s) · `ruff check app tests` OK · docs à jour
2026-10-01 12:35:22 -04:00
bruno df9a269d76 test: A32 phase 2a — library 10/10 + 2 routes fantômes supprimées (v7.12.0)
FlowDeck CI / lint (push) Successful in 1m53s
FlowDeck CI / docker (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 8m41s
- tests/test_smoke_uncovered.py : +6 tests pour library.py (1/10 → 8 routes
  couvertes) : les 5 listes en boucle (recents/favorites/published/private/
  workspace → 200 + items), /private avec page seedée retrouvée, /children/{id}
  avec parent/enfant seedés, /repository vide ET clé (string, aucun réseau),
  non-régression 404 sur les routes supprimées.
- DÉCOUVERTE (les smokes l'ont prouvé) : `/api/library/local-workspace-children`
  renvoyait un 500 systématique (test vert → ASGI double response.start) et
  `/api/library/local-workspace` 500 dès qu'un workspace existe — les deux
  lisaient `local_workspace_items`, table AUCUNEMENT créée dans le codebase
  (0 CREATE TABLE) et sans 1 seule référence front. Supprimés avec
  `library._format_size` devenu mort (la copie de dashboard.py est inchangée).
  `local_workspace_items` : 0 occurrence restante dans app/.
- helper `_seed_page` minimal (workspace NOT NULL inclus) + nettoyage en
  finally (pas de pollution des autres tests).

suite **1053/1053** (229 s) · `ruff check app tests` OK · docs à jour
restent phase 2b : api.py 3/23, dashboard.py 17/63, api_v2.py 50/115
2026-10-01 12:11:12 -04:00
bruno da7326ffde test: A32 phase 1 — 4 routers à 0 test couverts (10 smokes) (v7.11.0)
FlowDeck CI / lint (push) Successful in 1m50s
FlowDeck CI / test (push) Successful in 13m55s
FlowDeck CI / docker (push) Canceled after 0s
tests/test_smoke_uncovered.py — un smoke par route des 4 routers qui n'avaient
AUCUN test :
- webhooks.py 3/3 : réception sans secret → {"status":"ok"} ; HMAC faux → 401
  (secret piloté par monkeypatch, déterministe quel que soit le .env) ;
  register sans secret → 400 AVANT tout appel réseau ; status avec
  gitea.list_webhooks stubbé → {"registered": False} (zéro réseau réel)
- notes.py 2/2 : GET HTML + roundtrip POST→GET (upsert persisté en base,
  échappement HTML vérifié : &lt;b&gt; et non <b>)
- sidebar_config.py 2/2 : GET défauts ; PUT persisté puis RELU depuis
  users.sidebar_config ; 400 sans config ; remise en état en fin de test
- github_routes.py 2/2 : status {"linked": False} ; disconnect {"status": "ok"}

Reste (A32 phase 2) : quasi nuls — library 1/10, api 3/23, dashboard 17/63,
api_v2 50/115 → même recette, fixture client existante.

suite **1047/1047** · `ruff check app tests` OK · docs à jour
2026-10-01 11:54:43 -04:00
bruno 3a1276596c fix: A21 phase 2b — run_event_sync + 15 routes api_v2 en def (v7.10.0)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
- `run_event_sync(coro)` (app/services/automations.py) : exécute une coroutine
  d'événement depuis un handler synchrone — `asyncio.run` sur une boucle
  dédiée dans le worker threadpool : le worker est bloqué, JAMAIS la boucle
  d'event, et la réponse n'est produite qu'une fois l'événement terminé
  (déterministe, équivalent sémantique de l'await). ponytail: les clients
  httpx sont créés à chaque appel partout → aucun lien de boucle ; sinon
  run_coroutine_threadsafe + boucle du lifespan.
- 15 routes api_v2 dont les SEULS awaits étaient `request.json`,
  `_fire_event`, `fire_published`, `fire_unpublished` →
  `Body(default={})` + `run_event_sync(...)` + conversion en `def` (script
  : wrapping par appariement de parenthèses chaîne-aware, assert de flip
  « plus aucun await »).
- api_v2 : **111/115 routes hors loop**. Les 4 restantes ont de vrais awaits
  réseau et restent async volontairement : import_csv_v2 (multipart),
  project_tree_v2 (gitea), test_webhook_v2 (delivery), retry_webhook_deliveries.
- Repo-wide : 403 routes sync (hors loop) / 260 async (phase 2c).

tests : ciblé public_api_v2 + v65 + webhooks_v2 + audit = 90/90 (les webhooks
prouvent la détermination de run_event_sync) ; suite complète **1037/1037**
(228 s) · `ruff check app tests` OK · docs à jour
2026-10-01 11:35:43 -04:00
bruno 07904f05e5 fix: A21 phase 2a — api_v2 : body JSON en paramètre, 36 routes hors loop (v7.9.0)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
- Les 36 routes d'api_v2 dont le SEUL `await` était
  `body = await request.json()` (bloc try/except uniforme) → paramètre FastAPI
  `body: dict = Body(default={})` + conversion en `def` → threadpool :
  toute leur séquence SQLite quitte l'event loop.
- Équivalences vérifiées avant engament (probe FastAPI) :
  · corps absent → `{}` (identique à l'ancien try/except)
  · JSON invalide → 422 (avant : avalé comme `{}` — 422 est plus juste)
  · zéro `body[...]=` / setdefault / update dans api_v2 → défaut partagé
    jamais muté
- verify_webhook_signature (signature multi-ligne) traitée à la main.
- Piège courant évité : première version du script supprimait 5 lignes au
  lieu de 4 (slice m-1:m+4) → fichier restauré depuis git, slice corrigée,
  0 ligne perdue (diff logique +39/-183).

api_v2 : 96/115 routes hors loop (60 phase 1 + 36 ici) ; 19 async restantes
(fire_event, request.form, gitea/webhooks) = phase 2b.

suite **1037/1037** (242 s) · `ruff check app tests` OK · docs à jour
2026-10-01 11:19:58 -04:00
bruno 224bda74d5 fix: A21 phase 1 — 352 routes async sans await → threadpool (v7.8.0)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
- Conversion `async def` → `def` de TOUTES les routes dont le corps ne contient
  ni `await`, ni `async with`, ni `async for`, ni `asyncio` (scan automatique
  corps par corps sur app/ : 352 converties, 0 dangereuses, vérifié
  `asyncio`/`run_coroutine`/`.result()` absents). FastAPI exécute ces handlers
  dans son threadpool → tout leur SQLite (`get_conn()` + `conn.execute`) quitte
  l'event loop, sans changer une ligne de logique.
- Répartition : api_v2 60, dashboard 40, collections 25, board 23,
  workspace 19, wiki 17, permissions 14, api 14, main.py 6, + 35 fichiers.
- Les 4 routers prioritaires de l'audit sont couverts par ce lot :
  api_v2 60 + dashboard 40 + collections 25 + board 23 = 148 conversions
  (le reste de leurs routes attend la phase 2 : elles ont de vrais `await`).
- Reste (phase 2) : les 311 routes avec de vrais `await` → enrouler les blocs
  DB dans `await anyio.to_thread.run_sync(...)` ; pas de wrapper partagé livré
  (rien ne l'appellerait — YAGNI jusqu'au premier usage).

suite **1037/1037** (233 s) · `ruff check app tests` OK · docs à jour
2026-10-01 10:53:26 -04:00
bruno c718fe06de fix: A20 (partiel) — CSP nonce par requête, unsafe-inline sort de script-src (v7.7.0)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
- ContentSecurityPolicyMiddleware : nonce aléatoire par requête dans la
  ContextVar `CSP_NONCE` (posée avant `call_next` → visible des templates),
  `script-src 'self' 'unsafe-eval' 'nonce-…'` — plus aucun script inline
  sans nonce ne tourne (fin des XSS injectés en JS)
- 38 tags `<script>` des templates : `nonce="{{ csp_nonce() }}"` (passage
  scripté, vérifié : 0 restant) ; `LOCAL_LOGIN_HTML` (constante de module) :
  helper `_with_nonce()` au rendu ; collections.py : 3 scripts Python
  (chart/form/map) noncés
- `<meta name="htmx-config" content='{"inlineScriptNonce": …}'>` dans base.html
  : htmx ré-injecte les <script> des réponses boostées avec le bon nonce
- `script-src-attr 'unsafe-inline'` : les 74 handlers `onclick=` inline
  restent couverts (le nonce les aurait désactivés aussi)
- chart.js (cdn.jsdelivr.net) et leaflet (unpkg) ajoutés à script-src/style-src
  : vues chart/map déjà BLOQUÉES par la CSP depuis toujours
  (commentaire ponytail: upgrade = vendoriser puis retirer les hôtes)
- reste d'A20 : unsafe-eval (Alpine x-data → @alpinejs/csp), externalisation
  JS (A27), resserrer img-src/connect-src

test : test_csp_nonce_per_request (page base.html + page hors template,
nonce unique par requête)

suite **1037/1037** · `ruff check app tests` OK · docs à jour
2026-10-01 10:41:39 -04:00
bruno f706424f90 fix: A31 — transaction par migration + helper columns() (v7.6.0)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
- `_apply_one()` : BEGIN explicite → `fn(conn)` → marque `schema_version` →
  commit ; rollback complet à l'échec. Avant le DDL sortait en autocommit
  (isolation_level legacy) : un échec au milieu laissait un schéma partiel
  commité SANS ligne de version, et la reprise rejouait un DDL déjà appliqué.
  Si une transaction englobante subsiste (init_db commit juste avant), on la
  vide d'abord plutôt que de l'englober.
- Helper unique `columns(conn, table)` (valide l'identifiant, ValueError sinon)
  : 25 copies de `{r[1] for r in conn.execute("PRAGMA table_info(...)")}`
  éliminées dans migrations.py (21 littéraux + 3 f-string + 1 variante row).
  `table_exists`/`column_exists` préconisés par l'audit NON livrés : aucune
  migration n'interroge sqlite_master, un contrôle unitaire se lit dans le set.
- Smoke : DB fraîche → 28 migrations → version 29, ré-apply idempotent.

tests : test_migration_transaction_rolls_back (DDL partiel annulé + zéro marque
de version), test_columns_helper_validates_table_name

suite **1036/1036** · `ruff check app tests` OK · OpenAPI 511 chemins / 7.6.0
docs (ROADMAP/CHANGELOG/WORKLOAD/VERSION) à jour
2026-10-01 10:13:23 -04:00
bruno 7be96f0618 fix: A29 + A42(partiel) — publish partagé, fuite password_hash, data_dir (v7.5.0)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
- A29 — `app/services/publish.py` : slugify titré unique (fallback aléatoire),
  404 si la page n'existe pas, événements centralisés. Les 3 paires
  publish/unpublish déléguent (sharing = front, board, v2) :
  · board : mise à jour aveugle → 404 + contrôle de session ajouté
  · board : perd `share_mode='anyone'` en bonus, v2 : perd `is_shared=1` —
    le share dialog reste l'unique propriétaire de ces drapeaux
  · v2 : slug fourni conservé, slug vidé aussi à la dépublication (avant : laissé)
  · `/users/me` ×2 et listings collections ×3 = contrats versionnés distincts,
    décision documentée (on garde)
- Byproduct sécurité — `GET /api/users/me` (v1) et le contexte de `/accounts`
  faisaient `SELECT *` sur users → password_hash / login_attempts / locked_until
  exposés → colonnes whitelistées (liste v2)
- A42 (partiel) — 9 copies de `Path(os.environ.get("FLOWDECK_DATA_DIR", "/data"))`
  → `settings.data_dir` (property : lecture à chaque accès, les tests
  monkeypatchent l'env) ; cache Gitea : évacuation des entrées expirées à chaque
  écriture. Reste : client httpx partagé (52 créations, cache par event loop)

tests : test_publish_service_shared_and_safe, test_users_me_no_secret_columns,
test_gitea_cache_evicts_expired

suite **1034/1034** · `ruff check app tests` OK · OpenAPI 511 chemins / 7.5.0
docs (ROADMAP/CHANGELOG/WORKLOAD/VERSION) à jour
2026-10-01 10:01:39 -04:00
bruno 937ecfc2e0 fix: A30 + A37 + A39 + A40 + A41 — fin du P2/P3 XS/S (v7.4.0)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
- A30 — `require_scope()` câblé : 69 sites stricts de api_v2.py passent par la
  factory (Bearer + scope en 1 appel, contrôle manuel supprimé) ; sémantique
  alignée sur celle des handlers (pas de default "read" → 0 changement de
  comportement) ; 12 top-level morts supprimés (0 ref app ET tests) :
  unsync_block, find_referring, _b64url, strip_markdown, format_number,
  get_auto_property_value, get_next_unique_id, local_date_in_tz,
  verify_device_token, _get_dynamic_groups, _require_user_gitea,
  validate_upload_request
- A37 — CORS sans `*` : origines = app_base_url + allow_origin_regex
  (localhost/dev, origines d'extension pour le Web Clipper), méthodes et
  entêtes minutées, allow_credentials explicite + test test_cors_no_star
- A39 — htmx : décision « rien » documentée (32 attributs hx-* réels sur 6
  templates, conversion = refonte du view-switching sans test E2E)
- A40 — version d'assets à source unique : ENV.globals["asset_version"] lu au
  boot depuis le fichier VERSION ; littéraux `?v=` de base.html éliminés ;
  test test_asset_version_single_source
- A41 — app.css : 91 règles mortes purgées (-10 274 octets, 121 618 → 111 344),
  scan templates/JS/CSS/Python à 0 référence

suite **1031/1031** · `ruff check app tests` OK · OpenAPI 511 chemins / 7.4.0
docs (ROADMAP/CHANGELOG/WORKLOAD/VERSION) à jour
2026-10-01 09:30:37 -04:00
bruno cb47f5c7f4 fix: A26 + A33 + A34 + A35 + A36 + A43 — bande S/XS du P2 (v7.3.9)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
- A26 — champ `standalone` mort supprimé (0 lecteur) ; `sqlite:////data/…` ne
  produit plus de chemin UNC sous Windows ; `.env.example` ne promet plus
  PostgreSQL ; raise au boot si `APP_SECRET_KEY` vaut la valeur par défaut
- A33 — rate limit : préfixes manquants (`/scim/v2/`, `/workspace/`, `/db/` +
  non-GET sur `/s/` et `/f/`), limite lue dans `settings.rate_limit_requests`
  (60 annoncés / 100 codés en dur), clé `X-Forwarded-For` seulement derrière un
  proxy local (anti-spoof), `_store` épuré (mémoire bornée) + test dédié
- A34 — `_spawn()` : les 10 schedulers loggent leur exception et redémarrent
  après 10 s au lieu de mourir en silence ; 2 `logger.debug` de scheduler → warning
- A35 — OpenAPI régénéré 439 → 511 chemins (`info.version 7.3.9`), README
  (était v6.7.0), compteur API_GUIDE, titre dupliqué retiré du ROADMAP ;
  le drift Python 3.12/3.13 reste noté (rebuild d'image à valider)
- A36 — 4 dépendances mortes purgées de requirements.txt (aiosqlite, slowapi,
  loguru, packaging = 0 import) ; pyproject reste sans [project] : Docker et la
  CI installent requirements.txt, dupliquer les 22 deps créerait 2 sources
- A43 — 15 `datetime.utcnow()` → `now(UTC).replace(tzinfo=None)` (format ISO
  naïf identique, warnings de dépréciation divisés : 2374 → 926)

suite **1029/1029** · `ruff check app tests` OK · docs/ROADMAP/CHANGELOG/WORKLOAD à jour
2026-10-01 08:50:39 -04:00
bruno ffa1fa89ab fix: A25 + A21 (partiel) — plus d'exception muque, transaction protégée (v7.3.8)
FlowDeck CI / lint (push) Successful in 1m51s
FlowDeck CI / docker (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 10m12s
- A25 — 84 `except Exception: pass/…` → `logger.exception("<fonction>")`
  (19 fichiers : api_v2 30, dashboard 10, board 7, sites 5, workspace 5,
  api_v2_helpers 5, …) ; `logger` ajouté là où il manquait (api_v2_helpers,
  sites + `import logging`)
- A25 critique — les `try` autour de `materialize_properties` supprimés dans
  `create_collection_v2` ET `apply_db_template_v2` : un échec interrompt la
  transaction au lieu de commiter une collection sans schéma
- test `test_collection_rollback_when_materialize_fails` (Bearer v2, monkeypatch
  qui lève, assertions : RuntimeError + 0 ligne commitée)
- A21 partiel — `PRAGMA busy_timeout=5000` dans `get_conn()` (point d'entrée
  unique) ; commentaire `ponytail:` : le wrapper async + les 510 call sites
  restent à migrer module par module
- suite **1028/1028** · `ruff check app tests` OK
2026-10-01 08:16:42 -04:00
bruno 3ad2605c9e fix: A14 — fin du fallback « row admin » sur l'agent (v7.3.7)
FlowDeck CI / lint (push) Successful in 1m55s
FlowDeck CI / test (push) Successful in 20m33s
FlowDeck CI / docker (push) Canceled after 0s
- `_current_user_id` : 401 sans session (24 sites) au lieu de retomber sur
  `SELECT id FROM users WHERE login='admin'`
- `_current_admin` : suppression du même fallback — `PATCH /api/agent/providers`
  et `POST /api/agent/providers/test` (donc `LLMClient.ping(api_base=…)`)
  exigent une session admin : 401 sans session, 403 non-admin
- `_check_api_base()` sur les 2 routes : scheme http(s), pas d'identifiants
  dans l'URL (400) ; hôtes privés maintenus — Ollama `localhost:11434` est le
  provider par défaut du produit (commentaire `ponytail:` pour la fermeture)
- +1 test de non-régression → suite **1027/1027**, `ruff check app tests` OK
2026-10-01 07:53:06 -04:00
bruno 1f705ce512 fix: A19 terminé — plus aucun préfixe cookie-auth exempt du CSRF (v7.3.6)
FlowDeck CI / test (push) Successful in 20m10s
FlowDeck CI / lint (push) Successful in 1m51s
FlowDeck CI / docker (push) Canceled after 0s
- 46 appels non-GET équipés de `X-CSRF-Token` (expression cookie en ligne,
  portée indifférente) : agent_panel (9), settings (12), local_workspace (15),
  gitea_workspace (4), workspace (2), workspaces (5), library (2), welcome (2 en v7.3.5)
- 5 derniers préfixes sortis d'`EXCLUDED_PATHS` : /api/workspace (+/api/workspaces),
  /api/local-workspace, /api/settings, /api/gitea, /api/agent
- il ne reste exempté que : Bearer (/api/webhook, /api/v1, /api/v2, /scim/v2),
  callbacks /auth/*, pages publiques (/s/, /f/), /api/csrf-token et /api/frontend-error
- vérif : `node --check` des <script> des 39 templates → 0 échec (avant et après)
- tests : `anon_csrf()` sur les 5 tests d'anonymat devenus CSRF-first
- suite **1026/1026** · `ruff check app tests` OK
2026-10-01 07:41:23 -04:00
bruno 0861f1fdbf fix: A19 (partiel) — CSRF réservé aux vrais cas d'exemption (v7.3.5)
- Scan de tous les appels `fetch` non-GET du front par préfixe : 12 préfixes
  n'ont AUCUN call site sans `X-CSRF-Token` → sortis de `EXCLUDED_PATHS`
  (`/db/`, `/workspace`, `/workspace/automations`, `/board/api/favorites`,
  `/api/pages`, `/api/recents`, `/api/notifications`, `/api/comments`,
  `/api/automations`, `/api/github`, `/api/admin`, `/api/onboarding`)
- `welcome.html` : les 2 POST `/api/onboarding/*` reçoivent le header
  (`adminFetch` prouve que `/api/admin` était déjà couvert)
- reste A19 (front à équiper) : `/api/workspace` (+`/api/workspaces`),
  `/api/local-workspace`, `/api/settings`, `/api/gitea`, `/api/agent` — 49 fetch
- tests : helper `anon_csrf()` (anonyme + CSRF valide) pour isoler les 401 de
  route du 403 middleware — 4 tests d'anonymat ajustés
- suite **1026/1026** · `ruff check app tests` OK
2026-09-30 23:38:03 -04:00
bruno 72fcef2ba9 fix: A16 — ACL sur l'export et les pièces jointes (v7.3.4)
FlowDeck CI / docker (push) Successful in 1m44s
FlowDeck CI / lint (push) Successful in 1m49s
FlowDeck CI / test (push) Successful in 21m5s
- `export.py` : `_load_page_or_404(request, page_id)` — 401 sans session,
  404 si `PermissionManager.can_view_page` refuse (les 4 formats d'export)
- `dashboard.py` : helper `_require_page_view` sur `GET /api/pages/{id}/download`
  et `GET /api/pages/{id}/file-content`
- +1 test de non-régression → suite **1026/1026**, `ruff check app tests` OK
2026-09-30 23:20:26 -04:00
bruno 5a537f5dc3 fix: A12–A24 — SSRF, auth routes legacy, uploads, N+1 et routes doublonnes (v7.3.3)
FlowDeck CI / docker (push) Successful in 1m44s
FlowDeck CI / lint (push) Successful in 1m53s
FlowDeck CI / test (push) Successful in 20m45s
- A12 — `og_fetcher` : GET sans `follow_redirects`, `_is_public_host` revérifié à
  chaque saut (max 5) ; `POST /board/api/og/metadata` → 400 sur hôte privé/loopback
- A13 — router automations sous `Depends(_require_session)` (CRUD, run,
  press-button) + `created_by` sans fallback ; action `webhook` validée par
  `_is_public_host` avant POST (SSRF)
- A15 — webhooks sortants : `_require_admin` sur GET/POST/DELETE + `_is_public_host`
  sur l'URL en création
- A17 — router legacy `/api` sous `Depends(_require_session_or_bearer)` (session ou
  Bearer `/api/v1`), allowlist explicite `/api/health` + `/api/frontend-error`
- A22 — les 2 uploads locales : session exigée (`_require_user_id`) + `validate_upload`
  branché (taille + extension) + `FLOWDECK_DATA_DIR` au lieu de `/data` codé en dur
- A23 — N+1 : COUNT→`GROUP BY` (dashboard), cards→`executemany` (board sync),
  duplicata de propriétés→`executemany` + remap des ids par SELECT (collections)
- A24 — 2 routes écrasées supprimées : `GET /api/projects` (api.py) et
  `GET /workspace` (workspace.py) + test « aucun doublon méthode+chemin »
- Tests : +9 dans `tests/test_audit_p0_fixes.py` (SSRF, 401s, validate_upload,
  doublons de routes) ; tests OG sur hôtes résolubles (la garde fait du DNS)
- suite **1025/1025** · `ruff check app tests` OK
2026-09-30 23:12:20 -04:00
bruno 8ab6569974 fix: A11 + A18 — path traversal avatar et XSS/flags sur la vue publique (v7.3.2)
FlowDeck CI / docker (push) Successful in 1m44s
FlowDeck CI / lint (push) Successful in 1m49s
FlowDeck CI / test (push) Successful in 22m0s
- A11 : `GET /api/settings/avatar/{filename:path}` → `resolve()` + `relative_to()` (motif de `serve_uploaded_file`), 403 hors de `/data/avatars`
- A18 : `GET /workspace/public/{id}` → 404 HTML explicite pour `permission_type` restricted/private, `html.escape` sur le nom, l'icône et les titres de lignes (le f-string HTML ne passe pas par Jinja2)
- `tests/test_audit_p0_fixes.py` : 3 tests de non-régression (traversal, échappement, hidden restricted)
- ROADMAP A11/A18 cochés · CHANGELOG/WORKLOAD/VERSION → 7.3.2 · suite **1019/1019** · `ruff check app tests` OK
2026-09-30 22:40:34 -04:00
bruno 69a0aceba6 fix: A10 — autoescape Jinja2 activé partout via un Environment partagé
FlowDeck CI / docker (push) Successful in 1m44s
FlowDeck CI / lint (push) Successful in 1m49s
FlowDeck CI / test (push) Successful in 21m54s
- `app/templating.py` : `ENV = Environment(loader=FileSystemLoader("app/templates"), autoescape=select_autoescape(["html"]))`
- les 29 instantiations `Environment(loader=FileSystemLoader("app/templates"))` (9 routers) remplacées par `env = ENV` — plus aucune interpolation `{{ … }}` servie crue, les `|safe` redeviennent efficaces
- re-tri des `|safe` : `card_detail.html` corps d'issue et commentaires échappés (XSS stocké), placeholder de description sorti du ternaire, `sidebar_config` passé en dict + `|tojson` (échappement `</script>` en contexte script)
- `|safe` conservé sur `right_actions` (HTML fabriqué dans les templates, fiable)
- `ruff check app tests` OK · suite **1016/1016 verts**
2026-09-30 22:28:48 -04:00
bruno 1706ad1ee9 feat: v7.3.0 — cycle v6.8.0→v7.3.0 (Sites, Search, Automations, Calendar, SCIM, Wiki) + audit A9
FlowDeck CI / lint (push) Successful in 1m48s
FlowDeck CI / test (push) Failing after 21m19s
FlowDeck CI / docker (push) Skipped
- v6.8.0 Sites & Forms publics (migrations 24)
- v6.9.0 Recherche sémantique hybride + Ask AI (migration 25)
- v7.0.0 Automations v2 multi-étapes + Workers sandboxés (migration 26)
- v7.1.0 Calendar sync Google/CalDAV + Meeting Notes (migration 27)
- v7.2.0 Enterprise : SCIM 2.0, 2FA TOTP/passkeys, audit UI, agent approvals (migration 28)
- v7.3.0 Wiki/Teamspaces, verified pages, collab polish, charts, unfurl (migration 29)
- docs V68→V73, ROADMAP/CHANGELOG/WORKLOAD à jour, VERSION 7.3.0
- A9 : flowdeck.db, flowdeck_dev.db, test-commit.md, upload_test.txt et e2e/{node_modules,shots,test-results} désindexés + ignorés (.gitignore/.dockerignore)
2026-09-30 20:02:57 -04:00
bruno d074689b18 feat: v6.7.0 — SSO/SAML + OIDC entreprise (SP SAML signé + OIDC PKCE, auto-provisioning, groupes IdP → rôles workspace, SSO only, onglet Settings « SSO / Enterprise », API /api/v2/sso/*, help, migration 23, docs + OpenAPI 439 chemins) · 802 tests verts
FlowDeck CI / lint (push) Successful in 1m28s
FlowDeck CI / test (push) Successful in 11m52s
FlowDeck CI / docker (push) Successful in 1m51s
2026-09-24 13:32:17 -04:00
bruno 9562f30366 feat: v6.6.0 — Agent phase 5 : API publique agent (/api/v2/agents, run synchrone JSON) + marketplace skills (export/import portable + galerie de 6 presets, palette / du panneau) + webhooks agent.run.started/failed · 764 tests verts
FlowDeck CI / docker (push) Successful in 1m21s
FlowDeck CI / lint (push) Successful in 1m27s
FlowDeck CI / test (push) Successful in 11m5s
2026-09-24 10:16:24 -04:00
bruno f2f2f3209e feat: v6.4.0 Realtime production — merge 3-voix (au-delà du LWW) + broadcast non bloquant
FlowDeck CI / lint (push) Successful in 1m25s
FlowDeck CI / test (push) Successful in 10m20s
FlowDeck CI / docker (push) Successful in 1m21s
- app/services/realtime_merge.py : merge à 3 voix diff3-lite, regions disjointes conservees, conflit par champ + drapeau
- protocole base (client embarque la base de sa saisie) ; sans base -> LWW historique (retro-compat)
- ack renvoie le bloc fusionne + conflict ; adoption cote client + toast ; broadcast du resultat fusionne
- broadcast non bloquant : file sortante + tache writer par connexion, coalescence des curseurs
- clients trop lents deconnectes (4413), budget ops anti-flood (400/10s)
- fix fuite room 4404 + room_state() sur page inexistante
- GET /api/realtime/stats (observabilite)
- 26 tests test_realtime_v64.py ; suite 725 verte ; ruff + eslint OK ; version 6.4.0
2026-09-23 23:55:39 -04:00
bruno 246aef65ac docs: v2.2 + ROADMAP v3.0 — multi-user, multi-forge, standalone
FlowDeck CI / test (push) Failing after 4s
FlowDeck CI / docker (push) Has been skipped
ROADMAP.md: 7 phases détaillant la migration vers une plateforme:
- Phase 1: Auth locale (password + OAuth Gitea/GitHub)
- Phase 2: Abstraction multi-forge (adapter pattern)
- Phase 3: Workspace page + navigation projet
- Phase 4: Mode standalone (sans forge)
- Phase 5: Permissions collaboratives
- Phase 6: UI/UX polishing
- Phase 7: Infrastructure (DB, tests, CI/CD)

WORKLOAD.md: v2.1 → v2.2 (Share/Publish/Favorites/Library),
cible v3.0 ajoutée
2026-07-10 15:26:57 -04:00
bruno 7c8f8d719e docs: CHANGELOG, README, WORKLOAD updated for v2.1.0 — complete
FlowDeck CI / test (push) Failing after 4s
FlowDeck CI / docker (push) Has been skipped
- CHANGELOG: all 9 versions documented (v1.3 → v2.1)
- README: version 2.1.0, full feature list, 73/73 tests
- WORKLOAD: rewritten, 52/52 features (100%), architecture summary
2026-07-10 07:30:54 -04:00
bruno d94817bac5 docs: ROADMAP v1.3.0 ✅ + WORKLOAD/README updated
FlowDeck CI / test (push) Failing after 4s
FlowDeck CI / docker (push) Has been skipped
- ROADMAP: v1.3.0 déplacé dans Completed, item checkboxes [x]
- WORKLOAD: réécrit pour refléter v1.3.0 + plan v1.4.0
- README: version 1.3.0, features à jour, tests 34/34
2026-07-09 22:40:19 -04:00
bruno ff555c6fd4 docs: v0.4.0 complétée — ROADMAP + WORKLOAD à jour
CI / test (push) Failing after 3s
CI / lint (push) Failing after 4s
- v0.4.0 marqué ✅ (14/14 checkboxes)
- v0.5.0 Kanban avancé à 70% (colonnes, groupes, cartes, drag&drop)
- WORKLOAD: Phases 1-3 à 100%, Phases 4-7 progression mise à jour
2026-07-08 14:21:41 -04:00
bruno cc680a46c2 feat: Refonte UI Notion-complete — sidebar, topbar, multi-vues, dark mode
CI / test (push) Failing after 10s
CI / lint (push) Failing after 4s
- Layout 3 zones (sidebar 240px + topbar 44px + contenu)
- Sidebar: sections hiérarchiques, hover states, menu contextuel
- CSS design system Notion: #191919/#222/#333, Inter, coins 4-6px
- Vues: Kanban (groupes+colonnes), Dashboard Notion-style
- Templates: base.html refondu, card.html partial, board.html v2
- Backend: données sidebar + board Notion (groups/cards)
- Docs: ROADMAP v0.4→v1.0, WORKLOAD.md, README
- Backward compatible: conserve _issue_column pour api.py
- Images de référence + resized (analyse 2026-07-08)
2026-07-08 14:13:41 -04:00