Files
flowdeck/CHANGELOG.md
T
bruno 113374e499
FlowDeck CI / test (push) Failing after 3h12m50s
FlowDeck CI / lint (push) Successful in 1m54s
FlowDeck CI / docker (push) Skipped
test: A32 phase 2h — dashboard bloqué : 44/44 routes à 0 ref (v7.19.0)
+5 routes dashboard (fichier test_smoke_uncovered.py à 49 tests) :

- Members POST/PUT/DELETE : invitation de soi-même dans un workspace dédié
  (_own_workspace), rôle admin relu en base, membre supprimé (COUNT=0).
  Quirk documenté : les retours tuple des routes (`{"error": ...}, 400`)
  sont sérialisés FastAPI en tableau + 200 → assert sur `[0]["error"]`.
- upload-folder : validations SEULES (structure absente → 400 « No
  structure provided », JSON cassé → 400 « Invalid structure JSON ») —
  zéro fichier écrit, workspace dédié nettoyé.
- convert-to-database : collection + propriété title + vue table + page en
  content_format='collection' VÉRIFIÉS en base, 404 page inconnue,
  cleanup dans l'ordre FK (pages avant collections — IntegrityError corrigée).

Recoupement final : scan des 44 routes strictement à 0 ref de dashboard.py →
TOUTES exercées. Les 19 résidus du scan sont des faux positifs (paths en
f-string dans les tests : /api/workspace/1/…, f"/api/pages/{id}/…", …)
rapprochés manuellement un par un.

Reste A32 : les 6 routes Gitea d'api.py (issues ×4, créations checklists)
→ stub de transport httpx (effort S).

suite **1086/1086** · `ruff check app tests` OK · docs à jour
2026-10-01 15:24:38 -04:00

174 KiB
Raw Blame History

Changelog - FlowDeck

v7.19.0 (2026-10-01) — Audit : A32 : dashboard bloqué (44/44)

Tests

  • +5 routes dashboard.py — test_smoke_uncovered.py : 49 tests : · Members (POST/PUT/DELETE) : invitation de soi-même dans un workspace dédié, rôle relu en base, membre supprimé (COUNT=0) ; quirk documenté : les retours (..., 400) de ces routes sont sérialisés FastAPI en tableau + 200 ([{"error": "Invalid role"}, 400]) · upload-folder : validations seules (structure absente → 400, JSON cassé → 400) — zéro fichier écrit sur disque, workspace dédié nettoyé · convert-to-database : collection + propriété title + vue table + page en content_format='collection' vérifiés en base, 404 page inconnue, cleanup dans l'ordre FK (page avant collection)
  • Recoupement final : scan des 44 routes strictement à 0 ref de dashboard.py → toutes exercées (19 faux positifs résiduels = paths en f-string dans les tests, rapprochés manuellement)
  • Suite complète : 1086/1086

v7.18.0 (2026-10-01) — Audit : A32 phase 2g (dashboard +13)

Tests

  • +13 routes dashboard.py (cumul 43→56 sur 63) — test_smoke_uncovered.py : 46 tests : · /gitea-workspace : page HTML (200 ou redirection propre) · workspace/projects GET+POST : shape {builtin, gitea, github} avec github == [], projet créé retrouvé dans builtin, quirk error sans nom, nettoyage · Cycle items local-workspace (5 routes) : création → renommage relu en base → move → soft-delete (deleted_at relu) → restore (deleted_at IS NULL relu), nettoyage · Cycle tags d'item (5 routes) : POST (nom lowercasé), tags de l'item, liste workspace, search (shape), suppression vérifiée. Utilisateur + workspace créés dans le test : /api/local-workspace/tags a besoin d'un workspace actif (fallback « premier workspace du user ») — on ne le fait pas dépendre de l'utilisateur fixture partagé, tout est nettoyé
  • Suite complète : 1083/1083

v7.17.0 (2026-10-01) — Audit : A32 phase 2f (dashboard +7, garde-fous A16)

Tests

  • +7 routes dashboard.py (cumul 36→43 sur 63), centrées sur les garde-fous A16 — test_smoke_uncovered.py : 42 tests : · GET /api/files/{ws}/{path} : traversal encodé %2e%2e%2f → 403 « Path traversal denied » ; inexistant → 404 ; vrai fichier écrit dans le data_dir de test → 200 + octets exacts (nettoyé) · GET /api/pages/{id}/download : page markdown → 404 « downloadable » (pas de 500) ; page « file » avec chemin ../ qui sort de la racine → jamais 200 (404), et file-content → 404/415 · 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 — règle « 0 réseau »), 404 sans avatar · GET/POST /api/collections/{id}/table-data|pages : 404 inconnu, shape, ligne créée retrouvée dans table-data, nettoyage finally
  • Suite complète : 1079/1079

v7.16.0 (2026-10-01) — Audit : A32 phase 2e (dashboard +9, comptes)

Tests

  • +9 routes dashboard.py (cumul 27→36 sur 63) : · /accounts + /accounts/settings : 200 HTML, password_hash absent du rendu (whitelist A2 vérifiée côté page) · PUT /api/user/profile : persistance relue en base, valeur d'origine restaurée en finally · PUT /api/user/password : 403 « current password is incorrect » (A3 — la session seule ne change pas le mdp) + quirk assumé et documenté : longueur validée AVANT auth → 200 + message d'erreur · POST /api/user/token : format fd_ + 64 hex, ligne user_tokens nettoyée en finally · DELETE /api/user/forge/{provider} : {"status": "ok"} · PUT /api/settings/account : full_name/email persistés + 400 sur mdp court (validateur), restauration en finally · POST /api/workspaces/{id}/select : Set-Cookie flowdeck_workspace vérifié ; GET /api/local-workspace/breadcrumb : shape liste
  • test_smoke_uncovered.py : 38 tests ; suite complète 1075/1075

v7.15.0 (2026-10-01) — Audit : A32 phase 2d (dashboard +10 routes)

Tests

  • Scan strict dashboard.py : 44 routes à 0 référence (sur 63) — 10 couvertes cette passe dans test_smoke_uncovered.py (32 tests au fichier) : · Tags CRUD : création (SmokeTag → smoketag lowercasé), présence dans la liste, changement de couleur relu, suppression puis absence · Vie d'une page : GET content (contenu seedé relu) → PUT rename (ok + 400 sur titre vide + titre relu en base) → POST trash (parent_section='Trash' + deleted_at relus en base) · sidebar/workspace-tree : 200 HTML, fragment « No pages yet » sans cookie · settings/avatar-color : couleur relue sur l'utilisateur de la session (pas LIMIT 1), valeurs d'origine restaurées en finally · workspace/{id}/members : shape {"members": [...]}
  • Helper _seed_page : surcharge des colonnes par défaut (content=, …)
  • Suite complète : 1069/1069

v7.14.0 (2026-10-01) — Audit : A32 phase 2c (api_v2 +5 routes)

Tests

  • Scan strict des 115 routes api_v2.py contre tous les tests (chaîne de chemin littérale) → 5 routes à 0 référence, toutes couvertes : · POST /properties/evaluate-formula : 200 + shape, 400 sans expression (le moteur renvoie 1 + 2 tel quel aujourd'hui — le smoke valide le câble route/auth/parse, pas le moteur) · POST /properties/compute-rollup : 400 collection_id required, 401 sans bearer · GET /admin/audit-logs : portail admin vérifié — attendu calculé depuis /users/me (le tout premier utilisateur d'un worker est admin, état non contrôlable depuis le test), + token scope admin → 200 + logs liste · GET /webhooks/events : catalogue non vide + wildcards */page.* · POST /webhooks/verify-signature : valid=True avec sign_payload(secret, payload) (même helper que le serveur), False avec une signature bidon
  • test_smoke_uncovered.py : 27 tests au total
  • Suite complète : 1064/1064

v7.13.0 (2026-10-01) — Audit : A32 phase 2b (api.py 16/22)

Tests

  • api.py passe de 3 à 16 routes couvertes (22 @router au total) : · board-config GET/POST : défauts à 5 colonnes sans board, création puis relecture du roundtrip · col-mapping POST/DELETE : 404 sans board, upsert label vérifié, suppression vérifiée · card POST : 404 sans board, {"status": "ok"} avec · collaborators GET : gitea.get_collaborators stubbé (zéro 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 (réapparition COUNT=0) — seed + cleanup
  • Reste api.py : 6 routes Gitea (issues ×4 + créations checklists) → stub de transport httpx. Reste global : dashboard.py 17/63, api_v2.py 50/115

Tests

  • Suite complète : 1059/1059 (236 s) ; test_smoke_uncovered.py : 22 tests

v7.12.0 (2026-10-01) — Audit : A32 phase 2a (library 10/10)

Tests

  • library.py passe de 1/10 à 8 routes couvertes : les 5 listes (recents/favorites/published/private/workspace) en un test de boucle, /private avec une page seedée et retrouvée, /children/{id} avec un parent/enfant seedés (titre retrouvé), /repository vide et clé (aucun appel réseau — la clé n'est qu'une string de workspace)
  • Test de non-régression 404 sur les 2 routes supprimées

Removed

  • 2 routes cassées supprimées (découverte des smokes) : /api/library/local-workspace-children/{id} renvoyait un 500 systématique et /api/library/local-workspace un 500 dès qu'un workspace existait — les deux lisaient local_workspace_items, une table qui n'est créée nulle part dans le codebase (grep : 0 CREATE TABLE), avec 0 référence front. library._format_size devenu mort : supprimé aussi (une version vit dans dashboard.py, inchangée)
  • local_workspace_items : plus aucune occurrence dans app/

Tests

  • Suite complète : 1053/1053 (229 s) ; test_smoke_uncovered.py : 16 tests

v7.11.0 (2026-10-01) — Audit : A32 phase 1 (routers à 0 test)

Tests

  • tests/test_smoke_uncovered.py — 10 smoke tests, un 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) ; register sans secret → 400 avant tout appel réseau ; status avec gitea.list_webhooks stubbé (zéro accès réseau réel) · notes.py (2/2) : GET HTML + roundtrip POST→GET (upsert persisté, échappement HTML vérifié <b>) · sidebar_config.py (2/2) : GET défauts, PUT persisté relu depuis users.sidebar_config, 400 sans config, remise en état en fin de test · github_routes.py (2/2) : status {"linked": False}, disconnect ok
  • Reste (phase 2) : quasi nuls — library.py 1/10, api.py 3/23, dashboard.py 17/63, api_v2.py 50/115
  • Suite complète : 1047/1047

v7.10.0 (2026-10-01) — Audit : A21 phase 2b (api_v2 bouclé)

Changed

  • Helper 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 : bloqué = le worker, jamais la boucle d'event, et la réponse n'est envoyée qu'une fois l'événement terminé (déterministe, équivalent sémantique de l'await d'avant). Note ponytail: : clients httpx créés à chaque appel partout → aucun lien de boucle ; sinon run_coroutine_threadsafe + boucle du lifespan
  • A21 (phase 2b) — 15 routes api_v2 dont les seuls awaits étaient request.json / _fire_event / fire_published / fire_unpublished → paramètre Body(default={}) + run_event_sync(...) + conversion en def
  • api_v2 : 111/115 routes hors event loop — il ne reste que 4 routes async, toutes avec de vrais awaits réseau : import_csv_v2 (multipart), project_tree_v2 (gitea), test_webhook_v2, retry_webhook_deliveries
  • Repo-wide : 403 routes synchrones (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 en 228 s

v7.9.0 (2026-10-01) — Audit : A21 phase 2a (api_v2 hors loop)

Changed

  • A21 (phase 2a) — dans api_v2, les 36 routes dont le seul await était body = await request.json() passent à un paramètre FastAPI body: dict = Body(default={}) (parsing async fait par FastAPI avant l'appel) puis sont converties en def → threadpool. Équivalences vérifiées avant-engagement : corps absent → {} (identique au try/except d'avant), JSON invalide → 422 (avant : traité silencieusement comme {}), zéro body[...] = dans le fichier (le défaut partagé n'est jamais muté)
  • api_v2 : 96/115 routes hors event loop (60 en phase 1 + 36 ici) ; il ne reste que 19 routes async dans ce router (fire_event, request.form, appels gitea/webhooks — phase 2b)
  • Bug de transformation évité en cours de route : première version du script supprimait 5 lignes au lieu de 4 (slice fermant d'un cran trop loin) — fichier restauré depuis git puis script corrigé, 0 ligne perdue (diff logique : +39/-183 = 36 signatures + import, 4 lignes de try/except × 35)

Tests

  • Suite complète 1037/1037 (242 s) ; ciblée sur test_public_api_v2 + test_v65 + audit : 62/62 verts avant la passe complète

v7.8.0 (2026-10-01) — Audit : A21 phase 1 (SQLite hors event loop)

Changed

  • A21 (phase 1) — 352 routes async def sans aucun await converties en def : FastAPI les exécute alors dans son threadpool — tout leur travail SQLite (get_conn() + conn.execute) quitte l'event loop, sans changer une ligne de logique (la conversion est sémantiquement neutre : vérifié corps par corps — aucun await/async with/async for/asyncio dans les fonctions converties). Répartition : api_v2 60, dashboard 40, collections 25, board 23, workspace 19, wiki 17, permissions 14, api 14, + 35 autres fichiers
  • Reste (phase 2) — les 311 routes qui ont de vrais await (request.json(), fire_event, httpx) : enrouler les blocs DB dans await anyio.to_thread.run_sync(...) ; aucun wrapper partagé livré pour l'instant (rien ne l'appellerait)

Perf

  • Dernière suite : 233 s (écarts précédents mesurés : 235-359 s) — les handlers SQLite ne saturent plus la boucle pendant les tests

v7.7.0 (2026-10-01) — Audit : A20 (CSP — nonce, partie 1)

Security

  • A20 — script-src : 'unsafe-inline' remplacé par 'nonce-<aléatoire par requête>'. Le middleware CSP génère le nonce dans une ContextVar avant call_next (visible des templates via {{ csp_nonce() }}) ; 38 tags <script> inline des templates, la constante de module LOCAL_LOGIN_HTML (helper _with_nonce() au rendu) et 3 scripts Python dans collections.py le portent ; htmx reçoit le même nonce via <meta name="htmx-config"> (inlineScriptNonce — les scripts des réponses boostées restent valides)
  • Les 74 handlers onclick= inline restent fonctionnels via script-src-attr 'unsafe-inline' (détaché de script-src : le nonce les aurait désactivés aussi)
  • https://cdn.jsdelivr.net / https://unpkg.com ajoutés à script-src et style-src : les vues chart/map de collections les utilisent et étaient bloquées par la CSP depuis toujours (commentaire ponytail: → upgrade : vendoriser ces libs puis retirer les hôtes)
  • Reste d'A20 : unsafe-eval (Alpine x-data en string → build @alpinejs/csp), externalisation du JS inline (A27), resserrer img-src/connect-src

Tests

  • test_csp_nonce_per_request : page base.html (meta htmx-config + nonce du header identique sur tous les scripts inline, nonce différent d'une requête à l'autre) et page hors template (/auth/login?provider=local)

v7.6.0 (2026-10-01) — Audit : A31 (dette migrations)

Fixed

  • A31 — transaction par migration : _apply_one() fait BEGIN → 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é
  • A31 — 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. 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

Tests

  • test_migration_transaction_rolls_back (DDL partiel annulé + pas de marque de version, chemin nominal enregistré), test_columns_helper_validates_table_name

v7.5.0 (2026-10-01) — Audit : A29, A42 (partiel)

Changed

  • A29 — services/publish.py partagé : les 3 paires publish/unpublish (sharing = front, board, v2) déléguent ; 404 partout (board faisait une mise à jour aveugle), slugify titré unique (board : aléatoire ; v2 : slug fourni conservé), événements centralisés, board gagne le contrôle de session. Les bonus divergents disparaissent (share_mode='anyone' pour board, is_shared=1 pour v2) : le share dialog reste l'unique propriétaire de ces drapeaux, dépublier ne révoque donc pas un partage manuel. Les listings /users/me ×2 et collections ×3 restent : contrats versionnés distincts
  • A42 — 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 (il ne pouvait que grandir) ; les 29 Environment(...) étaient déjà couverts par A10. Reste : client httpx partagé (52 créations, à faire avec un cache par event loop)

Security

  • Byproduct A29 — GET /api/users/me (v1) et le contexte Jinja de /accounts renvoyaient SELECT * sur users : password_hash, login_attempts et locked_until exposés → colonnes whitelistées (identiques à la liste v2)

Tests

  • test_publish_service_shared_and_safe (slug, 404, partage préservé), test_users_me_no_secret_columns, test_gitea_cache_evicts_expired

v7.4.0 (2026-10-01) — Audit : A30, A37, A39, A40, A41

Changed

  • A30 — require_scope() est enfin câblé : 69 sites stricts de api_v2.py passent par la factory (Bearer + scope en un appel, contrôle manuel supprimé ; les 4 variants admin|is_admin restent manuels, ce sont d'autres contrôles) ; 12 top-level morts supprimés (unsync_block, find_referring, _b64url, strip_markdown, format_number, … — 0 référence app ET tests)
  • A37 — CORS : plus de allow_origins/methods/headers = ["*"] → origines dérivées de settings.app_base_url + localhost/origines d'extension (allow_origin_regex), méthodes et entêtes minutés, allow_credentials=True explicite ; test test_cors_no_star
  • A40 — version d'assets à source unique : {{ asset_version }} (global Jinja lu au boot depuis le fichier VERSION) ; les littéraux ?v=5.1.1, ?v=2.4.8, ?v=6.0.0 de base.html éliminés ; sw.js n'existe plus (audit obsolète) ; vendors gardent ?v= = version de la lib (correct)
  • A41 — 91 règles CSS mortes purgées d'app.css : -10 274 octets (121 618 → 111 344) — scan : classes définies dans app.css et absentes de templates, JS, autres CSS et code Python

Notes

  • A39 (htmx) — décision « rien » : 32 attributs hx-* réels, conversion = refonte du view-switching sans tests E2E ; à reconsidérer avec un test automatisé du view-switch

v7.3.9 (2026-10-01) — Audit : A26, A33, A34, A35, A36, A43

Fixed

  • A26 — champ standalone mort supprimé (0 lecteur) ; sqlite:////data/… ne produit plus un chemin UNC sous Windows ; .env.example ne promet plus PostgreSQL (non supporté) ; raise au boot si APP_SECRET_KEY vaut encore la valeur par défaut (il signe les sessions)
  • A33 — rate limit : préfixes manquants ajoutés (/scim/v2/, /workspace/, /db/, plus le non-GET sur /s/ et /f/ sans pénaliser la lecture) ; la limite vient de settings.rate_limit_requests (60 annoncés, 100 codés en dur) ; clé = X-Forwarded-For uniquement derrière un proxy local ; _store épuré (croissance mémoire bornée)
  • A34 — helper _spawn() pour les 10 schedulers : exception loggée + redémarrage après 10 s (ils mouraient en silence) ; 2 logger.debug de scheduler passés en warning
  • A35 — OpenAPI régénéré : 439 → 511 chemins, info.version 7.3.9 ; README à jour (était v6.7.0) ; compteur de API_GUIDE_V6.md à jour ; titre dupliqué retiré du ROADMAP
  • A36 — 4 dépendances mortes purgées de requirements.txt (aiosqlite, slowapi, loguru, packaging = 0 import)
  • A43 — 15 datetime.utcnow() dépréciés → now(UTC).replace(tzinfo=None) (format ISO naïf identique, zéro changement de comportement)

Notes

  • Le drift Python (Docker/CI/README 3.12 vs venv local 3.13) reste ouvert : l'alignement à 3.13 implique un rebuild d'image à valider

v7.3.8 (2026-10-01) — Audit : A25 (exceptions muettes) + A21 partiel

Fixed

  • A25 — 84 except Exception: pass/… deviennent logger.exception(fn) (19 fichiers, 63 dans des handlers async) : les échecs du pipeline d'événements/webhooks et des écritures sont enfin visibles dans les logs
  • A25 (critique) — plus de try autour de materialize_properties dans create_collection_v2 et apply_db_template_v2 : un échec annule la transaction au lieu de commiter une collection sans schéma
  • A21 (partiel) — PRAGMA busy_timeout=5000 dans get_conn() (le seul point d'entrée des connexions) ; le wrapper async + les 510 call sites synchrones sur l'event loop restent à migrer

Tests

  • test_collection_rollback_when_materialize_fails → suite 1028/1028

v7.3.7 (2026-09-30) — Audit sécurité : A14 (fallback admin agent)

Fixed

  • A14 — _current_user_id et _current_admin ne retombent plus sur la row admin : 401 sans session (les 24 sites de _current_user_id + PATCH/POST /api/agent/providers) — un anonymous ne pouvait plus orienter le ping() du serveur vers un api_base interne
  • _check_api_base() sur les 2 routes provider : scheme http(s) obligatoire, identifiants dans l'URL refusés (400). Les hôtes privés restent acceptés — le provider par défaut du produit est Ollama http://localhost:11434/v1 ( commentaire ponytail: : fermeture possible via allowlist provider local)
  • Test test_agent_providers_require_admin_and_valid_api_base → suite 1027/1027

Fixed

  • A19 (fin) — les 46 appels non-GET restants des 5 derniers préfixes (/api/agent, /api/settings, /api/local-workspace, /api/gitea, /api/workspace + /api/workspaces) reçoivent X-CSRF-Token (expression cookie en ligne, portée indifférente fonction/Alpine/attribut) ; les 5 préfixes sortent d'EXCLUDED_PATHS
  • Vérification syntaxe : les <script> des 39 templates passent node --check (interpolations Jinja neutralisées) — 0 échec avant/après
  • Tests : anon_csrf() là où le 403 CSRF masquait le 401 attendu, paire CSRF sur le TestClient jetable de test_sessions_listed_and_revocable
  • suite 1026/1026 · ruff check app tests OK — la liste CSRF ne contient plus que du Bearer, des callbacks /auth/*, des pages publiques et de l'infra

v7.3.5 (2026-09-30) — Audit sécurité : A19 (partiel) — CSRF réduit aux vrais cas

Fixed

  • A19 (partiel) — 12 préfixes sortis de EXCLUDED_PATHS après scan des appels non-GET du front (tous envoient déjà X-CSRF-Token) : /db/, /workspace, /workspace/automations, /board/api/favorites, /api/pages, /api/recents, /api/notifications, /api/comments, /api/automations, /api/github, /api/admin, /api/onboarding — les 2 fetch de welcome.html équipés du header
  • La liste ne garde que Bearer/webhooks/callbacks/pages publiques + les 5 préfixes dont le front n'est pas encore équipé (/api/workspace, /api/local-workspace, /api/settings, /api/gitea, /api/agent)
  • Helper anon_csrf() en test (anonyme + CSRF valide → on mesure le 401 de la route, pas le 403 du middleware) → suite 1026/1026

v7.3.4 (2026-09-30) — Audit sécurité : A16 (ACL sur lectures)

Fixed

  • A16 — _load_page_or_404 (4 exports) et les 2 routes pièce jointe (/download, /file-content) passent par session + PermissionManager.can_view_page → 401 sans session, 404 hors ACL ; la lecture legacy board.py était déjà couverte par A7
  • Test test_exports_and_attachments_require_auth → suite 1026/1026

v7.3.3 (2026-09-30) — Audit sécurité : A12–A24 (SSRF, auth legacy, uploads, perf)

Fixed

  • A12 — unfurl OG : follow_redirects manuel + _is_public_host à chaque saut → 400 vers loopback/link-local (ex. 169.254.169.254)
  • A13 — automations : Depends(_require_session) sur le router entier (CRUD, run, press-button) + action webhook validée avant POST
  • A15 — webhooks sortants : admin exigé + URL publique (SSRF scheduler)
  • A17 — router legacy /api : session ou Bearer (/api/v1) ; allowlist explicite /api/health, /api/frontend-error
  • A22 — uploads locaux : session exigée, validate_upload branché (10 MB + extensions), FLOWDECK_DATA_DIR remplace le /data codé en dur
  • A23 — N+1 : GROUP BY (compteurs de pages), executemany (cards de sync
    • duplicata de propriétés avec remap d'ids vérifié)
  • A24 — 2 routes silencieusement écrasées supprimées + test « aucun doublon méthode+chemin » sur les 680 routes

Tests

  • +9 non-régressions dans tests/test_audit_p0_fixes.py → suite 1025/1025

v7.3.2 (2026-09-30) — Audit sécurité : A11 + A18

Fixed

  • A11 — GET /api/settings/avatar/{filename:path} : resolve() + relative_to() (le motif de serve_uploaded_file) → 403 hors /data/avatars
  • A18 — GET /workspace/public/{id} : 404 explicite pour les bases restricted/private (permission_type) et html.escape sur nom, icône et titres de lignes — ce f-string HTML ne passe pas par Jinja2, donc l'autoescape A10 ne le couvrait pas
  • Tests : tests/test_audit_p0_fixes.py (3 non-régressions) — suite 1019/1019

v7.3.1 (2026-09-30) — Audit sécurité P0 : A1–A10

Corrections du bloc critique de l'audit du 2026-09-30 (ROADMAP) : plus aucune route cookie-auth n'accepte un anonymous, et Jinja2 échappe enfin sa sortie.

Fixed

  • A1/A2 — deps pyotp/webauthn/cbor2 installées, rebinding de app.config.settings supprimé dans test_v54.py (isolation rétablie) ; cycle v6.8→v7.3 committé + tag v7.3.0
  • A3 — PUT /api/user/password : 401 sans session + current_password exigé ; helper _require_user_id() sur profile/password/token/forge ; /api/user sorti de la liste CSRF exemptée
  • A4 — POST /api/v1/token et POST /api/user/token : 401 sans session, chemin legacy user_id=0 supprimé
  • A5 — CRUD membres d'espace : session + rôle admin de l'espace (ou admin global), placeholder user créé en is_admin=0
  • A6 — _require_view → 404 / _require_edit → 401 sans session (fin du legacy single-user sur les collections)
  • A7 — création ET lecture de page → 401 sans session (PermissionManager), /board/api/pages sorti du CSRF exempt (+ header manquant côté local workspace)
  • A8 — seed admin sans mot de passe codé en dur : aléatoire au premier boot (loggé une fois) ou FLOWDECK_ADMIN_PASSWORD
  • A9 — .db/fichiers de test désindexés + .gitignore/.dockerignore, rotation de APP_SECRET_KEY
  • A10 — app/templating.py : un seul ENV avec autoescape=select_autoescape(["html"]), 29 instantiations remplacées ; re-tri des |safe (corps d'issue + commentaires échappés, sidebar_config en |tojson)

Tests

  • Client de test connecté par défaut (_TestSessionAuth : session + CSRF injectés hors cookie jar) + helper anon() sur les 40 tests d'anonymat
  • Suite complète : 1016 passed / 0 failed · ruff check app tests OK

v7.3.0 (2026-09-29) — Wiki / Teamspaces + Polish (dernière version du cycle v7)

Connaissance vérifiée et finition collaborative : teamspaces, badge ✅ avec expiration, guests sans compte, réactions, follows, analytics de page et trois nouveaux blocs rendus côté serveur. Design : docs/V73_Wiki_Teamspaces_Polish.md.

Added

  • Teamspaces — app/services/wiki.py + app/routers/wiki.py : teamspaces (workspace_id, private, UNIQUE(workspace_id, name)) et teamspace_members (rôles owner/editor/commenter/viewer) ; CRUD + listing par workspace, ajout/retour de membres ; private=1 → 404 (pas 403) pour les non-membres, comme les collections restricted ; un teamspace public reste cantonné au workspace (pas de viewer implicite pour un compte qui n'en est pas membre) ; pages.teamspace_id + collections.teamspace_id
  • Verified pages — page_verifications (badge ✅, verified_by, note, expires_at 90 j par défaut, re-vérifier remplace) ; l'index GET /api/v2/wiki/verified exclut les badges expirés et les pages de teamspaces privés ; sweep POST /api/v2/wiki/verify-expiry-sweep (admin) notifie le vérificateur à J-7 (page.verification_expiring) ; la vérification exige un rôle editor/owner/admin, sinon 403
  • Guests sans compte — guest_shares (token, role viewer|commenter, expires_at, revoked) ; accès par GET /g/{token} sans session, enregistrement d'une vue, révocation idempotente ; page 404 HTML dédiée au lieu d'une redirection vers /workspaces
  • Collab polish — comment_reactions (agrégation par emoji + users, toggle), page_follows (toggle + followers), page_views (compteurs journaliers, séries sans trou via view_stats)
  • Wiki Home — GET /api/v2/wiki/home (teamspaces + verified + recents)
  • Blocs — app/services/wiki_blocks.py : mermaid (SVG inline si mmdc est installé, sinon <pre class="mermaid"> rendu côté client), equation_inline (KaTeX, source sanitizée : <, >, \ retirés pour empêcher la fermeture anticipée du délimiteur ou l'injection de balises), progress (agrégation directe sur property_values_json → barre %) ; les trois sont rendues par export.blocks_to_html (donc présentes dans l'export HTML/PDF) et exposées via POST /api/v2/wiki/blocks/preview
  • Migration 29 — teamspaces, teamspace_members, page_verifications, comment_reactions, page_follows, guest_shares, page_views + colonnes teamspace_id sur pages/collections
  • Sidebar teamspaces — section Teamspaces dans base.html (état Alpine teamspaces/loadTeamspaces/openTeamspace, section ouverte par défaut, icône/label/ordre, fallback du panneau de personnalisation) → GET /api/v2/wiki/teamspaces accepte désormais l'omission de workspace_id (listing cross-workspace avec workspace_name), page HTML GET /wiki/teamspaces/{id} (pages + collections du teamspace, rôle affiché) ; entrée teamspaces dans sidebar_config.DEFAULT_CONFIG
  • Notif page.updated aux followers — wiki.notify_followers_of_page_update (une par 10 min, actor_id exclu) branchée dans automations.fire_event ; payloads actor_id ajoutés dans board.update_page et board.save_page_blocks ; commenter une page = suivre (auto-follow wiki.ensure_follow, défaut ON)
  • Charts avancés — type number (KPI) avec agrégats count|sum|avg|min|max dans collections._render_chart (+ _chart_aggregate/_fmt_number) ; le « 0 » n'est plus forcé à 1 ; les dashboards multi-DB se rendent via GET /db/{collection_id}/dashboards/{dashboard_id} (widgets collection_id, ≤ 40 widgets, ≤ 200 lignes/chart) ; view_collection choisit maintenant la config de vue correspondant au view_type demandé
  • Unfurl gitea:/github: — POST /board/api/og/metadata résout les refs gitea:owner/repo / github:owner/repo via GiteaClient.get_repo_info (ajouté) ou GitHubAdapter (token optionnel, sinon API publique) sans télécharger la page ; champs bookmark url/title/description/image/site_name conservés dans l'autosave du bloc
  • UI Settings → Audit — settings.html : l'onglet admin-audit interroge désormais /api/v2/audit/logs (sources api/permissions/sso, filtres actor/action, pagination « Load more », export CSV)
  • SSO 21 casses — dépendances python3-saml==1.16.0 + authlib==1.8.0 (plus xmlsec/isodate/joserfc) installées → test_v67_sso.py 38/38
  • Sécurité — sanitize_equation retire désormais </>/\ (pas de breakout KaTeX/markup), revoke_guest 404 fondé sur l'existence, pas le rowcount

Tests

  • tests/test_v73_wiki_polish.py : 58 → 72 tests (sidebar cross-workspace + page teamspace owner/outsider, auto-follow par commentaire, notif page.updated throttlée + acteur exclu, ensure_follow idempotent, regex + unfurl gitea/github + endpoint /board/api/og/metadata, KPI + _chart_values (0 conservé) + dashboards multi-DB + 404)
  • ruff check app tests OK · suite complète python -m pytest -n auto : 1016 passed (était : 981 passed / 21 failed en SSO avant install des deps)

v7.2.0 (2026-09-29) — Enterprise admin : SCIM 2.0, 2FA, Audit, gouvernance agents

Le socle administration d'une instance auto-hébergée en équipe : provisionnement SCIM depuis l'IdP, TOTP + passkeys, journal d'audit unifié et garde-fous d'exécution pour les agents. Design : docs/V72_Enterprise_SCIM_2FA.md.

Added

  • SCIM 2.0 — app/routers/scim.py : GET/POST /scim/v2/Users, GET/PUT/PATCH/DELETE /scim/v2/Users/{id} (Bearer scim_tokens, schémas core:2.0:User, active → users.is_active + révocation user_sessions, Operations PATCH active/userName) ; tokens SHA-256 stockés, affichés une seule fois, révocables (/api/v2/scim/tokens) ; exempté CSRF (clients IdP sans cookie) et 404 application/scim+json au lieu d'une redirection
  • TOTP 2FA — app/services/two_factor.py : secret chiffré Fernet au repos, 10 codes de secours à usage unique (SHA-256), Défi pending signé 5 min ; POST /auth/local-login renvoie 2fa_required sans créer de session, puis POST /auth/local-verify l'échange contre une session ; setup/activate/ disable/status côté utilisateur
  • Passkeys WebAuthn — app/routers/webauthn.py : enregistrement (register/begin|finish, attestation vérifiée, COSE stocké) et connexion sans mot de passe (login/begin|finish, anti-rejeu sign_count, vérif. origine/RP) ; listing + suppression des clés
  • Domain claims — POST /api/v2/domain-claims (normalisation lowercase, jeton .well-known/flowdeck-verify.txt), POST .../verify (fetch HTTPS du domaine + comparaison), enforce_sso qui bloque le login local par domaine dans auth.local_login (les admins gardent l'accès local) ; jeton jamais renvoyé par le listing
  • Audit unifié — app/routers/audit.py : GET /api/v2/audit/logs fusionne api_audit_log + permission_audit_log + sso_login_history en un schéma commun, filtres source/actor/action, pagination, export ?format=csv (admin ou Bearer read:admin)
  • Gouvernance des agents — app/services/agent_policies.py + app/routers/governance.py : agent_policies (liste d'outils autorisés par workspace, max_steps, require_approval) consultée par AgentEngine avant les ACL, file agent_approvals pour les écritures, décision admin (/api/v2/agent-approvals/{id}/decide) + événement agent.run.approval_requested
  • Migration 28 — scim_tokens, domain_claims, webauthn_credentials, agent_policies, agent_approvals + users.totp_secret_enc, users.totp_backup_hashes

Fixed

  • Les 404 sur /scim/v2 et /auth/webauthn renvoyaient une redirection 302 vers /workspaces (handler global) : ils retournent désormais du JSON, et /scim/v2 répond en application/scim+json
  • /scim/v2 et les routes 2FA/WebAuthn ajoutées à la liste d'exclusion CSRF (authentification Bearer, pas de cookie de session)

Tests

  • tests/test_v72_enterprise.py : 52 tests (migration 28, SCIM tokens/CRUD/ suspend/duplicate/404, 2FA setup-activate-verify-backup-chiffrement-désactivation, challenges, domain claims, WebAuthn, audit multi-source + filtres + CSV + pagination, politiques et gate d'approbation)
  • ruff check app tests OK · tests/test_agent.py + tests/test_app.py : 261 verts (la gouvernance ne casse pas l'engine)

v7.1.0 (2026-09-28) — Calendar sync + Meeting Notes

Calendrier bidirectionnel Google/CalDAV + transcription → résumé IA qui déclenche les agents (pattern Notion 07/2026). Design : docs/V71_Calendar_Meetings.md.

Added

  • Sync bidirectionnelle — app/services/calendar_sync.py : calendar_links (tokens Fernet, collection_id, date_property), pull (event → ligne datée + external_event_id) + push, boucle 15 min dans le lifespan ; conflits (édité des 2 côtés → last-write-wins + notif calendar.conflict) ; lignes touchées par le pull jamais repoussées ; API CRUD + POST .../sync (app/routers/meetings.py, session ou Bearer write, tokens jamais leakés)
  • Sans dépendance — Google Calendar REST (401 → « relink »), CalDAV brut (REPORT + parseur multistatus, PUT) ; I/O module-level monkeypatchables
  • Meeting Notes v2 — app/services/meetings.py : upload audio 100 MB (mp3/wav/m4a/ogg/flac/aac), transcription STT_COMMAND ou transcript manuel, résumé ai_writing.summarize (offline-capable) → trigger meeting.summarized (branché automations v7.0) ; endpoints upload/texte/summarize
  • Free/busy — GET /db/{id}/calendar/freebusy (busy + free weekdays, récurrences expandues, 1..370 j)
  • Migration 27 — calendar_links, meeting_transcripts, collection_pages.external_event_id + index

Tests

  • tests/test_v71_calendar_meetings.py : 15 tests (migration, links, pull/push/idempotence/conflit + notif, 502, CalDAV, freebusy, meetings ×5)
  • ruff check app tests OK · suite 871 verts (21 échecs test_v67_sso.py pré-existants — onelogin absent de l'environnement)

v7.0.0 (2026-09-28) — Automations v2 + Workers lite

Du if-this-then-that aux chaînes multi-triggers + custom code sandboxé, parité Notion Automations + Workers. Design : docs/V70_Automations_Workers.md.

Added

  • Automations multi-étapes — automation_steps (trigger/condition/delay/action ordonnés) : CRUD GET/POST /workspace/automations/{id}/steps, PUT/DELETE /workspace/automations/steps/{id} (session, validation serveur), PUT .../mode (any défaut / all fenêtre 5 min) ; conditions AND réutilisant match_condition_props ; form.submitted déclenchable ; legacy sans steps intact (matcher legacy ignore les automatisations à steps → pas de double run)
  • Nouvelles actions — slack (incoming webhook, URL chiffrée Fernet au repos, décryptée à l'exécution), email (user:<id> résolu ou reply-to créateur, repli propre sans SMTP), forge_issue (Gitea via GiteaClient / GitHub API, token user_oauth_tokens), agent_trigger (conversation + run AgentEngine), delay (0..86400s validé, sleep plafonné 300s) ; interpolation [[prop]] / {{title}} conservée ; backends module-level = monkeypatchables
  • Bouton DB natif — type button (PROPERTY_TYPES), button_automation_id, POST /api/automations/press-button (CSRF-exempt, 400 explicites)
  • Workers lite — app/services/workers.py (lint AST : imports réseau/OS, open/exec/eval, dunders ; builtins restreints ; thread + timeout 30s ; budget journalier ; fork des partagés) + app/routers/workers.py (CRUD /api/v2/workers* session/Bearer write, code masqué aux non-owners, run/runs/fork/usage) ; crons branchés sur la boucle scheduler 60s
  • Migration 26 — automation_steps, workers, worker_runs, automations.trigger_mode, collection_properties.button_automation_id

Tests

  • tests/test_v70_automations_workers.py : 31 tests (migration, steps, any/all, chaînes, delay, 4 nouvelles actions, secret chiffré, button, legacy, workers ×11)
  • ruff check app tests OK · suite 855 verts (21 échecs test_v67_sso.py pré-existants — onelogin absent de l'environnement ; 1 flaky parallèle test_env_config_fallback… qui passe isolé)

v6.9.0 (2026-09-28) — Recherche sémantique + Ask AI

Retrouver (hybride lexical + vectoriel) et demander (RAG avec citations), parité Notion Enterprise Search + AI Q&A. Design : docs/V69_Search_Ask_AI.md.

Added

  • Moteur sémantique — app/services/semantic_search.py : chunking chevauchant (1200 cars / overlap 150, récursif dans children), encodeur hashed-TF hash-256 (md5 % 256, L2, déterministe, zéro dépendance, embed_texts() pluggable), cosinus pur Python, fusion RRF (k=60), job incrémental index_pending() (batch 50, scheduler 5 min dans le lifespan) + purge_orphans()
  • Recherche hybride — GET /api/v2/search/hybrid (app/routers/search_ai.py, session ou Bearer read) : lexical FTS5/LIKE + vecteurs, filtre workspace_id, scope membership, PermissionManager (pages restricted masquées), search_excluded respecté, pagination + X-Total-Count
  • Ask AI — POST /api/v2/search/ask : top-8 chunks autorisés (ACL avant prompt), LLMClient.complete() si provider configuré sinon extractif offline avec [[fdpage:ID]], citations résolues (titres, « Deleted page » gérée), cache 10 min, rate-limit 30/min, api_audit_log
  • Observabilité — GET /api/v2/search/index-status (ressources, vecteurs, modèle)
  • Migration 25 — semantic_embeddings, semantic_index_state, pages.search_excluded

Tests

  • tests/test_v69_search_ask.py : 24 tests (migration, chunk/overlap, déterminisme, cosinus, index idempotent, exclusion, purge, rappel partiel, hybride ×7, ask ×6, index-status)
  • ruff check app tests OK · suite 825 verts (seuls 21 échecs test_v67_sso.py pré-existants — onelogin absent de l'environnement)

v6.8.0 (2026-09-28) — Sites & Forms publics

Parité Notion Sites + Forms : publier un mini-site multi-pages et collecter des réponses anonymes dans une database. Design : docs/V68_Sites_Forms.md.

Added

  • Sites multi-pages — app/routers/sites.py : CRUD GET/POST/PATCH/DELETE /api/v2/sites (session ou Bearer + scope write, pagination X-Total-Count, api_audit_log), GET/POST/DELETE /api/v2/sites/{id}/pages (arbre ordonné, racine protégée), GET /api/v2/sites/{id}/stats (vues jour + total) ; rendu public GET /s/<slug> + GET /s/<slug>/<page-slug> (nav latérale, thèmes light/dark, blocs/synced/wiki résolus)
  • Gating & SEO — mot de passe (password_utils salé, cookie signé 24h, GET|POST /s/<slug>/auth), expiry (410), noindex, OG/Twitter cards, GET /s/<slug>/sitemap.xml, domaine custom via header Host
  • Forms publics — PUT/GET /api/v2/collections/{id}/form (enabled, public_token f_*, fields, required, success_message, notify_user_ids) ; GET /f/<token> (formulaire no-auth, ?embed=1 sans chrome) + POST /f/<token> (anonyme : rate-limit 20/h/IP, honeypot, validation validate_property_rule) ; chaque soumission = collection_pages + form_responses (ip_hash jour, pas d'IP), notif in-app + trigger form.submitted
  • Migration 24 — tables sites, site_pages, site_views, form_responses, colonne collections.form_config_json
  • CSRF exempte /s/ + /f/ (soumissions anonymes cross-site)

Tests

  • tests/test_v68_sites_forms.py : 20 tests — migration 24, CRUD, slug/conflit, auth 401, isolation inter-users, pages add/remove, rendu home/subpage/blocs, 404, vues comptées, password gate, expiry 410, sitemap, noindex, form config/submit JSON, required 400, 404, honeypot, rate-limit 429, embed
  • ruff check app tests OK · pas de régression (801 verts ; seuls 21 échecs test_v67_sso.py pré-existants — dépendance onelogin absente de l'environnement)

v6.7.0 (2026-09-24) — SSO / SAML + OIDC entreprise (Enterprise Auth)

Dernière feature de la roadmap v6.0.0 : authentification fédérée via un IdP d'entreprise (SAML 2.0 ou OpenID Connect + PKCE), auto-provisioning des comptes, mapping des groupes de l'IdP vers les rôles workspace, mode « SSO only » et configuration admin dans Settings. Design : docs/V6_SSO_SAML_Enterprise_Auth.md (checklist §9 cochée).

Added

  • SAML 2.0 SP — app/auth/providers/saml_provider.py (python3-saml 1.16.0) : login SP-initié GET /auth/saml/login (RelayState = id AuthnRequest + jeton CSRF en DB), ACS POST /auth/saml/callback (signature, audience, destination, InResponseTo, anti-replay sso_requests), SP metadata GET /auth/saml/metadata, SLO GET|POST /auth/saml/logout (relais LogoutRequest/Response vers l'IdP) ; allowSingleLabelDomains activé pour les hôts homelab/LAN
  • OIDC + PKCE — app/auth/providers/oidc_provider.py (authlib 1.8.0) : GET /auth/oidc/login (state + code_verifier stockés en DB), GET|POST /auth/oidc/callback (code → token → userinfo ; ID token vérifié via JWKS : aud/iss/nonce/exp), discovery + JWKS mis en cache 1 h, GET|POST /auth/oidc/logout (end_session_endpoint si présent)
  • Provisioning — app/services/sso_provisioning.py : création automatique de l'utilisateur au 1er login (auth_method=saml|oidc, lien via email), rôle par défaut + default_workspace_id, mapping groupes IdP → rôles workspace (re-synchronisé à chaque login + POST /api/v2/sso/sync), mode SSO only (login local refusé sauf administrateurs — design §7.1), secrets chiffrés Fernet dans sso_config (champ secret vide = conserver l'existant), détection de rejeu via sso_requests (TTL 15 min), sso_login_history (succès + échecs), safe_next_path (pas de redirection externe)
  • Routeur — app/routers/sso.py : parcours navigateur + API GET|POST|PUT|DELETE /api/v2/sso/config (admin, header X-CSRF-Token, audit_log), GET /api/v2/sso/providers (public, boutons de la page de login), GET /api/v2/sso/workspaces, POST /api/v2/sso/sync, GET /api/v2/sso/history ; CSRF exclut les préfixes /auth/saml et /auth/oidc (POST IdP cross-site)
  • Migration 23 — tables sso_config, sso_login_history, sso_requests
  • UI admin — onglet « SSO / Enterprise » dans Settings : statut, config SAML (Entity ID, SSO/SLO URL, certificat PEM, mapping attributs JSON, signature des AuthnRequests) et OIDC (issuer, client id/secret, scope), provisioning (auto-provision, SSO only, espace par défaut), tableau groupes → rôles, URL des métadonnées SP copiable, bouton « Re-sync des groupes », désactivation ; bouton SSO sur la page de login (nom dynamique, masqué si non configuré)
  • PermissionManager (design §7.2) — is_sso_only_workspace(), get_sso_roles(), sync_sso_permissions() (les grants SSO sont des lignes workspace_members explicites ; le fallback « viewer » implicite n'en est pas un)
  • Help — section « Enterprise SSO » + badge .help-badge.sso dans /help
  • .env (fallback bootstrap) — SSO_PROVIDER, SSO_NAME, SSO_ONLY, SSO_AUTO_PROVISION, SSO_ENTITY_ID, SSO_SSO_URL, SSO_SLO_URL, SSO_X509_CERTIFICATE, SSO_SIGN_REQUESTS, SSO_ISSUER_URL, SSO_CLIENT_ID, SSO_CLIENT_SECRET, SSO_SCOPE, SSO_ATTRIBUTE_MAPPING, SSO_GROUPS_MAPPING, SSO_DEFAULT_WORKSPACE_ID (la config admin prime sur l'environnement ; section ajoutée dans .env.example)
  • Dépendances — python3-saml==1.16.0, authlib==1.8.0, cryptography>=42.0

Changed

  • /logout délègue la déconnexion à l'IdP (SLO) quand la session vient de SAML, puis détruit la session locale ; sinon comportement existant
  • OpenAPI régénéré : docs/openapi-v2.json → 439 chemins, info.version = 6.7.0 (operation IDs uniques — les routes multi-méthodes sont enregistrées en deux routes mono-méthode)

Tests

  • tests/test_v67_sso.py : 38 tests — API de configuration admin (validation, CSRF, secret jamais renvoyé, secret vide préservé, réinitialisation, DELETE), métadonnées SP, login SAML complet contre un IdP mock signé (y compris SLO), OIDC complet (discovery/token/userinfo/JWKS mockés), cas négatifs (mauvais audience/issuer/destination/nonce/state, rejeu, signature altérée, assertion expirée), auto-provisioning, group mapping (nom → rôle workspace), sso_only, historique, rate limit, endpoint public providers, extension PermissionManager
  • pytest -n auto → 802 passed, 0 skipped · ruff check app tests OK · eslint static/js 0 problème

v6.6.0 (2026-09-24) — Agent phase 5 : API publique agent & skill marketplace

Dernière phase du plan agent en 5 phases (« Plateforme ») : l'agent devient pilotable par des intégrations tierces via /api/v2, et les skills deviennent partageables (export/import portable + galerie de presets installables).

Added

  • API publique agent — app/routers/api_v2_agent.py (15 routes, Bearer + scopes read/write, rate limit par token, idempotence, api_audit_log) :
    • GET/POST /api/v2/agents, GET/PUT/DELETE /api/v2/agents/{id}
    • GET/POST /api/v2/agents/conversations, GET/DELETE .../{id}, GET .../{id}/actions
    • POST /api/v2/agents/conversations/{id}/run — run synchrone JSON (le flux SSE reste interne) : {status, final, error, reasoning[], actions[], events[], duration_ms} (500 si failed)
    • POST /api/v2/agents/{id}/trigger — déclenchement externe d'un agent custom
    • propriété des conversations vérifiée par user_id → 404 pour un token tiers
  • Marketplace de skills — app/services/skill_gallery.py (source unique pour l'interne et le v2) :
    • GET /api/v2/skills/{id}/export → document portable {format: "flowdeck-skill", version: 1, skill{…}} (aucun id/workspace/auteur local) ; POST /api/v2/skills/import (409 sur collision, overwrite: true pour écraser)
    • GET /api/v2/skills/gallery + POST /api/v2/skills/gallery/{slug}/install — 6 presets : rapport hebdo, CR de réunion, base CRM, OKR, analyse repo Gitea, résumé de document
    • GET/POST/DELETE /api/v2/skills, GET /api/v2/skills/{id}, POST /api/v2/skills/{id}/apply
    • routes jumelles session : GET /api/agent/skills/gallery, POST .../gallery/{slug}/install, POST /api/agent/skills/import, GET /api/agent/skills/{id}/export, DELETE /api/agent/skills/{id} (le CRUD skills n'avait aucune suppression)
    • UI : section « Galerie » dans la palette / du panneau agent → installation + épinglage du chip
  • Webhooks de cycle de vie agent — agent.run.started et agent.run.failed émis (seul agent.run.finished l'était) via _fire_agent_webhook() ; les 3 sont déjà au catalogue → abonnement agent.*

Changed

  • Docs — docs/API_GUIDE_V6.md : nouveau §2.4 (tables d'endpoints + règles agent) ; nom d'événement corrigé agent.run.completed → agent.run.finished ; ROADMAP.md : 5 phases agent toutes cochées + section v6.6.0
  • OpenAPI — docs/openapi-v2.json régénéré : 427 chemins (was 402), info.version = 6.6.0
  • Version — 6.6.0 (VERSION + app/main.py)

Tests

  • tests/test_v66_agent_api.py : 15 tests (auth/scopes, CRUD agents, idempotence, run synchrone, ownership 404, trigger, export/import/validation, galerie + validité des outils des presets, routes internes, cycle de vie webhooks started→finished et started→failed)
  • pytest -n auto → 764 passed, 0 skipped · ruff check app tests OK · eslint static/js 0 problème

v6.5.1 (2026-09-24) — Tests webhooks_v2 complets + roadmap rattrapée

Les 7 tests d'intégration Webhooks v2 (stubs @pytest.mark.skip depuis v5.15.0) sont désormais réellement câblés, et le ROADMAP rattrapé sur ce qui avait été livré entre-temps.

Added

  • 7 tests d'intégration webhooks (tests/test_webhooks_v2.py) : retry échec→succès (journal retrying→retrying→delivered), échec après MAX_ATTEMPTS (4 POST + failed final), fire_event ignore les events inconnus, aucun abonné → aucun appel, fan-out wildcard (page.* reçoit, collection.created non), flow complet signé bout en bout (HMAC vérifié sur le body reçu + journal delivered), retry_due_deliveries (row retrying périmée → re-fire → delivered, original superseded)
  • Fixture db isolée — SQLite temporaire par test (env + singleton settings restaurés, safe -n auto), tables webhook créées par init_db() ; httpx.MockTransport injecté via stub module + RETRY_DELAYS=(0,0,0) (zéro sleep réel)

Changed

  • ROADMAP — case « migration sync.py vers Bearer » cochée (livrée v6.4.0 : Bearer-first + repli session PWA) ; ligne v5.15.0 mise à jour ; résumé final → « Reste: SSO/SAML » ; suite 749 verts, 0 skip

Tests

  • pytest -n auto → 749 passed, 0 skipped · ruff check app tests OK · eslint static/js 0 problème

v6.5.0 (2026-09-24) — Synced blocks production (databases & vues)

Les synced blocks passent en production et existent enfin dans les databases : chaque ligne de database gagne une page contenu (éditeur complet), la lecture résout la source côté serveur (zéro cache périmé) et la mise à jour d'une source réécrit réellement toutes les pages référentes.

Added

  • Page contenu par ligne de database — migration 22 (pages.collection_row_id INTEGER REFERENCES collection_pages(id) ON DELETE CASCADE + index) ; app/services/row_pages.py : ensure_row_page() (création lazy, réparation si trashée, workspace hérité de la page hôte, collection_id hérité pour les ACL) ; sync de titre bidirectionnelle ligne ↔ page
  • Endpoint GET /db/pages/{row_id}/open/api — renvoie le page_id de contenu d'une ligne (garde _require_view collection) ; le peek de toute vue DB (table/board/gallery/list/calendar) passe par là
  • Résolution serveur à chaque lecture — resolve_content_json() sur les 2 routes /pages/{id} (dashboard + board), GET /api/pages/{id}/content, GET /api/local-workspace/page-content/{id}, page publique /p/{slug}, GET /api/v2/pages/{id} et les rooms realtime (load_room)
  • Rendu public des synced blocks — branche synced dans _render_blocks_public (avant : JSON brut en <p>) + état « Deleted synced block »
  • Éditeur — état « Deleted synced block » quand la source a été supprimée (au lieu du cache fantôme / « Loading… » éternel)
  • API v2 — content_page_id sur GET /api/v2/pages/{id} pour une ligne (lecture sans création lazy)

Fixed

  • Propagation des synced blocks — PUT /api/synced-blocks/{id} réécrit le contenu stocké de chaque page référente via sync_synced_blocks_in_page() (fonction morte depuis v5.14.0), récursif dans les children (colonnes/toggles), broadcast WS seulement après réécriture DB
  • Suppression d'une source — ids des pages référentes collectés avant la cascade FK, _synced_deleted marqué dans le stocké (mark_synced_block_deleted), broadcast dédié manager._broadcast_synced_to()
  • Ouverture des lignes de database — le peek appelait /pages/{rowId} qui lit la table pages : avec des ids croisés il ouvrait une page au hasard (ligne id 5 « Ask AI » → page id 5 « Données ») ; désormais résolution via l'endpoint dédié
  • Listings — les pages contenu (parent_section='DbRow') sont exclues (AND collection_row_id IS NULL) de l'arbre sidebar, des racines workspace, de la liste des pages du board, des projets builtins, de l'arbre Library et du page_count des workspaces

Tests

  • tests/test_v65_synced_db.py — 17 tests ; suite complète 742 verts (pytest -n auto) ; ruff check app tests OK ; eslint static/js 0 problème

v6.4.0 (2026-09-22) — Realtime editing (production)

Le realtime passe en mode production : résolution de conflits au-delà du last-write-wins (merge à trois versions) et édition à grande échelle (broadcast non bloquant, coalescence des curseurs, corrections de fuites, observabilité).

Added

  • Merge à 3-voix (diff3-lite) — nouveau app/services/realtime_merge.py :
    • merge_text_3way(base, current, incoming) : fusion de caractères. Les régions modifiées qui ne se chevauchent pas sont toutes conservées (les deux saisies survivent) ; ordre d'arrivée indifférent ; un chevauchement réel retombe en LWW et est signalé.
    • merge_block_3way(base, current, incoming) : fusion champ-par-champ. Serveur et client modifiant des champs différents n'ont plus de conflit ; conflit limité au seul champ divergent (plus « tout le bloc perdu »).
    • Fonctions pures, testées sans WebSocket ni base de données.
  • Protocole base — chaque update envoyé par le client embarque la version du bloc dont dérive sa saisie ; le serveur calcule merge_block_3way(base, current, incoming). Sans base (ancien client) → LWW historique, rétro-compatible.
  • Adoption du résultat fusionné — l'ack renvoie le bloc fusionné + drapeau conflict ; le client met à jour sa base, adopte le résultat (hors bloc en cours d'édition) et affiche un toast en cas de conflit. Le serveur diffuse toujours le bloc final fusionné pour convergence de tous les pairs.
  • Broadcast non bloquant — chaque connexion a une file sortante (asyncio.Queue) + une tâche _writer dédiée ; _broadcast() fait put_nowait et n'attend plus le socket → un client lent ne fige plus la room entière.
  • Coalescence des curseurs — le writer réduit les messages sel empilés à la position la plus récente (seule la dernière compte), en préservant l'ordre des messages importants.
  • Anti-flood — budget d'opérations par connexion (OP_WINDOW_MAX=400 / fenêtre 10 s) ; au-delà, réponse ack stale sans application.
  • Observabilité — GET /api/realtime/stats (authentifié) : rooms, connexions (dont totales), ops, merges, conflits, déconnexions lentes, + détail par page.

Changed

  • Déconnexion des clients trop lents — file sortante pleine (MAX_OUT_QUEUE=512) → fermeture 4413 + compteur slow_disconnects, pour qu'une room ne stagne jamais sur un pair mortel.
  • app/templates/_page_editor_realtime.html — les updates incluent désormais base ; nouveau traitement de l'ack avec merged.

Fixed

  • Fuite de rooms — une tentative de connexion WS sur une page inexistante (4404) enregistrait une Room orpheline en mémoire pour toujours ; la room n'est plus enregistrée tant que la page n'est pas validée.
  • room_state() sur page inexistante — retournait/plantait sur None ; retourne désormais {"blocks": [], "title": "", "version": 0} sans laisser d'entrée dans _rooms.

Tests

  • 26 nouveaux tests tests/test_realtime_v64.py : merge purs (régions disjointes, ordre indépendant, chevauchement → conflit, suppression de champ, id préservé), protocole WS (ack fusionné, convergence de 2 clients sur le même bloc, rétro-compat LWW sans base, conflit scalaire), fuite 4404, coalescence des curseurs, endpoint stats, anti-flood.
  • 14 tests tests/test_realtime.py existants préservés (non-régression).
  • ruff check app tests OK · eslint static/js 0 problème.

v6.3.0 (2026-09-21) — API publique complète v2 (REST + scopes + OpenAPI)

L'API publique /api/v2 devient une surface REST complète façon Notion : CRUD sur tous les domaines (collections, pages, propriétés, vues, commentaires, notifications, favoris, tags, partage, sprints, templates, forges, admin…), auth Bearer + scopes hiérarchiques, pagination/filtres/tri, erreurs RFC 7807, idempotence, audit et webhooks. Wrappers sur les services existants — un seul chemin de code.

Migrations (v20)

  • api_tokens : colonnes scopes TEXT DEFAULT 'read,write' et expires_at TIMESTAMP (ALTER idempotents, rétro-compatibles : anciens tokens → read,write).
  • Nouvelles tables api_audit_log (user, token, action, ressource, IP), webhook_deliveries (statut, http_code, durée, payload) et idempotency_keys (clé → réponse rejouée).

Auth, scopes & conventions (app/services/api_v2_helpers.py)

  • Bearer unifié resolve_bearer_token() / get_bearer_user() : hash sha256 sur api_tokens, support extension_devices (clipper) et legacy user_tokens, vérification revoked + expires_at, mise à jour last_used_at.
  • Scopes hiérarchiques read < write < admin (un scope supérieur satisfait un besoin inférieur), dépendance require_scope() → 403 explicite ; validate_scopes_input() → 400 sur scope inconnu.
  • Pagination parse_pagination() (défaut 30 / max 100) + header X-Total-Count ; ISO-8601 UTC via to_iso8601() ; parsing des colonnes *_json.
  • Erreurs RFC 7807 application/problem+json (type/title/status/detail/instance) pour tout /api/v2, via le handler unifié sur StarletteHTTPException (app/main.py).
  • Idempotence Idempotency-Key sur les POST de création (check_idempotency / store_idempotency).
  • Audit audit_log() sur toutes les mutations v2 (api_audit_log).
  • Rate limit par token check_v2_rate_limit() (300 req/min, configurable API_V2_RATE_LIMIT_PER_TOKEN).
  • public_api_insecure_ok : le token de dev fd-public-key n'est accepté que si PUBLIC_API_INSECURE_OK=true (dev/test) — refusé par défaut en production.

Router app/routers/api_v2.py (prefix /api/v2, ~100 endpoints)

  • Tokens : POST /tokens (name, scopes, expires_at, montré une fois), GET /tokens (prefix only, jamais le hash), DELETE /tokens/{id}, POST /tokens/{id}/rotate.
  • Users : GET/PATCH /users/me, GET /users/search, préférences.
  • Workspaces & membres : CRUD + GET/POST/PATCH/DELETE .../members.
  • Collections : CRUD, /linked, /task, /sources.
  • Pages : GET /collections/{id}/pages (filtres filter[prop], sort, fields, query), CRUD, /restore, /move, /sub-items, /dependencies.
  • Propriétés : CRUD, /relation, evaluate-formula, compute-rollup.
  • Vues & dashboards : CRUD + save-as.
  • Comments / mentions / notifications / favoris / tags / recents.
  • Partage & publication : pages/{id}/shares, publish / dé-publish (/p/<slug>).
  • Historique (page_history + page_versions) + restore.
  • Sprints (CRUD, assign, burndown), templates (page + database, apply), export/import (MD/HTML/PDF, CSV).
  • Forges : GET /projects, /projects/{owner}/{repo}/tree.
  • Recherche FTS5 (GET /search, repli LIKE).
  • Admin : users (list/patch/delete) + GET /admin/audit-logs.
  • Webhooks : CRUD subscriptions + POST /{id}/test + GET /{id}/deliveries (CRUD simple d'abord).

OpenAPI & config

  • docs_url="/docs" + redoc_url="/redoc" activés ; docs/openapi-v2.json généré (402 chemins).
  • .env.example : PUBLIC_API_INSECURE_OK=false, API_V2_RATE_LIMIT_PER_TOKEN=300.
  • VERSION et app/main.py passés à 6.3.0.

Tests & robustesse

  • tests/test_public_api_v2.py — 24 tests (auth 401, token lifecycle, scopes read/write/admin, pagination + X-Total-Count, CRUD collections/pages/propriétés/vues, filtres, RFC7807, idempotence, search, notifications, tags, sharing, webhooks, workspaces, sprints, templates, admin).
  • tests/conftest.py + fixtures locales : PUBLIC_API_INSECURE_OK=true (le token de dev reste testable sans impacter la prod).
  • Suite complète 668 verte (pytest -n auto) ; ruff check app tests OK.

v6.2.1 (2026-09-20) — Web Clipper : polish & fix bloc bookmark

Patch UX et correctif bloc bookmark pour l'extension v6.2.0.

  • Extension — bouton flottant — rond transparent draggable (évite de masquer le contenu), toggle d'affichage persistant (chrome.storage), clic → popup de capture ; position restaurée au reload.
  • Refresh auto sidebar — après POST /api/v2/web-clipper/clip le workspace sidebar est rafraîchi sans reload (polling + event clipper:clipped) pour que la page clippée apparaisse immédiatement.
  • Bloc bookmark — le service create_page_from_clip() renvoie désormais un bloc bookmark fidèle au payload OG (URL d'origine + embed_src résolu via embeds.py), rendu correct en éditeur / page publique /p/<slug> / exports MD/HTML/PDF (préserve favicon + description).
  • Fix éditeur 7f998fa — SyntaxError duplicate inner dans _page_editor_scripts.html (variable inner redéclarée lors du drag & drop multi-sélection) corrigé ; autosave + WS realtime non bloqués.
  • Tests & build — flowdeck-clipper.zip régénéré (static/extension/), ruff/eslint verts, 16 tests test_web_clipper.py verts.

v6.2.0 (2026-09-19) — Web Clipper : extension navigateur (capture article/selection/bookmark/screenshot)

Capturez n'importe quelle page web en une page FlowDeck : article complet, sélection, bookmark ou screenshot — depuis une extension Manifest V3 (Chrome/Edge/Firefox) + API directe POST /api/v2/web-clipper/clip.

Extension navigateur (extension/ + static/extension/)

  • Manifest V3 — manifest.json (permissions activeTab, storage, scripting, contextMenus, host_permissions <all_urls>), background.js (service worker OAuth + clip → fetch Bearer), content.js (bouton flottant + menu contextuel sélection + Ctrl+Shift+C), popup.html/popup.js (sélection workspace, type de capture), clipper.css, icônes 16/32/48/128, bundle flowdeck-clipper.zip servi à /static/extension/.
  • Types de capture — article (HTML complet → sanitize_html + html_to_blocks), selection (sélection HTML → Markdown), bookmark (URL + OG → carte bookmark v5.5.0), screenshot (base64 → upload image + page). Cap 10 MB / 200 blocs, garde SSRF inexistante (validation URL), sanitisation HTML côté serveur.

Serveur (app/routers/web_clipper.py + app/services/web_clipper.py)

  • Endpoints prefix /api/v2/web-clipper — POST /clip (crée page via create_page_from_clip() + log_clip()), GET /status (auth + compteurs devices/clips), POST /auth/verify (enregistre device register_device() → token fd_… montré une fois), GET /devices, DELETE /devices/{id} ; page HTML GET /extensions (téléchargement + liste devices/clips).
  • Auth triple — session cookie flowdeck_session OU Bearer api_tokens (hash sha256) OU Bearer extension_devices OU legacy user_tokens (_user_from_request()), rate-limit 50 clips/heure/device (_check_rate_limit, 429), payload normalisé (url, title, content, content_type, selection_html, image_base64, tags, target_workspace_id).
  • Tables migration 19 — extension_devices (user_id, extension_name, device_id, device_name, token_hash, scopes, last_used_at, revoked, UNIQUE(user_id,extension_name,device_id)) + extension_clips (user_id, device_id, clip_type, source_url, target_page_id, target_workspace_id, title) avec index idx_ext_*.
  • Settings UI — onglet Extensions dans app/templates/settings.html (devices, clips count, revoke, token copy).

Tests & wiring

  • tests/test_web_clipper.py — 16 tests (sanitize, blocks, article/bookmark/selection/screenshot, Bearer, rate-limit, devices, /extensions).
  • Wiring app/main.py:50,158 — web_clipper_api_router + web_clipper_router inclus ; VERSION bump 6.1.0 → 6.2.0.

v6.1.0 (2026-09-19) — Granular Permissions : page / collection / property ACL + groupes + audit

Permissions fines héritables : chaque page / database / propriété peut être restreinte à des utilisateurs ou groupes explicites. L'héritage suit la chaîne page → collection → workspace (moindre privilège), avec bypass owner/admin et audit complet.

Base de données (migration 18)

  • Tables user_groups (workspace-scoped, UNIQUE(name)), group_members (N-ary), page_permissions, collection_permissions, property_permissions (user_id XOR group_id, CHECK, UNIQUE), permission_audit_log.
  • Colonnes pages.permission_type, collections.permission_type, collection_pages.permission_type (inherit|restricted|private, défaut inherit).
  • Index idx_pp_page, idx_pp_user, idx_cp_collection, idx_propp_prop, idx_perm_audit_res, idx_gm_*.

PermissionManager (app/services/permission_manager.py)

  • Résolution héritage + moindre privilège : grant explicite sur page > grant collection > rôle workspace ; restricted/private sans grant → 404.
  • Méthodes page : get_page_permission(), can_view/edit/comment/manag ; collection : get_collection_permission(), can_view/edit/manag ; propriété : can_view/edit_property(), get_visible_properties().
  • Groupes : create_group(), add/remove_user_from_group(), get_groups_for_workspace(), get_group_members(), is_workspace_admin(), user_group_ids().
  • Helpers : _explicit_grant_role() (best rank user+groups), _collection_role(), _property_*, _owns_workspace(), _is_admin().
  • Cache 60 s (_cached()) + invalidate() appelé après chaque grant/revoke/type_change.
  • log_permission_change() (audit, never-throw).

API (app/routers/permissions.py — prefix /api/v2)

  • Pages : GET /pages/{id}/permissions (grants + mine + type + can_manage), GET /pages/{id}/permissions/mine, POST /pages/{id}/permissions (user_id|group_id + role viewer/commenter/editor/owner), POST /pages/{id}/permissions/batch, DELETE /pages/{id}/permissions/{perm_id}, POST /pages/{id}/permission-type.
  • Collections : GET /collections/{id}/permissions, POST /collections/{id}/permissions, DELETE /collections/{id}/permissions/{perm_id}, POST /collections/{id}/permission-type, GET /collections/{id}/properties/visible (split visible/hidden).
  • Properties : GET /collections/{cid}/properties/{pid}/permissions, POST …/permissions (role viewer|editor), DELETE …/permissions/{perm_id} (extra_cols collection_id).
  • Groupes : GET /groups?workspace_id, POST /groups, PUT /groups/{id}, DELETE /groups/{id}, GET/POST /groups/{id}/members, DELETE /groups/{id}/members/{uid} (workspace owner/admin only).
  • Access pickers + audit : GET /users?workspace_id&q, GET /audit/permissions?limit (owner/admin, 500 max).
  • Validation : 401 sans session, 403 sans can_manage, 400 rôle invalide / cible manquante, 404 user/group/property inconnu.

Guards existants

  • board.py : GET /board/api/pages/{id} (404 si !can_view_page), PUT /board/api/pages/{id} (404 si non-view, 403 si viewer/commenter, 423 si locked reste prioritaire).
  • collections.py : DELETE /db/api/{id} (404 si !can_view_collection, 403 si !can_manage), GET /db/pages/{id}/api (404), PUT/DELETE /db/pages/{id}/api (403), GET /db/{id}/properties/api filtré par visible, _require_view/_require_edit helpers avec _session_user() (pas de fallback admin sur null session).

UI (backend-ready)

  • Endpoints prêts pour panneau Permissions de l'éditeur (icône 🔒), panneau collection, masquage colonnes via visible/hidden, et gestion des groupes en Settings. Les grants sont consommés par can_view_property / get_visible_properties côté vues.

Tests

  • tests/test_v60_granular_permissions.py — 21 tests : inherit allow + edit 403, restricted hidden (owner 200 / member 404), grant viewer unlock + viewer cannot edit, editor can edit but not manage, revoke 404, private + admin override, mine + batch, permission-type via API, restricted collection hidden (API 404 + HTML 302), collection viewer 403 create/delete, owner delete, collection editor create pages, property visibility, property hidden, property grant 403, group grant inherits + listing shows group, group removal revokes, groups listing/members + non-owner 403, audit log (grant+type_change, member 403), endpoints 401, validation 404/400. Suite 21/21 verte.

v6.0.0 (2026-09-18) — PWA : Progressive Web App, offline support

FlowDeck devient une PWA installable et utilisable hors ligne : le shell est mis en cache par un service worker, les pages visitées restent disponibles, et les modifications faites sans réseau sont mises en file d'attente dans IndexedDB puis synchronisées au retour de la connexion, avec résolution de conflits côté serveur.

PWA & Service Worker

  • static/manifest.json + static/icons/* (72→512 + maskable + apple-touch) générés par scripts/generate_pwa_icons.py.
  • static/sw.js — precache du shell (CSS/JS/fonts/icônes), stratégies cache-first (assets) / network-first (HTML, API GET), page offline inline, Background Sync (sync-flowdeck), purge des anciens caches.
  • base.html / landing.html — meta PWA (manifest, theme-color, apple-mobile-web-app) + enregistrement du service worker. Routes GET /manifest.json et GET /sw.js servies par FastAPI.

Offline client (static/js/offline.js)

  • Base IndexedDB flowdeck-offline : pages_offline, collections_offline, sync_queue, sync_meta.
  • window.FlowOffline : enqueue, flush, delta, status, savePageOffline, getQueue, pendingCount, onChange, markDirtySidebar.
  • Sauvegarde optimiste hors ligne (hook de l'éditeur), file de mutations rejouée automatiquement (online, Background Sync, polling 30 s).
  • Durcissement : max 100 mutations/batch, timeout 30 s (AbortController), rétention queue 30 jours.

Serveur de synchronisation

  • app/services/sync_engine.py — get_delta (pull des changements, détection des soft-deletes), apply_batch (mutations optimistes + contrôle de version sync_version).
  • app/routers/sync.py — GET /api/v2/sync/delta, POST /api/v2/sync/batch, GET /api/v2/sync/status (auth session, CSRF-exempt via /api/v2).
  • Migrations — table offline_sync_queue (+ index) et colonnes sync_version sur pages, collection_pages, collections avec triggers AFTER UPDATE.
  • Conflits — edit-edit (last-write-wins + rapport), edit-delete (page orpheline recréée), create-create (renommage « copie offline »).

UI

  • Banner hors ligne avec compteur de modifications en attente, badge de synchronisation (spinner), toasts de fin de sync, icône ⟳ sur les pages non synchronisées.
  • Section /help « Offline mode (PWA) ».

Tests

  • tests/test_sync.py, tests/test_sync_migrations.py, tests/test_service_worker.py, tests/test_pwa_offline.py ; E2E Playwright e2e/pwa_offline.spec.js (manifest, SW, queue offline, replay, navigation hors ligne).

Deux versions roadmap livrées ensemble : le graphe de connaissances Notion (liens [[, mentions de page et de date, renommage propagé) et les options de page (galerie de templates globale, verrouillage lecture-seule, pleine largeur, texte compact).

  • Service app/services/wiki_links.py — tokens stockés dans le texte des blocs : [[fdpage:ID]] (lien interne résolu à l'affichage) et [[fddate:YYYY-MM-DD]] (chip de date). Le token ne contient que l'id → renommage propagé automatiquement.
  • Endpoints — GET /board/api/wiki/pages?q= (picker, recherche substring + sous-séquence floue « mnt » → « Meeting notes ») et GET /board/api/wiki/titles?ids= (labels courants en batch, icônes incluses, « Deleted page » si supprimée).
  • Éditeur — taper [[ ouvre le picker de pages (recherche live, navigation clavier ↑↓/Entrée/Échap, sélection à la souris), Enter insère une chip de page atomique (icône + titre, clique = navigate). Le menu @ existant gagne deux sections : Pages (@Nom → lien inline) et Date (@today, @tomorrow, @YYYY-MM-DD → chips). Les chips sont relu en tokens bruts (gtTok) à chaque sync : le drag, le split Entrée, l'undo/redo et l'autosave préservent les liens.
  • Backlinks — le scanner v5.4.0 reconnaît désormais les tokens [[fdpage:ID]] : « Lié depuis » fonctionne avec les wiki-links.
  • Page publique — _render_blocks_public résout les chips (liens absolus + dates lisibles, HTML échappé).

v5.12.0 — Templates & verrouillage de page

  • Template picker global — « + New page » (sidebar, footer, palette Ctrl+K) ouvre la galerie : 5 templates built-in (app/services/ block_templates.py : Empty, Meeting notes, Weekly report, To-do list, Project doc) + les templates personnels sauvegardés ; « Empty » retombe sur la création classique (workspace item).
  • Save as template — menu « … » de la page → capture le contenu blocs courant comme template personnel (page_global_templates, visibles uniquement par leur créateur, created_by IS NULL = partagé).
  • Bouton « Use template » — POST /board/api/page-templates/{id}/use (id 0 = built-in par clé) duplique le contenu des blocs dans une nouvelle page du workspace de l'utilisateur et renvoie son id.
  • Page lock — menu « … » → « 🔒 Lock page » : bannière sticky « locked (read-only) », blocs et titre passés contenteditable=false, handles/actions masquées. Serveur : POST /board/api/pages/{id}/lock, garde _ensure_page_editable → 423 sur PUT /api/pages/{id} et POST /api/pages/{id}/blocks pour tout non-privileged ; déverrouillage réservé à la personne qui a verrouillé (locked_by) ou un admin (403 sinon).
  • Full-width / Small text — toggles dans le menu « … », persistés par page (pages.full_width, pages.font_small), options POST /board/api/pages/{id}/options, rendu via classes CSS (.full-width / .small-text).
  • Migrations 13 — pages.is_locked, pages.locked_by, pages.full_width, pages.font_small, table page_global_templates.
  • Tests — tests/test_v511_v512_wiki_templates.py : 15 tests (schéma, helpers tokens + échappement, picker flou, renommage propagé, persistance des tokens + backlinks, chips en page publiée, lock 423 / 403 / admin / ré-ouverture, options, templates built-in + perso + cloisonnement, shapes service, câblage front). Suite complète 538 verte, ruff check OK.
  • Version — 5.12.0.

v5.11.7 (2026-09-13) — v5.8.0 Calendrier & Rappels

Calendrier complet (jour / semaine / mois) avec récurrences expandues côté serveur, rappels avant échéance (in-app + email), fuseaux horaires par utilisateur et par événement, notifications d'assignation et centre de notifications enrichi.

  • Moteur de récurrence — app/services/recurrence.py : sous-ensemble RRULE (daily/weekly/monthly, intervalle, count, until, byweekday lundi=0, timezone IANA) ; expansion virtuelle par fenêtre, jamais persistée, bornée (garde anti-explosion). validate_rule() pour le 400 en écriture.
  • Vues Jour / Semaine / Mois — la vue calendar de _database_table_scripts.html gagne un sélecteur de mode (persisté dans viewConfig.calendar_mode), navigation ‹/› adaptée au mode, vue Jour en agenda trié par heure, Semaine en 7 colonnes. Les événements viennent de GET /db/{id}/calendar/api (expansion serveur des occurrences).
  • Récurrences dans l'UI — double-clic sur un événement : popover Time / Timezone / Repeat (Every + Ends never|after count|on date) / Remind. Règle stockée dans property_values_json sous __recurrence__ (clé méta par id de propriété date), badge ↻ sur les chips et cellules date.
  • Rappels — app/services/reminders.py : scan_and_fire(now) déterministe (lead minutes/heures/jours, occurrence suivante timezone-aware), reminder_scheduler() toutes les 60 s ; dédup via la table reminder_log (page_id, occurrence_date) ; cible = personnes assignées, repli premier admin ; notifie en in-app + email (pref reminders). Config reminders_enabled / reminder_scan_interval_seconds.
  • Fuseaux horaires — colonne users.timezone ; réglable dans Settings → Notifications (picker IANA via GET/POST /api/notifications/timezone, liste GET /db/timezones/api) ; priorité de résolution par ligne : __timezone__ de l'événement → règle de récurrence → timezone de l'utilisateur.
  • Notifications d'assignation — notify_assignment() compare les propriétés person avant/après sur PUT /db/pages/{id}/api : les nouvellement assignés reçoivent une notif in-app + email (pref assignments). Préférences étendues : comments, mentions, reminders, assignments.
  • Template Meeting notes — propriétés Agenda et Notes ajoutées (migration 12 ; ne touche pas une version personnalisée du template).
  • Migrations 11 & 12 — reminder_log, users.timezone, enrichissement du template seed.
  • Tests — tests/test_v58_calendar_reminders.py : 20 tests (moteur pur, schéma, validation des méta-clés 400, calendar/api, timezone user, scanner avec horloge injectée + dédup + récurrence + cible assignée, notifications d'assignation, prefs, câblage front). Suite complète 523 verte, ruff check OK.
  • Version — 5.11.7.

v5.11.6 (2026-09-13) — v5.7.0 Database Avancée (Pt. 2)

Complète la parité Notion des bases de données : personnes et propriétés automatiques, groupes de propriétés, vues sauvegardées par utilisateur, swimlanes Kanban + WIP limits, cartes configurables, calendar avec glisser-déposer et gallery avec couvertures.

  • Types propriété — person, created_time, created_by, last_edited_time, last_edited_by câblés de bout en bout. apply_auto_properties() (property_types) remplit/rafraîchit les valeurs automatiques à la création et à la mise à jour des lignes ; created_* est figé, last_edited_* est rafraîchi. Nouvel endpoint GET /db/{collection_id}/members/api (membres du workspace, repli tous utilisateurs) alimente le sélecteur person (chips + avatars).
  • Groupes de propriétés — colonne collection_properties.group_name, en-tête de table avec sections pliables (db-group-row + chevron) et POST /db/{collection_id}/property-groups/api pour réassigner les colonnes.
  • Vues sauvegardées par utilisateur — collection_views.created_by ; GET /db/{id}/views/api ne renvoie que les vues de l'utilisateur (les vues partagées NULL restent visibles), save-as/duplicate attribuent le propriétaire, DELETE /db/views/{id}/api et renommage.
  • Swimlanes Kanban — sub_group_by : une rangée de colonnes par lane (2e dimension de groupement).
  • WIP limits — wip_limits par colonne, colonne surlignée + compteur WIP n/limit au dépassement.
  • Cartes configurables — card_properties (propriétés affichées), card_size (compact/détaillé) et couverture cover_mode (none/icon/color/property via cover_property).
  • Calendar drag & drop — vue mois (date_property configurable), navigation ‹/›/Today, glisser une carte sur un jour = reschedule immédiat.
  • Gallery avec couvertures — vignettes image (propriété files/url), icône, couleur ou cover de ligne ; tailles small/medium/large.
  • Composant multi-vues — _database_table_scripts.html réécrit : barre de vues (ajout/suppression/duplication/paramètres), vues table/board/calendar/gallery/list, éditeurs de cellules par type (select/status/date/number/checkbox/multi_select/person), cellules read-only pour les types automatiques, popover de configuration.
  • Correctif — PUT /db/pages/{id}/api fusionne désormais les valeurs soumises avec l'existant (PATCH partiel) au lieu de remplacer toute la ligne ; cover_url par ligne (collection_pages.cover_url).
  • Migration 10 — collection_properties.group_name, collection_views.created_by/updated_at, collection_pages.cover_url.
  • Tests — tests/test_v57_db_advanced.py : 12 tests. Suite complète 503 verte, ruff check OK.
  • Version — 5.11.6.

v5.11.5 (2026-09-13) — v5.6.0 Import de données (Phase 5 — durcissement)

Finalise la solution d'import : ré-import incrémental, lots, URL, dépôt forge, relations Notion et rapports exportables. v5.6.0 est complet (Phases 0→5).

  • Import incrémental / re-sync — nouveau paramètre mode (skip par défaut, update = mise à jour des pages et upsert des lignes par titre, duplicate). Compteurs pages_updated / rows_updated dans le rapport.
  • Erreurs partielles — l'import continue page par page en cas d'échec (status: "partial", liste errors), au lieu d'abandonner.
  • Import de dépôt forge — POST /api/import/forge-repo : arborescence Gitea/GitHub → pages (dossiers conservés, fichiers texte, code en blocs ; GitHubAdapter.list_repo_files, GiteaForgeAdapter.list_repo_files).
  • Web clipper — POST /api/import/url : fetch d'une page (garde SSRF : loopback/privé/link-local refusés), conversion HTML → blocs + carte bookmark (OG metadata).
  • Lot multi-fichiers — POST /api/import/run-batch (rapport par fichier) et file d'attente dans l'assistant /import (sélection multiple, statut par fichier, barre de progression).
  • Relations Notion — POST /api/import/relations/resolve : les colonnes texte qui référencent les titres d'une autre collection deviennent des propriétés relation (valeurs = ids de collection_pages). Auto-exécuté après un import Notion (clé relations du rapport).
  • Rapport exportable — GET /api/import/jobs/{id}/report (JSON) + bouton « Télécharger le rapport » dans l'UI.
  • Correctif — les lignes importées stockent désormais leurs valeurs par id de propriété (au lieu du nom) : rendu correct dans les vues database et compatibilité avec les relations/rollups.
  • UI — assistant /import enrichi : multi-fichiers, mode de ré-import, import URL, import forge (issues et fichiers), téléchargement du rapport.
  • Tests — tests/test_v56_import.py porté à 43 tests (incrémental ×4, lot, URL ×2, forge-repo ×2, relations, rapport de job). Suite complète 491 verte, ruff check OK.
  • Version — 5.11.5.

v5.11.4 (2026-09-13) — v5.6.0 Import de données (Phases 3 & 4)

Complète le chantier v5.6.0 : documents/bureautique (Phase 3) et signets/dev/divers (Phase 4). v5.6.0 est désormais complet (5 phases).

  • Phase 3 — Documents & bureautique
    • Word .docx (python-docx) — titres, listes, citations, tableaux GFM et images inline → page ; couvre aussi les exports Google Docs Takeout.
    • PDF (pypdf) — extraction texte par page + images → page ; avertissement si pages sans couche texte (scan).
    • HTML fichiers/dossier — détection élargie (archive contenant ≥ 1 HTML), assets collectés comme pièces jointes.
  • Phase 4 — Signets, dev & divers
    • Forge issues — POST /api/import/forge (Gitea/GitHub) : issues → collection (Number, State, Labels, Milestone, Assignee, dates, URL, Body) + collections Labels et Milestones. GitHubAdapter.list_issues/list_labels/list_milestones ajoutés ; token par utilisateur (user_oauth_tokens).
    • Signets — Raindrop.io, Pocket (CSV/HTML), Readwise (highlights CSV/MD), Shaarli (JSON) et signets Netscape HTML → collection (URL, description, tags, date).
    • Calendrier .ics — parseur RFC 5545 (VEVENT) → collection d'événements.
    • OPML — flux RSS/outlines → collection (URL, dossier).
    • Standard Notes — sauvegarde JSON → pages (notes chiffrées ignorées).
  • UI — l'assistant /import liste désormais les 20 sources, accepte .docx/.pdf/.ics/.opml et propose un panneau « import depuis une forge ».
  • Dépendances — python-docx, pypdf ajoutés.
  • Tests — tests/test_v56_import.py étendu à 32 tests (signets ×5, ics/opml/standard notes, docx, pdf, forge ×4). Suite complète verte, ruff check OK.
  • Version — 5.11.4.

v5.11.3 (2026-09-13) — v5.6.0 Import de données (Phases 0, 1, 2)

Démarre le chantier v5.6.0 « Import de données » avec le socle unifié (Phase 0) et les deux premières familles de sources : Notes & Markdown (Phase 1) et Données & tableaux (Phase 2).

  • Socle d'import unifié (Phase 0) — package app/services/importers/ : modèle normalisé (ImportResult, ImportPage, ImportAttachment), interface Importer (detect() / parse()) + registre auto-détection, pipeline commun (hiérarchie parent_id, pièces jointes, réécriture des liens, collections + lignes, rapport d'import), jobs d'arrière-plan (app/services/importers/jobs.py, polling), déduplication idempotente (table import_items), migration 9 (import_items + import_jobs).
  • Assistant UI d'import — page /import (template import.html) : choix de source ou détection auto, glisser-déposer, aperçu dry-run, mapping colonnes → types (override des types inférés), barre de progression, rapport. Lien « Assistant d'import avancé » ajouté à la modale Import de l'éditeur.
  • API — GET /api/import/sources, POST /api/import/preview, POST /api/import/run (sync ou async=true), GET /api/import/jobs[/{id}].
  • Phase 1 — Notes & Markdown : Obsidian (vault .zip, frontmatter YAML, [[wikilinks]], ![[embeds]], pièces jointes, hiérarchie de dossiers), Notion (export .zip, hiérarchie complète, databases .csv → collections, images), Logseq & Roam (outliner, propriétés key:: value, pages journal, {{[[TODO]]}}), HTML (Apple Notes, Bear, Ulysses, OneNote), Google Keep (Takeout JSON), plus l'import Markdown/.zip générique.
  • Phase 2 — Données & tableaux : CSV/TSV typé (inférence texte/nombre/date/checkbox/email/url/select/multi_select + mapping manuel), Excel .xlsx (une collection par feuille, openpyxl), JSON générique (tableau d'objets → collection).
  • Correctif — _md_to_blocks reconnaît désormais les cases à cocher - [ ]/- [x] (elles étaient absorbées par la branche puces à puces).
  • Dépendances — openpyxl, beautifulsoup4, PyYAML ajoutés.
  • Tests — tests/test_v56_import.py : 18 tests (registre, dry-run, CSV typé, dédup, TSV, Excel multi-feuilles, JSON, Obsidian, Notion, Logseq, Roam, HTML, Google Keep, mapping, wizard, job async). Suite complète 463+ verte, ruff check OK.

v5.11.2 (2026-09-12) — v5.5.0 Embeds & Média riche (complétion)

Valide et complète le chantier v5.5.0 (parité embeds Notion). Une grande partie était ébauchée ; ce patch répare les chemins d'API cassés côté éditeur, finalise la résolution d'embed, le parsing OG, la lightbox publique et couvre le tout de 47 tests dédiés.

  • Embed universel — app/services/embeds.py réécrit : détection sous-domaine correcte, YouTube (watch/shorts/youtu.be), Vimeo, Figma, Google Maps, Google Docs/Sheets/Slides, Loom, CodePen, Miro, Spotify, SoundCloud, Twitch (parent réel), X/Twitter, Pinterest, Microsoft Office ; helpers resolve_embed(), inline_kind(), provider(). Nouvel endpoint POST /board/api/embed/resolve.
  • Résolution d'embed dans l'éditeur — la saisie d'une URL appelle le résolveur et met en cache embed_src/embed_provider (l'URL d'origine est conservée) ; le rendu (éditeur, pages publiques, exports MD/HTML/PDF) préfère embed_src. Le champ est persisté via la sérialisation des blocs.
  • Bookmark cards — app/services/og_fetcher.py : parseur <meta> robuste (ordre d'attributs libre), titre de repli, favicon, transport injectable pour les tests ; fetch_og_metadata ne lève jamais sur erreur réseau.
  • Image lightbox — navigation clavier ←/→ et Escape, multi-images, dans l'éditeur et les pages publiques /p/<slug> (CSS .fd-lightbox).
  • Fix chemins API (bug v5.4.0/v5.5.0) — l'éditeur appelait /api/pages/... alors que les routes sont /board/api/pages/... : cover, icon, versions, backlinks, import, move et OG metadata étaient donc en 404. Tous corrigés, avec un test de régression statique.
  • Tests — tests/test_v55.py : 47 tests (embeds par provider, resolve API, parse OG + MockTransport, lightbox, préviews, cover upload/URL/suppression, icône, page publique). Suite complète 444 verte, ruff check OK.
  • Version — 5.11.2.

v5.11.1 (2026-09-11) — v5.2.0 Infrastructure & Polish (complétion)

Finalise le chantier v5.2.0 : la plupart des briques étaient déjà livrées (design tokens/components, API tokens, sessions, onboarding, backups, projets + sync, GitHubAdapter, Docker multi-stage) ; ce patch sécurise l'isolation des tests, active les tests parallèles et rend le linting vert.

  • Tests parallèles (pytest-xdist) — tests/conftest.py mute désormais le singleton app.config.settings au lieu de le remplacer. Les modules qui importaient settings au chargement (ex. app/services/backup.py) gardaient sinon les valeurs par défaut, ce qui rendait les tests backup instables. Chaque test a une base SQLite temporaire + un dossier de backup dédiés → tests parallèles lancés explicitement (pytest -n auto en local et en CI). La config de base ne force plus -n (évite l'échec unrecognized arguments: -n si pytest-xdist n'est pas installé).
  • Backups réellement testés — les 2 tests précédemment skip (flaky) sont remplacés par des tests réels : snapshot daté, prune_old_backups, backup_due et API admin /api/settings/backups.
  • OAuth (mock) — tests d'intégration complets dans tests/test_v52_infra.py : redirect authorize → callback (Gitea + GitHub), mode link sur un compte local, rejet d'un state invalide, construction des URLs d'autorisation.
  • Schéma complet hors lifespan — init_db() crée aussi webhook_subscriptions (auparavant uniquement dans le lifespan FastAPI).
  • Linting — ruff check app tests passe sans erreur (corrections E701/E702, B904, B007, E741, F821, F841, W293, UP031, E731 + config FastAPI pour B008) ; migration de .eslintrc.json vers eslint.config.mjs (flat config, ESLint v9+).
  • CI — nouveau job lint (ruff + eslint) ; tests exécutés en parallèle avec couverture ; déclencheurs élargis à develop.
  • Version — 5.11.1. 397 tests verts.

v5.11.0 (2026-09-11) — FlowDeck Agent : UX chat améliorée

Quatre améliorations majeures du panneau Agent (v4.10.0 → v4.14.0) pour une expérience plus proche de Notion AI.

  • Auto-titre à chaque requête — Le titre de la conversation se met à jour après chaque échange avec l'IA (pas seulement à la création). Il reflète le sujet courant et rend l'historique facile à parcourir.
  • Aperçu au survol / appui long — Dans l'onglet Historique, un survol souris (desktop) ou un appui long (mobile) ouvre un pop-up avec le premier message utilisateur et la dernière réponse de l'agent, sans quitter la liste.
  • Normalisation des blocs write_blocks — L'outil tolère désormais les erreurs fréquentes du LLM : text→content, heading→heading_1…heading_4, bullet/list→bulleted_list, génération d'ID, types inconnus→paragraphe. Le script PowerShell (ou tout code) s'écrit correctement dans un bloc code.
  • Accordéon style Notion — Le raisonnement de l'agent s'affiche dans un composant pliable : « Pensées » (chevron, contenu complet au clic) et « Actions » (lignes simples, passé, non-cliquables). Le tout reste visible en permanence sous la réponse, sans encombrer l'écran.
  • Tests — test_write_blocks_normalizes_llm_blocks, test_engine_titles_conversation_on_each_request, test_suggest_title_strips_composer_prefixes.

v5.9.0 (2026-09-10) — AI Writing Assist (éditeur)

L'IA Notion dans l'éditeur : commandes slash /ai, autocomplétion en ligne et remplissage automatique des propriétés d'une base, au-dessus du moteur FlowDeck Agent (v4.10.0) et de l'endpoint headless /api/agent/generate.

  • Service app/services/ai_writing.py — AIWritingService sans outils, six actions (write, summarize, translate, continue, autocomplete, properties). Prompt système strict (Markdown léger, aucun préambule) et replis déterministes hors-ligne (aucun appel réseau sans clé API).
  • Commandes slash IA — nouveau groupe « AI » dans le menu / de l'éditeur : Write with AI, Summarize, Translate, Continue writing. Write ouvre le composeur inline existant ; Summarize et Continue insèrent le résultat sous le bloc ; Translate remplace le contenu de la page dans la langue demandée.
  • Autocomplétion — module AIAC : après ~900 ms d'inactivité pendant la frappe dans un bloc texte, une suggestion courte est proposée dans une pastille « Tab » ancrée au bloc ; Tab l'insère, Escape la rejette.
  • AI properties — bouton ✨ sur chaque ligne de la table database : POST /api/agent/writing/properties propose des valeurs (Status, Priority, Résumé…) à partir du titre/contenu, appliquées et persistées via l'API collection existante.
  • Endpoints — POST /api/agent/writing (action + prompt/contexte/langue/ préfixe) et POST /api/agent/writing/properties (suggestions structurées), provider/modèle par utilisateur via les clés LLM existantes.
  • Tests — tests/test_ai_writing.py (+29) : service hors-ligne, coercition des valeurs, parsing JSON, routes et câblage des templates. Version 5.9.0.

v5.5.0 (2026-09-08) — Partage : réorganisation & indicateurs

Vue complète du partage : sidebar « Shared » scindée en « Par moi » / « Avec moi », onglet Shared de la bibliothèque filtrable, fenêtre Share rechargée à l'ouverture (bug de refresh), et emoji 👥 sur les fichiers partagés du workspace local.

  • Correctif fenêtre Share — ouverture de la boîte de dialogue (toggleShareOpen) au lieu de laisser la liste vide après un refresh ; les partages sont rechargés (loadShares) à chaque ouverture.
  • Sidebar Shared — la section affiche deux sous-groupes : « Par moi » (partages nominatifs créés + pages en « lien ») et « Avec moi » (partages reçus), chacun avec son propre état vide (les pages déjà dans « Par moi » sont exclues de « Avec moi » pour éviter les doublons).
  • Bibliothèque — onglet Shared doté d'un filtre « Tous / Par moi / Avec moi » ; l'endpoint GET /api/library/shared accepte dir=made|received|all et renseigne share_dir sur chaque élément.
  • Indicateur de partage — la barre du document affiche « 👥 Shared » au lieu de « 🔒 Share » dès que la page est partagée (membre, lien ou publiée) ; état recalculé à la volée après chaque action de partage.
  • Workspace local — les fichiers partagés affichent l'emoji 👥 juste avant leur nom dans l'arborescence (is_shared exposé par /api/local-workspace/tree).
  • Tests (tests/test_sharing.py +6) : direction made/received/all de la bibliothèque, fallback dir invalide, et rendu pageIsShared sur la page. Suite complète 325 verte (+3 PDF pré-existants).

v5.4.1 (2026-09-08) — Partage de page : correctif permissions + autocomplete membres

Le partage à un membre existant échouait avec « Invalid permission. Use view, comment, or edit. » — le client envoyait editor/commenter/viewer alors que l'API n'accepte que view/comment/edit. Corrigé, et enrichi.

  • Correctif permissions — défaut du champ d'invitation passé à edit ; valeurs, libellés et surbrillance du menu de permission des participants alignés sur le contrat API (view/comment/edit).
  • Mise à jour de permission persistée — nouvel endpoint PUT /api/pages/{page_id}/share/{share_id} ; le menu de permission d'un participant enregistre désormais le changement (avant : visuel uniquement).
  • Autocomplete des membres existants — en tapant dans le champ d'invitation, recherche dans /api/notifications/users/search?q= (debounce 150 ms) avec liste de suggestions (avatar couleur, login, nom complet), navigation clavier ↑/↓/Entrée/Échap ; soi-même et membres déjà partagés exclus.
  • Partage lié au compte — la sélection d'un membre dans les suggestions envoie user_id, le partage est donc lié au compte (pas seulement à un email) ; un email peut toujours être saisi librement.
  • Upsert anti-doublon — partager un membre déjà présent met à jour sa permission au lieu de créer une ligne dupliquée ; trim de l'email côté serveur.
  • Tests (tests/test_sharing.py) : partage par user_id/email, permission invalide rejetée, upsert, PUT permission, suppression avec unset is_shared, erreurs d'auth. Suite complète 319 verte (+3 PDF pré-existants).

v5.4.0 (2026-09-08) — Interactions de bloc

Manipulation riche des blocs façon Notion : undo/redo, duplication, menu de bloc ⋮ (turn into, couleurs, lien vers le bloc, déplacer, supprimer), glisser-déposer multi-sélection et en-têtes de tableau configurables.

  • Undo/Redo : pile d'historique (100 entrées) alimentée à chaque mutation (frappe Entrée, retour arrière, slash, tableau, toggle, colonnes…), rappel Ctrl+Z / Ctrl+Shift+Z ou Ctrl+Y, restauration avec re-focus du bloc.
  • Duplication de bloc : Ctrl+D, menu bloc ou commande slash « Duplicate » ; copie profonde (enfants columns/toggle inclus) insérée juste en dessous ; en multi-sélection, le groupe entier est dupliqué.
  • Menu de bloc ⋮ sur la poignée (clic ou clic droit) : « Turn into » (sous-menu conservant le contenu), Duplicate, Copy link to block (#fdblk-<id>, scrolls/focus au chargement), Move to… (dialogue de recherche des pages de l'arbre, déplacement du bloc via l'API), Delete, couleur de texte et de fond appliquées à un bloc ou à la sélection.
  • Glisser-déposer multi-sélection : Shift+clic sur la poignée ajoute un bloc à la sélection ; déplacer un bloc sélectionné déplace tout le groupe (reconstruction de l'ordre depuis le DOM via data-id).
  • Commandes slash étendues : groupe « Actions » avec Duplicate, Turn into…, Copy link to block et Delete block.
  • En-têtes de tableau : toggles « Header row » / « First col » dans la barre du tableau ; has_header (toujours actif par défaut) et nouveaux first_col_header persistés et pris en compte dans les exports Markdown/HTML (app/services/export.py) ainsi que que dans blocksToMarkdown client.
  • Intégration realtime : méthode syncNow/forceEmit sur window.__fdRT appelée après chaque mutation programmatique pour pousser immédiatement les ops (undo, duplicate, drag, turn into, couleurs, déplacement, toggles).
  • Tests (tests/test_block_interactions.py) : persistance des blocs avec has_header/first_col_header/style, exports table Markdown/HTML, endpoint « Move to » (get page + POST blocks sur la cible).

v5.3.0 (2026-09-08) — Realtime : édition collaborative en direct

Deux utilisateurs peuvent maintenant éditer une même page en même temps
les blocs, le titre, les curseurs et la présence se synchronisent via une connexion WebSocket, avec fusion last-write-wins et repli sur polling.
  • Passerelle WebSocket WS /ws/pages/{page_id} (app/services/realtime_server.py
    • app/routers/realtime.py) : salons en mémoire par page, authentifiés via le cookie de session (refus 4401), page introuvable/supprimée → 4404.
  • Protocole de synchronisation : hello/sync/op/ack/title/sel/ ping/peer_join/peer_leave ; chaque page porte une version incrémentée à chaque opération appliquée ; un client périmé reçoit un sync complet.
  • Merge last-write-wins par bloc : insert, update (remplacement de bloc), delete, move — appliqués dans l'ordre d'arrivée ; une frappe en cours dans un bloc focalisé n'est jamais écrasée par une mise à jour distante.
  • Client éditeur (app/templates/_page_editor_realtime.html) : diff local → ops (update/delete/move/insert), envoi dès que l'utilisateur tape (debounce), application des ops distantes avec re-render discret + re-focus du bloc actif.
  • Présence & curseurs : avatars colorés dans la barre du haut et curseurs de chaque pair positionnés dans le document (calque dédié, mise à jour à l'édition/au scroll).
  • Titre synchronisé : diffusion du titre (debounce) sans écraser un titre en cours d'édition.
  • Fallback polling : si le WebSocket est indisponible, rafraîchissement toutes les 10 s de l'état serveur (adopté uniquement si pas de brouillon local) avec reconnexion automatique.
  • Persistance : écriture debounce (~1 s) de content + title en base, flush immédiat à la déconnexion du dernier client.
  • Sécurité : connect-src CSP étendu à ws: ; aucune donnée sensible transmise (les peers n'exposent que id/login/full_name/couleur).
  • Tests (tests/test_realtime.py) : auth, page absente, hello→sync, merge LWW à deux clients + persistance, présence join/leave, curseurs, titre, resync des clients périmés, apply_op/merge_ops unitaires.

v5.2.0 (2026-09-07) - Automations : un moteur de règles (if-this-then-that)

Nouveau moteur de règles : brancher des actions sur des événements de pages (création, mise à jour, suppression, déplacement), sur un planning cron ou sur un bouton cliquable directement dans l'éditeur.

  • Moteur de règles : trigger (événement / cron / bouton), conditions combinables (eq, neq, contains, is_empty, is_not_empty, changed) et actions (webhook, changer une propriété, créer une page, notification).
  • Événements couverts : collection créée/modifiée/supprimée, page créée/modifiée/supprimée/déplacée (API et Canvas).
  • Déclencheur cron : */N * * * *, minute fixe, @hourly, @daily, avec un planificateur en arrière-plan qui exécute les règles dues chaque minute.
  • Bouton d'automation : nouveau bloc de type button dans l'éditeur (menu slash) qui déclenche une règle au clic.
  • Interface : panneau Automations dans les paramètres (créer, modifier, activer/désactiver, lancer, supprimer, historique des exécutions).
  • Historique des exécutions : chaque déclenchement est tracé (statut, détail, page/collection concernée).

v5.1.11 (2026-09-07) — Éditeur : collage intelligent (découpe en blocs)

Coller un texte dans un bloc le plaçait en entier dans ce bloc, même s'il contenait plusieurs paragraphes, des listes, des titres markdown, du code ou un tableau.

  • Découpe automatique au collage : le contenu collé est analysé (paste2b) et réparti en plusieurs blocs selon son format (markdown ou texte simple).
  • Pris en charge : titres #→####, listes à puces (-, *, +), listes numérotées, cases à cocher - [ ] / - [x] (y compris [X]), citations > , séparateurs ---/***, blocs de code (fences ```), tableaux markdown et paragraphes.
  • Texte simple : les lignes courtes (≤ 55 caractères) deviennent chacune un bloc ; les blocs de texte longs (paragraphes) restent groupés ; une ligne vide crée une nouvelle section.
  • Comportement au curseur : le texte avant le curseur reste dans le bloc courant (qui est réutilisé comme premier bloc si vide), puis les blocs analysés sont insérés, et le texte après le curseur conserve sa propre place (refocus sur le dernier bloc inséré).
  • Les blocs non textuels (code, tableau, image, formules…) reçoivent toujours le collage en texte brut, sans découpe.

v5.1.10 (2026-09-07) — Espace local : boîte de renommage à la taille du nom

Renommer un fichier en cliquant sur son nom ouvrait une boîte d'édition qui s'étendait sur toute la largeur de la ligne, et le bouton « Open » restait collé à droite du panneau.

  • La boîte de renommage inline (inline-rename-input) est maintenant large comme le nom du fichier : la largeur est mesurée à l'ouverture (_fitNameWidth) avec un minimum de 60 px et est bornée à l'espace disponible pour les très longs noms (le texte défile alors dans la boîte).
  • Le bouton Open est placé juste à droite de la boîte d'édition (marge 6 px) au lieu du bord droit : il est déplacé à l'intérieur de la cellule .name.file, devenue inline-flex (avec overflow:hidden + max-width), pour que le bouton ne passe jamais à la ligne même en colonne étroite.
  • S'applique aux deux rendus de l'arbre : la vue principale (x-for) et renderChildren (dossiers imbriqués / arbre statique).

v5.1.9 (2026-09-07) — Validation robuste des modèles Nvidia + logo Agent noir & blanc

La liste Nvidia « diminuée » de la v5.1.8 contenait encore des modèles en erreur 404/410. Deux causes réelles : (1) le payload de validation ne correspondait pas à celui utilisé au runtime et un payload partagé entre les requêtes concurrentes envoyait des noms de modèles croisés (race) ; (2) rien ne protégeait l'usage si un modèle devenait indisponible après validation.

Validation des modèles (fournisseur bruité)

  • _validate_chat_models() utilise désormais le payload de production (messages + temperature: 0.2, sans max_tokens) : un modèle qui répond 200 à la validation répond aussi 200 à l'usage réel.
  • Race fixée : le payload est reconstruit pour chaque modèle (plus de nom de modèle croisé entre requêtes concurrentes) — c'est pourquoi la v5.1.8 retombait par intermittence sur la liste brute non validée.
  • Un seul payload partagé retiré ; retente une fois sur timeout/5xx (les modèles lents mais fonctionnels survivent) ; rejette définitivement les 4xx (404 modèle inconnu, 410 retiré/déprécié) ; garde les 429 (routés donc utilisables).

Repli automatique au runtime (model not found / gone)

  • LLMClient._http_complete : si le modèle choisi répond 404/410, retente une fois avec le modèle par défaut du provider et ajoute un notice expliquant le basculement — visible dans le panneau Agent (nouvel événement SSE « notice », bandeau .fd-ap-notice, réinitialisé à chaque conversation).
  • Le modèle réellement utilisé est persisté dans l'historique de conversation.

Panneau Agent — logo noir & blanc thémé

  • Le rond bleu en bas à droite (FAB) et le logo du header du panneau (à gauche de « How can I help you today? ») passent de l'accent bleu + émoji 🤖 à un rond noir/blanc qui suit le thème clair/sombre, avec une étincelle ✨ monochrome (SVG currentColor : rond --text-primary, étincelle --bg-primary).

v5.1.8 (2026-09-07) — Multi-lignes réellement conservées + modèles IA validés (Nvidia)

Deux correctifs : le bug multi-lignes (v5.1.7 corrigeait le symptôme au re-rendu, mais la vraie cause était la lecture en textContent qui élimine les <br>) et, côté Agent, seuls les modèles réellement utilisables du fournisseur Nvidia sont désormais proposés dans la configuration IA.

Éditeur — multilignes conservées (vraie cause)

  • sync() lisait el.textContent, qui supprime les <br> : les sauts de ligne étaient perdus dès la sauvegarde du bloc. Ajout de gt() (basé sur innerText, qui rend bien les <br> en \n) utilisé par sync(), les tableaux (toggle/columns) et le calcul de position du curseur.
  • La scission sur Entrée (et la limite bas/haut de bloc) utilisait désormais splitCaret() : un marqueur temporaire est inséré au curseur puis gt()/innerText découpe le contenu en « avant » / « après », exactement comme un vrai clavier. Fini le décalage before qui perdait la fin d'un bloc multi-lignes.
  • _resultText (composer IA) lit par gt() pour préserver les \n.

Agent — validation des modèles (fournisseur bruité)

  • GET {base}/models de Nvidia liste tout le catalogue, mais la plupart des modèles (embeddings, rerank, génération d'images…) répondent 404 sur /v1/chat/completions — l'erreur observée dans la config IA.
  • fetch_provider_models() filtre désormais pour nvidia : suppression des modèles manifestement non-chat (nom), puis « probe » réelle de chaque candidat (POST /chat/completions, payload minimal, pipeline limité à 6 en parallèle, 6 s par appel) — seuls les modèles qui répondent sont listés. Un 429 (limite de débit) est conservé (le modèle est routé, donc utilisable).
  • Filet de sécurité : si toutes les validations échouent, on garde la liste filtrée par nom plutôt que de vider le sélecteur.
  • Tests : tests/test_llm_config.py (5 cas) — catalogue mock, 404 chat, retour sans validation pour les autres providers, chute de sécurité, offline.

v5.1.7 (2026-09-07) — Retours à la ligne conservés quand on change de bloc (Entrée)

Un bloc multi-lignes (Shift+Entrée) perdait ses retours à la ligne dès qu'on appuyait sur Entrée pour créer un nouveau bloc.

Cause

  • Shift+Entrée insère des <br> ; sync() lit el.textContent, qui contient alors des \n. mdEsc() (render des blocs) échappait le texte mais ne convertissait pas les \n, et le HTML les replie en espace → les lignes disparaissaient au re-rendu après la scission Entrée.

Correctif

  • mdEsc() convertit désormais les \n restants en <br> (après les remplacements markdown inline) : les retours à la ligne sont conservés dans tous les types de bloc (paragraphe, listes, to-do, toggle, callout, code…).

Conforme au comportement Notion : le titre affiche « New Page » quand il est vide ; les aides de blocs (« Press 'space' for AI… », « Heading 1 »…) ne s'affichent que lorsque le bloc a le curseur.

Titre « New Page » (document vide)

  • Le placeholder du titre utilisait :empty avec le texte figé « New page » et la couleur très sombre --text-dim. :empty casse dès qu'un <br> est injecté dans le contenteditable.
  • Ajout de data-placeholder="New Page" et d'une bascule de classe .empty (même mécanisme que les blocs) : le placeholder s'affiche en gris pâle (--text-secondary, opacité .75) dès que le titre est vide, y compris après effacement.
  • save() stocke désormais un titre vide (au lieu de forcer « New page ») : le placeholder persiste après autosave ; la sidebar garde son fallback d'affichage.
  • duplicatePage() retombe sur « New Page » si le titre est vide.

Aides de blocs affichées seulement au focus

  • Les placeholders des blocs (« Press 'space' for AI or '/' for commands », « Heading 1 », « List »…) s'affichaient en permanence quand le bloc était vide.
  • Les règles passent sous :focus-within : l'aide n'apparaît que lorsque le bloc est focalisé (curseur dans le bloc) et disparaît dès qu'on le quitte.
  • Même comportement pour l'aide « Type code... » du bloc code.

Les textes d'aide « Press 'space' for AI or '/' for commands », « Heading 1 », « List », etc. restaient affichés pendant l'édition.

Cause

  • La classe .empty (qui pilote les placeholders) n'était basculée qu'au render(). Pendant la frappe, l'événement input mettait à jour dirty et autoSave mais jamais .empty : le texte d'aide persistait sur un bloc qui avait pourtant du contenu.

Correctif

  • Le listener input des blocs (rendu render()) bascule désormais .empty en direct selon le contenu : dès qu'un caractère est saisi, .empty est retiré et le placeholder disparaît ; quand le bloc est vidé, .empty revient et le placeholder réapparaît (mécanisme identique pour les titres « Heading 1… », listes, callout, etc.).

Résout les derniers retours UI de l'éditeur v5.1.2/5.1.3.

Placeholder « Press 'space' for AI » — jamais affiché

  • Cause réelle : la classe .empty est basculée sur l'élément interne [data-bid] (sélecteur [data-placeholder] au render), mais la règle CSS ciblait l'élément externe .block-content — deux éléments différents, donc le ::before d'aide ne se rendait jamais.
  • La règle cible désormais .block-content [data-bid].empty::before (l'élément qui porte réellement data-placeholder + .empty).

Flèches ↑/↓ du menu /

  • La navigation dépendait de l'endroit où se trouvait le focus (_in vs bloc vs ailleurs) : onKd n'est lié qu'à #_blocksCt, l'input vit dans #_slashMenu (un voisin), d'où un comportement fragile/incohérent.
  • Ajout d'un écouteur keydown au niveau document (actif quand le menu est ouvert) : ↑/↓/Entrée/Échap fonctionnent quel que soit le focus, plus de double déclenchement.
  • La sélection .selected passe en !important pour toujours gagner sur .slash-item:hover, et l'élément sélectionné est auto-défilé (scrollIntoView).

Item « vocabulaire / vocabulaire » du menu / mal positionné

  • Les icônes texte 'lightbulb' (Callout) et 'image' (Image) étaient injectées en clair dans la pastille d'icône, provoquant un texte décalé/débordant.
  • Remplacées par les emojis 💡 (Callout) et 🖼️ (Image).

Composer « Edit with AI » — largeur

  • Le composer AI utilisait une largeur fixe 420px ; il s'aligne désormais sur la largeur du bloc ciblé (min 280px, max 1200px).

Résout les régressions du composer AI et de l'éditeur Notion-style v5.1.2.

AI inline — erreur 400 DeepSeek (espace → AI)

  • fdAgent.generate passait par l'engine à outils (/conversations/{id}/run) qui envoie un schéma tools/tool_choice que DeepSeek rejette en 400 Bad Request.
  • Bascule de generate sur l'endpoint /api/agent/generate (sans outils, prompt propre + contexte) : le composer AI, « Ask AI », la note de réunion et la synthèse renvoient désormais un simple texte Markdown. Le bloc « 400 » disparaît.

Bloc vide (placeholder)

  • Le hint « Press 'space' for AI or '/' for commands » était rendu trop discret (--text-dim). Il passe à --text-secondary avec opacité, plus lisible. (Le texte n'apparaît que sur un paragraphe réellement vide — comportement Notion.)

Slash menu — flèches ↑/↓

  • La sélection n'était pas visible au démarrage (rien de surligné) → les flèches semblaient inactives. _rs() est désormais appelé à l'ouverture et après filtrage, et l'item sélectionné reçoit un fond accentué + liseré gauche clair. Testé en jsdom : ↓/↑ déplacent la sélection, Entrée applique l'élément.

Contour du bloc sélectionné

  • Suppression des anneaux de focus (outline/box-shadow) sur tous les blocs éditables ([data-bid], .block-content, titre de page) quand sélectionné.

Poignée de déplacement (⠿)

  • La poignée 6 points restait invisible (seule .block-column-wrapper:hover la montrait). Ajout de .block-wrapper:hover/:focus-within .block-handle { opacity:1 } pour les blocs normaux.

Tests / validation

  • 271 tests pytest verts ; rendu édition 200 (hint/AIC/trigger présents) ; route /api/agent/generate enregistrée ; JS validé (node --check + jsdom).

Adapte le visuel des blocs à l'expérience Notion AI : hint sur bloc vide, menu / compact avec raccourcis + aperçu au survol, et composer AI inline (« Edit with AI » → « Brewing… » → résultat encadré → « Insert below »).

Bloc vide & commandes (Images 1 & 3)

  • Hint de bloc vide : sur un paragraphe vide, le placeholder devient « Press 'space' for AI or '/' for commands ».
  • Menu slash refait : compact (icône + nom + raccourci #/##/###… à droite), groupes renommés (« Basic blocks », « Media », « Data »), footer « Close menu (esc) », champ « Type to search » en bas, et aperçu au survol d'un item (carte « Our Values » / exemple de rendu de bloc), positionnée à côté du menu.

Composer AI inline (Images 2, 4, 5, 6)

  • Espace sur un bloc vide ouvre le composer AI inline (pill « Edit with AI »).
  • Entrée → l'agent génère : indicateur « Brewing… » animé (bouton ■ pour stopper), puis résultat encadré (markdown rendu : titres, listes, gras…) avec barre d'actions (👍/👎) et bouton « Insert below ».
  • Insert below insère le contenu comme vrais blocs (via applyAIBlocks → md2b).

Tests / validation

  • 271 tests pytest verts (aucune régression) ; éditeur rendu 200 (hint, menu, AIC présents) ; JS validé (node --check).

L'agent FlowDeck et l'insertion dans les pages affichent désormais le markdown rendu (titre, gras, tableaux, code…) au lieu des caractères bruts ##, **…

Éditeur de page (blocs)

  • Insertion agent → vrais blocs : applyAIBlocks() (et le mode replace de fdApplyDocument) convertit la réponse markdown en blocs réels via md2b() : ## titre → bloc heading_2, tableaux GFM → bloc table, ``` → bloc code, listes -/1., to-do, citations, séparateurs. Plus de ## affiché littéralement.
  • Rendu inline : les blocs de texte affichent désormais le markdown inline (gras **, italique *, code `, lien [t](url), barré ~~) sans les caractères, via mdEsc() dans renderBlock (paragraphes, titres, listes, to-do, toggle, citation, callout, code).
  • Les raccourcis clavier markdown existants (# + espace → H1…) restent inchangés.

Panneau FlowDeck Agent

  • renderMarkdown() (agent_panel.html) : les réponses de l'agent s'affichent en HTML rendu — titres H1–H4, tableaux (GFM), blocs de code, listes à puces/numérotées (+ cases), citations, séparateurs, gras/italique/code/lien/barré. HTML échappé avant rendu (sécurité face au contenu LLM). CSS dédié (.fd-md-*, pre, tableaux).
  • Le bouton « Insérer dans la page ouverte » continue d'utiliser fdApplyDocument (maintenant → blocs réels).

Tests / validation

  • 271 tests pytest verts (aucune régression). Rendu markdown vérifié fonctionnellement en Node (titres, tableau, code, listes) ; éditeur et panneau rendus 200 avec les nouveaux composants présents.

Livre la v5.3.0 du roadmap : databases inline dans les pages (slash /database), galerie de templates prédéfinis (CRM, Project tracker, …) et validation des propriétés (required / unique / min / max) côté serveur + UI.

Added — Inline databases (slash /database)

  • Commande slash /database (groupe DATA) dans l'éditeur : ouvre un sélecteur de templates, crée une database inline liée à la page et insère un bloc embed_type:'collection' rendu par FlowDeckDB.renderInto.
  • Le bouton « Database » de la barre Get Started ouvre désormais le même sélecteur (au lieu de créer une database vide directement).

Added — Templates de database prédéfinis

  • 6 templates intégrés seedés (migration v4) : Project tracker, CRM / Contacts, Task list, Content calendar, Meeting notes, Reading list — avec icône, description et schéma de propriétés.
  • app/services/db_templates.py : materialize_properties() (schéma → collection_properties) et create_from_template() (collection + propriétés + vue par défaut).
  • POST /db/api et POST /db/inline/api acceptent "template": "<nom>" et matérialisent les propriétés (titre exclu — c'est le titre de ligne). Colonne icon ajoutée à database_templates (migration v4).
  • apply_db_template (/workspace/templates/database/{id}/apply) matérialise les propriétés.

Added — Validation des propriétés (required / unique / min / max)

  • Nouvelle colonne collection_properties.validation_json (migration v4) ; le type/format et les règles de validation sont portés par validate_property_rule() dans property_types.py (required, unique, min, max, min_length, max_length).
  • Serveur : create_page_api et update_page_api (/db/{c}/pages/api, /db/pages/{id}/api) valident les valeurs avant écriture → 400 + message clair. La règle unique exclut la page en cours d'édition. Les valeurs sont résolues par id ou par nom.
  • API propriétés : POST/PUT /db/{c}/properties/api acceptent validation (persisté).
  • UI (_database_table_scripts.html) : la modale « Add property » expose Required, Unique, Min, Max ; en cas de 400 à l'édition d'une cellule, la cellule est marquée en rouge et un toast affiche le message (valeur restaurée).

Tests

  • 271 tests (+9 dans tests/test_db_advanced.py) : templates seedés, création de collection/inline depuis template (propriétés matérialisées), apply template, validation required/unique/min-max (create + update, exclusion de la ligne courante), persistance de la config de validation, payload collection.

Livre deux chantiers du roadmap v5 : la migration au système de schéma versionné (v5.2.0, priorité n°1 — débloque tout le reste) et la Command Palette + recherche full-text (v5.0.0).

Added — Command palette (v5.0.0)

  • Palette de commandes Ctrl+K / Ctrl+P dans base.html : modale centrée autofocusée, navigation clavier (↑/↓/Enter/Esc), surlignage fuzzy des correspondances, recherche universelle et quick actions (Nouvelle page, Library, My Tasks, Trash, Settings, Help). Le bouton Search de la sidebar et openQuickFind() ouvrent désormais la palette (fini le toast « Quick Find — Ctrl+K »).
  • Endpoint GET /api/search?q= (app/routers/search.py) + service app/services/search.py : recherche unifiée des pages (titre + contenu) et des databases (nom + description), scopée aux workspaces accessibles de l'utilisateur.
  • Index full-text FTS5 (pages_fts, migration v3) avec triggers de synchronisation (insert/update/delete) sur la table pages ; repli automatique sur LIKE si FTS5 absent. Exclusions automatiques des pages supprimées (corbeille).

Added — Migrations versionnées (v5.2.0)

  • Runner léger app/migrations.py (alternative à Alembic pour ce projet SQLite) : table schema_version enregistrant la version appliquée ; le schéma de base existant (init_db) devient la baseline v1, et chaque changement futur est une étape versionnée appliquée une seule fois, dans l'ordre.
  • Migration v2 — indexes manquants : users(email), user_oauth_tokens(user_id, provider), collections(workspace_id), pages(workspace_id), pages(deleted_at).
  • Migration v3 — FTS5 : pages_fts + triggers pages_fts_ai/ad/au + backfill.
  • init_db() termine désormais par apply_migrations(conn) : nouveau schéma et bases existantes convergent sans rejouer les migrations déjà appliquées.

Tests

  • 267 tests (8 nouveaux dans tests/test_search_migrations.py) : suivi de la version schema_version, indexes créés, table/triggers FTS5, synchronisation de l'index, recherche pages+collections, exclusions de la corbeille, requête vide et sans résultat.

Corrections

  • Test de connexion multi-provider : le modèle configuré globalement (ex. deepseek-v4-flash) n'est plus envoyé lorsqu'on teste un autre provider (ex. nvidia) — c'était la cause d'un HTTP 404 « model not found ». Chaque provider utilise désormais son modèle par défaut tant que le modèle global ne le concerne pas.
  • Presets NVIDIA actualisés : les anciens modèles (nvidia/llama-3.1-70b-instruct, nvidia/nemotron-4-340b-instruct) étaient retirés de la plateforme ou soumis à abonnement ; liste remplacée par des modèles disponibles (nvidia/nemotron-3-super-120b-a12b, meta/llama-3.1-70b-instruct, deepseek-ai/deepseek-v4-pro, z-ai/glm-5.2, …).

Note : un 404 persistant après cette correction vient de NVIDIA lui-même (code « Function … : Not found for account ») — cause fréquente : le droit « Public API Endpoints » pas activé sur le compte/clé pour le modèle choisi.

v4.15.4 (2026-09-06) — Correctifs listes agent (@ et /)

Corrections

  • Menus « @ » vides : le template Alpine du menu de mentions avait deux racines (Alpine n'en accepte qu'une) — seuls les titres de sections s'affichaient, sans éléments. Les éléments sont maintenant bien rendus sous chaque section.
  • Sélection clavier visible dans le menu « / » : le changement de focus (↑/↓) n'était pas visible faute de style .focus ; ajout d'un surlignage (fond + barre d'accent à gauche).

v4.15.3 (2026-09-06) — Agent : mentions @ par espace courant + skills composés en arrière-plan

La commande « @ » propose en priorité les fichiers de l'espace de travail ouvert, puis les autres contextes (collections, pages de collections, autres documents). La navigation clavier (↑/↓) positionne maintenant visuellement la sélection dans la fenêtre (défilement), et les fichiers identiques ne sont plus affichés plusieurs fois (déduplication par titre).

Améliorations

  • Mentions @ contextuelles : nouvelle API /api/agent/mentions par sections : Fichiers de l'espace courant (« workspace_id transmis par le frontend »), Collections / bases, Pages de collections, Autres documents.
  • Navigation clavier corrigée : ↑/↓ font défiler la liste ouverte pour garder la sélection visible à l'écran (aussi appliqué au menu /). Entrée/Tab sélectionne toujours l'élément focalisé.
  • Déduplication : deux fichiers du même type et même titre (ex. « Sans titre », « Aussi Vide ») n'apparaissent qu'une seule fois dans les propositions.
  • Skills composés en arrière-plan : sélectionner un skill n'injecte plus son texte dans la boîte d'édition. À l'envoi la requête est assemblée : contexte1 + contexte2 + skill-1 + skill-2 + texte. Les skills intégrés (templace) et enregistrés (skill_ids) sont tous deux pris en compte ; les contextes @ continuent d'être résolus en contenu côté serveur.

v4.15.2 (2026-09-06) — Agent : skills multi-épinglés en bulles + chips de contexte cliquables

Les skills s'épinglent désormais au contexte comme les mentions @ (bulles « / » purples dans l'éditeur du chat), on peut en associer plusieurs à un même post, et chaque bulle de contexte référencée s'ouvre d'un clic comme document courant.

Améliorations

  • Skills en bulles : sélectionner un skill (menu /, intégré ou enregistré) ajoute une vraie bulle dans le composer, comportement identique aux mentions @. La bulle d'un skill intégré greffe son modèle de demande dans le texte ; plusieurs skills peuvent coexister.
  • Plusieurs skills par post : l'API /run accepte skill_ids (tableau) et l'engine injecte tous leurs prompts dans les instructions système (skill_id reste supporté pour la rétro-compatibilité).
  • Chips de contexte cliquables : cliquer sur une bulle de contexte (@ document / page / base) ouvre l'élément comme document courant — document:<id>/page:<id> → /pages/<id>, collection:<id> → /db/<id>. Le bouton « ✕ » retire toujours la bulle (sans ouvrir).
  • Envoi possible avec uniquement des skills (sans texte) : « Applique le(s) skill(s) sélectionné(s)… ».

Correctifs

  • L'écriture du chat (send) renvoie désormais tous les skill_id épinglés via skill_ids.

v4.15.1 (2026-09-06) — Agent : réparation des conversations réelles (protocole tool_calls)

Le chatbot ne « répondait » plus : avec un fournisseur réel, dès qu'un outil était appelé la passe suivante était refusée par l'API (message assistant sans ses tool_calls) et l'agent retombait silencieusement sur le mock hors-ligne, qui se contentait de répéter la question.

Correctifs

  • Protocole tool-calling conforme (agent_engine.py) : le message assistant qui annonce un appel d'outil transporte désormais ses tool_calls (avec id), et chaque résultat d'outil répond avec le tool_call_id correspondant (y compris en cas de refus/permission). Le run se poursuit alors correctement et se termine par une vraie réponse du modèle.
  • Plus de repli silencieux (llm_client.py) : quand un fournisseur réel configuré échoue, l'erreur remonte (événement SSE error) au lieu de produire silencieusement un écho hors-ligne. Le mock n'est utilisé que si le fournisseur est réellement non configuré (offline / pas de clé).
  • Les tool_calls analysés conservent leur id et les arguments bruts (arguments_raw).
  • Test de régression test_engine_tool_protocol_messages.

v4.15.0 (2026-09-06) — Agent : actions sous chaque réponse, mentions @ & skills /

Le panneau FlowDeck Agent gagne l'expérience « type Notion AI » : chaque réponse de l'agent expose des actions rapides (copier, insérer dans la page, feedback 👍/👎), on peut épingler au contexte plusieurs documents / pages / bases via la commande @ (ou le bouton +), et la commande / propose des skills de travail réutilisables en plus des commandes admin existantes.

Sous chaque réponse de l'agent

  • ⧉ / ➕ / 👍 / 👎 : boutons à icônes seules (infobulle au survol) — copier la réponse, insérer dans la page ouverte, feedback 👍/👎 enregistré en base.
  • Le bandeau statique « Contexte : … » du haut du panneau est supprimé.

Contexte épinglé — commande « @ » / bouton « + » (style Notion AI)

  • Le composeur est maintenant un éditeur contenteditable : les références sélectionnées s'insèrent inline comme des pastilles cliquables (icône + nom) directement dans le message.
  • Au-dessus du composeur, une barre de contexte groupée rassemble les fichiers épinglés (rangée documents, icônes 📄) et les skills (rangée séparée, icône ✨), chacun retirable.
  • @ ouvre le sélecteur (icône + titre + chemin) listant documents, pages et bases ; l'élément choisi devient un jeton inline et une puce de contexte. + épingle un élément au contexte sans l'insérer inline. Les puces persistent jusqu'au retrait (et sont vidées en nouveau chat).
  • Contexte d'ouverture : ouvrir l'agent sur un document (/pages/:id) ou une base (/db/:id) ajoute automatiquement ce contexte comme mention (puce épinglée) — plus de bandeau statique.
  • À l'envoi, chaque référence est transmise comme mention ; le backend résout son contenu réel.

Skills — commande « / »

  • Le menu / liste les skills intégrés (research, resume, brainstorm, plan, meeting-note, livrable…), les skills enregistrés (table agent_skills) et les commandes admin classiques.
  • Un skill intégré pré-remplit le composeur d'un modèle à compléter ; un skill en base s'épingle en chip et est appliqué via skill_id à chaque envoi.
  • Navigation clavier (↑/↓/Entrée/Échap) dans les menus ; aucun envoi parasite pour « / » seul.

Infra & API

  • Nouvelle table agent_feedback (rating, conversation/message, extrait, commentaire).
  • GET /api/agent/mentions?q= : recherche des éléments mentionnables du workspace.
  • POST /api/agent/feedback : enregistre un retour 👍/👎.
  • VERSION → 4.15.0 ; app/main.py version → 4.15.0.

v4.14.0 (2026-09-06) — Agent : création de documents dans un espace + nouveaux outils & titre auto

L'agent sait enfin créer, lire, modifier et supprimer des documents (pages éditeur) dans un espace de travail — la demande « crée un document « Projet » dans le workspace « TEST WS 1 » » ne se termine plus par un simple search_workspace vide. Il liste aussi les espaces, et renomme automatiquement les conversations avec un titre descriptif au lieu de « Nouvelle conversation ».

Outils (nouveaux / corrigés)

  • create_document : crée une page éditeur (pages) dans un espace (par workspace_id ou workspace_name), avec contenu Markdown optionnel — snapshot d'undo.
  • read_document : lit un document (titre + contenu + métadonnées).
  • read_workspaces : liste les espaces accessibles (rôle, nb de documents/collections).
  • delete_document : suppression → corbeille (deleted_at), annulable (softdelete).
  • write_blocks étendu : renommage de titre et/ou écriture des blocs ; undo restaure titre + contenu.
  • search_workspace étendu : recherche désormais aussi les documents et les espaces de travail.

Contexte & planification

  • Contexte de l'agent enrichi : sections « Documents » et « Espaces de travail » (avec ids) pour que le LLM cible le bon espace. Guide mis à jour. Mock hors-ligne : détection « créer un document dans un espace » / « lister les espaces ».

Conversations

  • Auto-titre : après la première exécution d'une conversation encore nommée « Nouvelle conversation », l'agent génère un titre descriptif (déterministe, hors-ligne sûr) ; l'historique devient navigable.

UX

  • Indication claire dans le composeur du panneau : Entrée ↵ = envoyer · Maj+Entrée = nouvelle ligne (le comportement existait déjà, il est maintenant visible + placeholder mis à jour).

Infra

  • VERSION → 4.14.0 ; app/main.py version → 4.14.0.

v4.13.2 (2026-09-06) — Saisie libre « écrire un texte » → proposition à appliquer au document

Quand un document est ouvert et qu'on tape directement une demande de rédaction/contenu dans le chat (« Écrire un texte sur… », « Rédige… », « Résume… », « Traduis… »), la requête est maintenant routée vers la génération de texte (sans outils) et affichée comme proposition à appliquer ou rejeter — au lieu d'un search_workspace puis d'un simple « Objectif traité ». « Traduire » remplace le contenu de la page ; les autres demandes l'insèrent en fin de page.

Fix

  • Envoi libre avec contexte document : détection d'intention de contenu (écrire/rédiger/générer/ résumer/traduire/améliorer/note de réunion/texte sur/série…) → POST /api/agent/generate + aperçu « ✓ Appliquer au document / ✕ Rejeter ».
  • Mock : « écrire » (infinitif) reconnu comme verbe de rédaction.

Infra

  • VERSION → 4.13.2 ; app/main.py version → 4.13.2.

v4.13.1 (2026-09-06) — Corrections actions contenu Agent & aperçu à approuver

Corrige les actions de contenu sur document (« Résumer », « Traduire cette page », « Proposer des améliorations », « Créer une note de réunion ») qui partaient en recherche (search_workspace) sans retour utile, et ajoute un flux aperçu des changements → Appliquer / Rejeter.

Fix

  • Le contenu du document n'est plus interprété comme des intentions d'outil : le planificateur (mock) n'analyse que l'objectif de l'utilisateur, jamais le texte injecté dans # Contexte. Un document contenant « recherche », « collection », etc. ne déclenche plus search_workspace / create_collection intempestifs.
  • Ask AI n'emprunte plus le canal outil : il utilise la génération de texte directe.

Added

  • Endpoint POST /api/agent/generate : génération de texte sans outils (l'IA répond à partir du contexte du document fourni, sans chercher/créer dans le workspace).
  • Aperçu & approbation : les actions de contenu sur document affichent la proposition de l'agent dans le panneau avec boutons « ✓ Appliquer au document » / « ✕ Rejeter ». Appliquer insère la proposition (mode insert) ou remplace le contenu de la page (mode replace pour « Traduire cette page ») via window.fdApplyDocument() ; rejeter la conserve sans modification. En mode hors-ligne, l'action explique que la modification nécessite un modèle connecté (pas d'application accidentelle).

Infra

  • VERSION → 4.13.1 ; app/main.py version → 4.13.1.

v4.13.0 (2026-09-06) — Agent : panneau « Notion AI », ouverture fiable (Ctrl+J) & contexte universel

Refonte complète du panneau Agent (style Notion AI) : ouverture/fermeture fiable (API window.fdAgent.open/close/toggle, bouton flottant + sidebar « Agents » + raccourci Ctrl/Cmd+J), contexte détecté et épinglé à l'ouverture (document en cours, bibliothèque, My Tasks, espaces, base de données, board, corbeille…) et sélecteur fournisseur + modèle intégré à la zone de saisie.

Added

  • Ouverture robuste : window.fdAgent.open()/close()/toggle() appliquent l'état directement (classe .open + attribut) ; FAB, entrée « Agents » du sidebar, icône Chat et fdAgent.ask ouvrent explicitement le panneau (fini le toggle silencieux). Raccourci Ctrl/Cmd+J global.
  • Contexte universel window.FlowDeckCtx : détection par priorité — (1) marqueur d'écran (window.fdViewContext / data-fd-context), (2) document ouvert (éditeur : titre + contenu des blocs), (3) route de l'application (Accueil, Bibliothèque, Corbeille, My Tasks, Espaces, Espace local/Gitea, Base /db, Board, Notes, Paramètres…). Le contexte est épinglé à l'ouverture du panneau, affiché en chip (« Contexte : … ») et rafraîchi automatiquement si l'utilisateur ouvre un autre document avant d'envoyer.
  • Fournisseur & modèle affichés : sous-titre du panneau Provider · Modèle (ex. OpenAI · gpt-4o) dès qu'un modèle réel fonctionnel est sélectionné ; « hors-ligne (mock) » n'apparaît que lorsqu'aucun fournisseur actif n'existe. Sélecteurs provider → modèle regroupés dans une pastille « Auto · » au-dessus de la zone de saisie.
  • Actions rapides contextuelles (état vide) en français selon la section : Résumer / Traduire / Améliorer un document, Créer une note de réunion, Analyser une base, Mes tâches / planifier, ou guides « Comment utiliser FlowDeck ? » et création de collection/board.

Changed

  • Panneau redessiné façon Notion AI : largeur 470 px, en-tête « How can I help you today? », icône avatar, ligne de contexte, historique, grande textarea arrondie auto-redimensionnable (jusqu'à ~220 px) avec toolbar (+-équivalent, contexte, modèle, flèche d'envoi) et sélection de modèle en popover.

Infra

  • VERSION → 4.13.0 ; app/main.py version → 4.13.0.

v4.12.1 (2026-09-05) — Corrections configuration Agent & IA + AI Meeting Note

Correctifs UI sur la section Settings → Agent & IA et remplacement de l'ancien bouton « AI meeting note » (qui insérait du texte) par une vraie boîte de réunion interactive.

Fix

  • Champs « Clé API » : le x-text embarquait le SVG de l'icône œil dans l'attribut HTML, ce qui cassait l'attribut et laissait du texte résiduel (sk-…': '👁'" style="top:50%;">). Les boutons d'affichage/masquage utilisent désormais deux <span x-show> avec l'icône en contenu.
  • Champs « URL API » : pré-remplis avec l'URL réellement utilisée par l'application (base par défaut du fournisseur, ex. https://api.openai.com/v1) quand aucune URL personnalisée n'est stockée — exposée via base_url dans /api/agent/providers (fournisseur par défaut + au changement de provider).

Changed

  • AI Meeting Note : le bouton « Get started » ne génère plus du texte via l'agent. Il crée un bloc meeting interactif dans la page (page renommée « Meeting @date » si vide) :
    • Étape 1 — consentement (moi-même / message audio auto) + « Start transcribing » ;
    • Étape 2 — transcription live via Web Speech API (fr-FR, horodatée), boutons Pause / Stop, fallback « sans audio » (transcript à saisir/coller) si le navigateur ne supporte pas ;
    • Étape 3 — onglets Notes / Transcript / Summary, bouton « Générer le résumé » (helper headless window.fdAgent.generate sur le FlowDeck Agent) produisant Résumé / Décisions / Action items, copie presse-papiers et « Nouvelle transcription ».
    • Persistance du bloc et de son état dans la page (champ meeting ajouté aux blocs sauvegardés).

Infra

  • VERSION → 4.12.1 ; app/main.py version → 4.12.1.

v4.12.0 (2026-09-05) — Agent & IA : refonte des réglages, fournisseurs activés/fonctionnels dans le panneau, contexte document & guide

Refonte de la section Settings → Agent & IA pour la rendre plus claire, et aligne le panneau Agent sur la réalité de la configuration : seuls les fournisseurs configurés ET dont le test de connexion a réussi sont proposés (avant, tous les fournisseurs connus apparaissaient, même non configurés). Le panneau sait aussi sur quel document l'utilisateur travaille et peut guider sur l'utilisation de l'application quand aucun document n'est ouvert.

Added

  • Vérification des fournisseurs : colonnes verified / verified_at / last_error sur user_llm_keys et llm_config (migration automatique au démarrage). Un fournisseur est « fonctionnel » après un test de connexion réussi ou un chargement de modèles réussi.
  • Endpoint non-admin POST /api/agent/keys/{provider}/test : teste la clé enregistrée de l'utilisateur (sans mock) et met à jour l'état verified ; l'endpoint admin /providers/test marque le fournisseur par défaut de l'espace.
  • /api/agent/providers enrichi : chaque provider expose configured, verified, functional, last_error et source (user/global/open) ; default_provider + default_verified en tête de réponse.
  • Panneau Agent : les listes fournisseur/modèle ne contiennent plus que les providers activés et fonctionnels (+ mode hors-ligne). Bandeau « Aucun fournisseur activé » avec lien vers les réglages et rappel des clés « à tester » le cas échéant.
  • Contexte document : le panneau joint automatiquement la page ouverte (titre + contenu des blocs) à chaque requête — l'agent peut interagir avec le document courant.
  • Guide d'utilisation : le prompt système intègre un guide FlowDeck concis pour répondre aux questions « comment faire… ? » (pages, collections/vues, Gitea, corbeille, réglages…) lorsqu'aucun document n'est fourni.

Changed

  • Settings → Agent & IA refondu : carte de statut (prêt / N fournisseurs actifs / mode hors-ligne), cartes de fournisseurs avec badges d'état, erreur du dernier test, boutons « Enregistrer & tester », « Tester », « Modèles », « Supprimer » et encart d'aide.
  • Le bouton Save configuration admin gagne un rappel « Tester la connexion » pour activer le fournisseur pour tous ; changer de clé réinitialise la vérification.

Fix

  • Saisie clé : une clé vide conserve désormais l'existante aussi pour la config globale (au lieu de l'effacer), et un changement de clé invalide l'ancien statut verified.

Infra

  • VERSION → 4.12.0 ; app/main.py version → 4.12.0.

v4.11.1 (2026-09-05) — Correction & intégration des points d'entrée IA

Aligne les entrées AI de l'interface sur le FlowDeck Agent (alignement Guide Notion AI) et corrige les trois boutons qui ne fonctionnaient pas : Ask AI, AI meeting note (toolbar « Get started » de l'éditeur de page) et le bouton rond flottant FlowDeck Agent (coin bas-droit).

Fix

  • Bouton flottant Agent 🤖 (coin bas-droit) : le chat restait inerte tant qu'aucune conversation n'existait (send() retournait silencieusement sans currentConv). Ajout d'une création de conversation lazy (ensureConversation()) effectuée avant chaque envoi — la 1ère saisie crée la conversation puis l'exécute (mode offline ou LLM réel).
  • Mode hors-ligne plus utile : le mock LLM (_draft_reply) génère désormais un vrai contenu structuré pour les demandes de rédaction de page (« Ask AI », titre détecté via « intitulée ») et les AI meeting notes (squelette Résumé / Décisions / Action items), au lieu de se contenter de renvoyer le prompt tel quel — les boutons produisent donc du contenu même sans clé API.
  • « Ask AI » & « AI meeting note » : ne faisaient qu'insérer du texte placeholder (« Ask AI to write... » / « AI Meeting Note ») sans aucun appel IA. Ils déclenchent désormais une génération inline réelle via le FlowDeck Agent :
    • méthodes askAI() / meetingNote() / aiRun() / applyAIBlocks() sur l'éditeur de page ;
    • un prompt contextuel est envoyé au panneau agent (exposition d'window.fdAgent.ask) ;
    • la réponse (event final du flux SSE) est insérée automatiquement dans la page sous forme de blocs édités (hook window.__fdInsertAI).
  • Un état aiBusy évite les doubles envois pendant la génération.

Infra

  • VERSION → 4.11.1.

v4.11.0 (2026-09-05) — Agent IA : clés API par utilisateur & chargement dynamique des modèles

Suite de v4.10.1 : migre la config LLM d'un global admin vers des credentials par utilisateur. Chacun peut enregistrer plusieurs provider + clé API dans son profil, les modèles disponibles sont chargés automatiquement depuis le fournisseur, et le provider/modèle actif d'une conversation se pilote directement dans le chat via des commandes slash.

Added

  • Clés par utilisateur : table user_llm_keys (UNIQUE(user_id, provider)) + service app/services/llm_config.py : get_user_llm_key, list_user_llm_keys (masqué), upsert_user_llm_key, delete_user_llm_key. Une clé vide conserve l'existante.
  • Chargement dynamique des modèles : fetch_provider_models() (httpx) — GET {base}/models pour OpenAI-compatible (Bearer), headers x-api-key/anthropic-version (Anthropic), x-goog-api-key (Gemini) ; stockés dans models_json et fusionnés dans GET /providers.
  • Endpoints API (tout utilisateur authentifié) :
    • GET /api/agent/keys, PUT/DELETE /api/agent/keys/{provider},
    • POST /api/agent/keys/{provider}/models (fallback sur la clé stockée, pas de fuite de clé),
    • PATCH /api/agent/conversations/{id} (title / provider / model — commande slash).
  • POST /run : utilise la clé API de l'utilisateur pour le provider de la conversation (sinon config globale / .env / offline mock).
  • UI Settings → Agent & IA (visible pour tous, plus seulement admin) : carte par provider (clé API masquée, URL API, « Charger les modèles » en live, modèle par défaut, Enregistrer / Supprimer). La config globale reste admin sous « Configuration globale ».
  • Commandes slash dans le panel agent : /help, /providers, /keys, /provider <nom>, /model <nom> — menu de suggestions live, appliquées via PATCH et sans appel LLM.

Infra

  • VERSION → 4.11.0 ; migration idempotente (création table user_llm_keys + index).

Tests

  • 239 tests verts (+7 : CRUD clés + masquage des secrets, isolation par utilisateur, provider invalide, fetch modèles (clé stockée / body gagnant / erreur), PATCH conversation, /run utilise la clé de l'utilisateur).

Comble le trou UX de v4.10.0 : l'utilisateur pouvait discuter avec l'agent mais ne pouvait configurer ni provider ni modèle. Ajout d'un sélecteur provider/modèle dans le panneau, d'une config runtime persistée en base (table llm_config) et d'un écran admin avec test de connexion.

Added

  • Sélecteur provider/modèle dans le panneau agent (agent_panel.html) : deux <select> (provider + modèles connus par provider) dans l'en-tête du chat, branchés sur POST /run et persistés par conversation (colonnes provider/model sur agent_conversations, migrées automatiquement). Bouton ⚙️ pointant vers la config admin.
  • Endpoints API :
    • GET /api/agent/providers enrichi : liste complète des providers avec modèles connus, requires_key, api_base et has_api_key.
    • PATCH /api/agent/providers (admin) : persiste provider/modèle/clé/base en runtime.
    • POST /api/agent/providers/test (admin) : test de connexion sans fallback mock (méthode LLMClient.ping()) ; le provider offline répond sans réseau.
  • Table llm_config (1 ligne, id=1) + service app/services/llm_config.py : précédence base (runtime) > .env, créée seulement au premier enregistrement.
  • Écran admin : nouvelle section Agent & IA dans Settings (onglet Admin) : provider, modèle, clé API (masquée / réaffichable), URL API optionnelle, boutons Save configuration et Test connection avec retour d'état inline.

Fixed

  • LLMClient.__init__ : le modèle par défaut suit maintenant la config runtime (DB > .env) au lieu d'être toujours gpt-4o ; PROVIDER_MODELS documente les modèles connus par provider.

Infra

  • VERSION → 4.10.1 ; migration idempotente des colonnes agent_conversations.provider/model.

Tests

  • 232 tests verts (+6 : providers enrichi, PATCH config (admin + 403 non-admin), test connexion offline, test connexion clé manquante, persistance provider/modèle par conversation).

v4.10.0 (2026-09-05) — FlowDeck Agent : agent IA natif conversationnel

Implémentation complète du pilier « Agent IA » : un agent ReAct intégré qui agit sur le workspace (pas seulement répond) — panneau de chat, boucle raisonnement→action, streaming SSE, 18 outils avec snapshots rollback, permissions par rôle, contexte automatique, custom agents, déclencheurs planifiés, skills réutilisables et multi-LLM (offline mock + 8 providers).

Added

  • Interface conversationnelle : panneau agent dans le sidebar (🤖 Agents) piloté par Alpine.js + streaming SSE ; FAB ouvrant le chat ; historique de conversations.
  • Boucle ReAct (app/services/agent_engine.py) : raisonnement → action → observation, max 12 itérations, budget de tokens, timeout, journal d'audit + fonction undo_action() (rollback).
  • Streaming SSE : endpoint /api/agent/conversations/{id}/run qui émet reasoning, action et final en temps réel.
  • 18 outils (app/services/tool_registry.py) : search_workspace, read_collection, read_page, create_collection, create_view, add_property, create_page, update_page, write_blocks, add_relation, create_sub_item, add_dependency, apply_template, delete_page, delete_collection, read_gitea_issues, sync_gitea, create_gitea_issue — chacun avec ToolResult + snapshot d'undo réversible.
  • Permissions (app/services/permission_manager.py) : même ACL que l'utilisateur par rôle workspace (viewer/editor/admin/owner) + gating des outils.
  • Contexte automatique (app/services/context_builder.py) : snapshot du workspace actif, collections, pages, mentions @, fichiers uploadés injectés dans le prompt système.
  • Multi-LLM (app/services/llm_client.py) : OpenAI, Anthropic, Google, Ollama, DeepSeek, QwenCloud, NVIDIA, OpenRouter + offline mock (planner déterministe, aucune clé requise).
  • Custom agents, skills, déclencheurs : CRUD agents + conversations, agent_* tables (agents, agent_conversations, agent_messages, agent_actions, agent_skills, agent_triggers), skills (/api/agent/skills, apply), scheduler de fond pour triggers planifiés.

Infra

  • VERSION → 4.10.0 ; config LLM (LLM_PROVIDER/MODEL/API_KEY/API_BASE, AGENT_MAX_*) ; exclusions CSRF /api/agent ; routeur agent + tâche scheduler dans le lifespan de app/main.py.

Tests

  • 226 tests verts (+18 v4.10.0 : schéma DB, mock LLM, permissions, outils+undo, engine ReAct + audit + rollback, CRUD agents/conversations, streaming SSE, skills, tools, providers).

v4.9.0 (2026-09-04) — Collaboration : commentaires inline, mentions @, notifications

Implémentation complète du pilier « Collaboration » — basée sur la revue des docs Notion (docs/Guide_Complet_Notion_sharing_collaborartion.md) : commentaires avec ancres inline, mentions @ avec autocomplétion, centre de notifications in-app et emails SMTP.

Added

  • Commentaires inline sur les pages : sélection d'un texte dans l'éditeur → bouton flottant « 💬 Comment » → commentaire ancré (block_id + offsets). Table comments étendue (target_type, target_id, anchor_block_id, anchor_start, anchor_end) et migrée de son FK collection_pages vers un schéma générique (aucune perte de données, idempotente).
  • Panneau de commentaires dans l'éditeur : liste, résolution (✔/↪), suppression, compteur dans la topbar.
  • Mentions @ : autocomplétion en temps réel (recherche d'utilisateurs), insertion de @login dans un bloc ou un commentaire → notification ciblée à l'utilisateur mentionné.
  • Centre de notifications in-app : cloche dans la topbar (badge non-lus, polling 30s), panneau déroulant, « mark as read » / « mark all read ». Table notifications.
  • Emails de notification : service SMTP (app/services/mailer.py) + templates HTML ; préférences email par utilisateur (comments/mentions) réglables dans Settings → Notifications. Sans SMTP configuré, seules les notifications in-app sont émises (aucune erreur).
  • API : GET/POST /api/notifications, /api/notifications/read|read-all|prefs, /api/notifications/users/search, GET/POST /api/pages/{id}/comments, PUT/DELETE /api/comments/{id}, POST /api/pages/{id}/mentions.
  • Config SMTP (SMTP_HOST/PORT/USER/PASSWORD/FROM/USE_TLS, APP_BASE_URL) dans .env.example.

Infra

  • VERSION → 4.9.0 ; app/main.py (log + version=) ; router notifications + collaboration enregistrés ; exclusions CSRF (/api/notifications, /api/comments) ; le RateLimitMiddleware respecte désormais settings.rate_limit_enabled.

Tests

  • 208 tests verts (+9 v4.9.0 : table notifications, commentaires inline + mentions, centre de notifications, préférences, recherche utilisateurs, résolution/suppression, mentions de page).

Fixed

  • Bouton « + Row » (bas du tableau) inopérant — le code passait la nouvelle ligne en argument deleteCount de Array.splice au lieu d'utiliser splice(index, 0, ligne) : aucune ligne n'était jamais insérée. Corrigé ; les insertions de lignes (menu contextuel aussi) fonctionnent.
  • Coloration/handles alignés sur l'en-tête.

Added

  • Bouton « + » à droite du tableau (sur toute la hauteur) → ajoute une colonne à la droite (équivalent d'un « Add column » comme « Add Row »).
  • Redimensionnement des colonnes à la souris : au survol d'une bordure de colonne (en-tête) le curseur devient col-resize ; glisser ajuste la largeur. Les largeurs sont persistées dans le bloc (colsW) → conservées au rechargement, à l'ajout/suppression et dans l'auto-save.

Infra

  • VERSION → 4.8.1 ; cache CSS app.css?v=4.8.1.

v4.8.0 (2026-09-03) — Bloc Tableau simple (mode édition Notion)

Insertion via /table, cellules éditables au clic, ajout/suppression/modification de lignes et colonnes comme dans Notion.

Added

  • Bloc table dans l'éditeur de page. Nouvelles commandes slash /table (menu « BASIC ») → tableau 3×3 avec en-tête ; première ligne stylée en en-tête, cellules contenteditable éditées au clic, auto-sauvegarde (format de bloc conservé : {type:'table', rows:[...]}).
  • Gestion des colonnes : bouton de poignée (⋮⋮) au-dessus de chaque colonne → menu contextuel Insert left / Insert right / Duplicate / Clear contents / Delete column / Delete table.
  • Gestion des lignes : bouton « + Row » sous le tableau (clic = ajouter une ligne) + menu contextuel (clic droit sur une cellule de corps) → Insert above / Insert below / Duplicate / Clear row / Delete row / Delete table.
  • Import Markdown (GFM) : md2b détecte les tableaux pipe (| a | b | + ligne de séparateurs) et produit un bloc table éditable au lieu de paragraphes aplatis (pages .md/markdown).
  • Roundtrip Markdown : blocksToMarkdown (JS) et l'export Python reconstruisent un tableau pipe valide depuis les blocs.
  • Export : les pages contenant un bloc table sortent correctement en Markdown / HTML / PDF (géré côté serveur).
  • UX : cliquer sous un tableau en fin de page ajoute un paragraphe pour continuer la rédaction.

Infra

  • VERSION → 4.8.0 ; app/main.py (log + version=) ; cache CSS app.css?v=4.8.0 ; ROADMAP renuméroté (v4.8.0 Bloc Tableau, Collaboration → v4.9.0, Agent → v4.10.0).

Tests

  • Suite pytest verte (199) ; vérification syntaxe JS du template via node --check.

v4.7.3 (2026-09-03) — Fix export PDF/HTML : tableaux + émojis

Fixed

  • Tableaux Markdown rendus comme du texte brut dans les exports HTML/PDF (les lignes | a | b | ressortaient en paragraphes). Ajout d'un parseur GFM de tableaux (_md_to_blocks → bloc table) et d'un rendu en vrai <table> (en-tête <th>, corps <td>, alignement gauche/centre/droite, bordures fines via .ftable). Le format Markdown reconstruit un tableau pipe valide.
  • Émojis en « carrés noirs » dans le PDF. Cause : le moteur xhtml2pdf (reportlab) n'embarque que des polices de base sans glyphes Unicode/émojis. Le moteur PDF passe à WeasyPrint (vraies tables CSS + émojis couleur via Pango + fonts-noto-color-emoji, installés dans l'image Docker). Repli automatique sur xhtml2pdf si WeasyPrint n'a pas ses libs natives (ex. poste de dev Windows) — le contenu s'exporte quand même.

Infra

  • Dockerfile : ajout des libs WeasyPrint (pango/harfbuzz/gdk-pixbuf/shared-mime-info) + polices fonts-dejavu-core et fonts-noto-color-emoji.
  • requirements.txt : + weasyprint==69.0 (xhtml2pdf conservé en repli).

Tests

  • 199 tests (4 nouveaux) : parse table → <table> HTML réel + alignements ; standalone HTML embarque .ftable ; roundtrip table → markdown pipe ; PDF d'une page avec table = PDF valide.
  • Vérifié en réel sur README.md : PDF avec tableau structuré + émojis en couleur (pixels colorés confirmés), HTML avec tableau bordé.

v4.7.2 (2026-09-03) — Fix export : le contenu des documents était absent

Fixed

  • Export Markdown / HTML / PDF : seuls le nom de page et le titre étaient inclus, jamais le contenu. Cause : le service d'export ne lisait que les pages en content_format='blocks'. Or les documents peuvent être stockés autrement :
    • content_format='file' (fichiers .md/code uploadés) : le content de la page n'est que des métadonnées JSON — le vrai texte est un fichier sur disque (/data/uploads/workspace_*). L'export n'allait jamais le lire → uniquement le titre.
    • content_format='markdown' : le HTML/PDF se contentait d'envelopper chaque ligne en <p> (titres et listes aplatis).
  • Le service résout désormais la vraie source pour les 3 formats : lit le fichier sur disque pour les pages file, rend les pages markdown/fichiers .md en blocs (headings, listes, code…), et traite les fichiers texte non-markdown comme bloc de code. Les fichiers binaires (PDF/images) sont laissés de côté (rien d'exploitable).
  • Chemin des uploads rendu configurable pour les tests via FLOWDECK_DATA_DIR (défaut /data, identique à dashboard.py).

Tests

  • 195 tests au total (5 nouveaux v4.7.2) : page markdown → corps exporté ; page markdown → HTML avec headings/listes ; fichier .md uploadé → contenu réel dans Markdown/HTML/PDF ; fichier code → exporté ; fichier binaire → non déversé.
  • Vérifié en réel sur /data (page README.md et arborescence « Base de Connaissances » → fichiers .md) : le corps complet est maintenant présent dans les 3 formats.

v4.7.1 (2026-09-03) — Fix UI éditeur : menus Share / More invisibles

Fixed

  • Boutons « Share » et « ⋯ » (More) sans réaction visible dans l'éditeur de page — les popovers (share-dialog, more-menu, activity-popover, move-dialog), enfants de .page-editor-wrapper (conteneur à overflow-y:auto), étaient ancrés en absolute; top:100% → rendus SOUS le viewport, donc invisibles. Repositionnés en position:fixed sous la topbar (scoped .page-editor-wrapper > … pour ne pas casser les .more-menu des pages library/local_workspace) + variantes mobile dans le media query < 768px.
  • Sous-menu Export se refermait — l'itém « ↗ Export » faisait moreOpen=false avant d'ouvrir exportOpen : les 4 formats restaient inaccessibles au clic. Devient un toggle (exportOpen = !exportOpen), le menu More reste ouvert.
  • Cache CSS — version du query string app.css bumpée (v=3.0.0 → v=4.7.1) pour invalider le cache navigateur.

Vérifié

  • Playwright headless (desktop 1280×800) : Share, More, sous-menu Export, popover Activity et menu More après scroll = tous visibles dans le viewport ; téléchargement Markdown + toast « Exported as Markdown » OK ; zéro pageerror.
  • 190/190 tests pytest passent.

v4.7.0 (2026-09-03) — Export

Added

  • Export Markdown — conversion serveur complète de tous les blocs (headings, listes, to-do, quote, code, math, toggle, colonnes, callout, image), images incluses
  • Export Markdown récursif — les sous-pages sont incluses automatiquement
  • Export PDF — via xhtml2pdf (pur Python, aucun lib système), rendu print-friendly
  • Export HTML — document autonome self-contained (styles inline)
  • Export Site — site statique multi-pages en .zip (index.html + une page HTML par sous-page)
  • 4 endpoints serveur : /api/export/markdown|html|pdf|site/{page_id}
  • UI : menu « More › Export » dans l'éditeur (Markdown, HTML, PDF, Site .zip)

Améliorations

  • Nouveau service app/services/export.py (conversion blocs → Markdown / HTML / PDF réutilisable)
  • Nouveau routeur app/routers/export.py
  • Téléchargements via Content-Disposition (avec nom de fichier encodé UTF-8)

Tests

  • 190 tests au total (6 nouveaux tests v4.7.0) : Markdown, sous-pages, HTML, PDF, Site zip, 404

v4.6.0 (2026-09-02) — Content Blocks Enrichis

Added

  • Callout blocks — boîtes colorées avec sélecteur d'emoji/icône cliquable
  • Table of contents — bloc auto-généré depuis les headings, avec ancres de navigation
  • Math equations — bloc LaTeX/KaTeX avec aperçu en temps réel (édition inline)
  • table_of_contents et math — nouveaux types dans le slash menu (éditeur)
  • Multi-colonnes — bloc columns avec layout flexible, bouton "+ Add column" / retrait
  • Toggle lists enrichis — contenu expandable/collapsible avec blocs enfants imbriqués

Améliorations

  • Intégration KaTeX 0.16.11 self-hosté (/static/js/katex.min.js, /static/css/katex.min.css, /static/fonts/) — aucun CDN externe
  • Rendu des nouveaux blocs dans les pages publiques (/p/<slug>): TOC ancré, KaTeX, colonnes flex, toggle <details>
  • Export Markdown étendu aux nouveaux types de blocs
  • Correction d'un bug pré-existant : public_page.html n'importait pas la macro fd_icon → erreur de rendu des pages publiées

Tests

  • 183 tests au total (5 nouveaux tests v4.6.0) couvrant le rendu public TOC/math/colonnes/toggle et la persistance children

v2.7.1 (2026-07-13) — Private Pages Gitea

Added

  • Table gitea_private_pages: pages FlowDeck locales liées à un projet Gitea (user_id, owner, repo)
  • API CRUD: GET/POST/PUT/DELETE /api/gitea/projects/{owner}/{repo}/private-pages
  • UI: liste des pages privées + éditeur titre/contenu dans le workspace Gitea
  • Sidebar: lien "Private Pages" dans l'arborescence du workspace
  • Persistance: les pages survivent au changement de workspace / session

Fixed

  • get_user_repos(limit=100) — bruno/flowdeck était tronqué à la page 2 (30 repos par défaut)

v2.7.0 (2026-07-13) — Intégration Gitea Phase 1

Added

  • OAuth2 Provider: Gitea comme fournisseur OAuth2 pour FlowDeck (Client ID/Secret dans .env)
  • Redirect URI dynamique: basé sur le header Host — fonctionne depuis localhost et 192.168.x.x
  • Workspace listing: 2 sections — My Workspaces + Gitea Projects (groupés par organisation)
  • Tabs par org: All | user | org1 | org2 avec compteurs de projets
  • Barre de recherche: filtrage live par nom/description dans Gitea Projects
  • Lazy tree: arborescence chargée un dossier à la fois (API Gitea GET /repos/{o}/{r}/contents)
  • File viewer: lecture fichiers Gitea avec décodage base64
  • Éditeur + commit: modifier un fichier et commiter dans Gitea avec message personnalisé
  • Delete fichier: suppression avec confirmation + commit
  • Arborescence dans le sidebar: comme un workspace local, avec lazy-load des sous-dossiers
  • Settings › Integrations: connecter/déconnecter Gitea (GET /api/gitea/status, DELETE /api/gitea/disconnect)
  • Fallback token admin: opérations GET utilisent le token serveur si pas de token OAuth utilisateur
  • Write ops protégées: commit et delete exigent un token OAuth utilisateur
  • Routes API: /api/gitea/orgs, /api/gitea/projects, /api/gitea/projects/{o}/{r}/tree, /api/gitea/projects/{o}/{r}/file, /api/gitea/projects/{o}/{r}/labels

Fixed

  • Bouton "Register with Gitea" ne crée plus de session admin fantôme (fallback retiré)
  • _sidebar_data gère le cookie gitea:owner:repo pour l'arborescence

v2.6.0 (2026-07-13) — My Account & Auth Locale

Added

  • Section My Account: modifier username, full name, email, mot de passe
  • Toggle mot de passe: bouton voir/cacher dans les formulaires login/register
  • Label "Email or username": login accepte les deux formats
  • Refresh cookie session: après modification du compte, le cookie est mis à jour
  • Register avec Gitea: boutons dynamiques "Login/Register with Gitea" selon l'onglet actif

Fixed

  • Session cookie non rafraîchi après PUT /api/settings/account (création nouvelle session)
  • Input login type="email" → type="text" (accepte usernames non-email)

v2.5.0 (2026-07-13) — Administration Avancée

Added

  • Premier compte = admin automatique: is_admin=1 pour le premier utilisateur créé
  • Panneau Admin dans Settings: visible uniquement pour les admins
  • Users & Roles: créer/modifier/supprimer utilisateurs, changer rôles
  • Table login_history: audit des connexions (IP, user_agent, timestamp)
  • Statistiques par utilisateur: workspaces, pages, dernière connexion
  • API /api/admin/*: users, stats, audit — protégées par admin_required
  • CSRF fix: token CSRF dans toutes les requêtes admin (adminFetch)

v2.4.0 (2026-07-13) — Tags Personnalisés par Utilisateur

Added

  • Tags isolés par user_id: chaque utilisateur voit uniquement ses propres tags
  • UNIQUE(name, user_id): remplace UNIQUE(name) dans la table tags
  • Migration: tags existants assignés au premier utilisateur
  • Filtrage transparent: l'API retourne les tags filtrés par user_id (session cookie)

Fixed

  • Affichage des tags dans le menu contextuel workspace (sync Alpine.js)
  • Création de tag avec couleur persistée
  • Fermeture auto du menu contextuel après ajout de tag
  • Filtrage par tags: toggleTagFilter() appelle doFilter()
  • Pastilles tags: fond = couleur du tag, sans rond séparé

v2.3.0 (2026-07-12) — Multi-Vues Workspace & Preview Panel

Added

  • Sélecteur de vue: Tree, List, Details, Title, Content — 5 modes d'affichage
  • Vue List: liste compacte avec Name, Type, Size, Modified
  • Vue Details: table complète avec Path, Author, Tags
  • Vue Title: grille de cartes avec icônes et métadonnées
  • Vue Content: liste avec aperçu inline (taille ou nombre d'items)
  • Preview Panel: panneau latéral droit avec contenu du fichier sélectionné
    • Header avec icône + titre + bouton fermer
    • Métadonnées (type, taille, date)
    • Tags affichés en chips
    • Corps avec rendu du contenu (blocks JSON → HTML, texte brut)
    • Actions: Open, Rename, Delete
    • Animation slide-in/out
  • Tri par date: options Oldest/Newest dans le selecteur de tri

v2.2.0 (2026-07-12) — Tags & Recherche Avancée

Added

  • Tags: système complet de tags avec tables tags + page_tags, CRUD API
  • API Tags: GET/POST/DELETE /api/local-workspace/items/{id}/tags, GET /api/local-workspace/tags
  • Vue Table enrichie: colonnes Name, Path, Size, Modified, Type, Author, Tags
  • Filtrage par tags: barre de chips avec compteurs, clic pour filtrer
  • Ajout/retrait tags inline: bouton + dans la table, chips avec × pour retirer
  • Tree endpoint enrichi: chaque nœud retourne size, size_display, created_at, updated_at, author, tags
  • Filtre "Dossiers": nouveau chip Folders dans la barre de filtres
  • Auto-switch table: quand recherche/filtre actif, passe en vue table automatiquement

v2.1.1 (2026-07-12) — Fix Arborescence Workspace

Fixed

  • Arborescence: création fichier/dossier dans un sous-dossier met à jour l'arbre sans rechargement
  • parentFolder: corrigé le bug où parentFolder était nullifié avant l'insertion dans l'arbre JS
  • displayTree: nouvelle référence array après chaque mutation pour forcer le re-render Alpine.js
  • doSort/doFilter: gèrent maintenant le cas où un filtre/tri est actif après création

v2.1.0 (2026-07-10) — Intégrations Avancées

Added

  • API publique: /api/v1 (collections, pages, my-tasks) avec token auth
  • Token generation: POST /api/v1/token
  • Webhooks sortants: GET/POST/DELETE /workspace/webhooks + dispatcher
  • PWA: GET /manifest.json
  • CSRF: /api/v1 exempté

Tests

  • 73/73 passent (+6 tests v2.1)

v2.0.0 (2026-07-10) — Multi-User Workspaces & Editor Complete

Added

  • Workspaces: tables workspaces + workspace_members, CRUD API, roles admin/editor/commenter/viewer
  • Comments: table comments avec thread (parent_id), resolved
  • Page History: table page_history, snapshot JSON
  • Favorites: table favorites, add/remove/list
  • Templates: database_templates + page_templates, apply API
  • CSV Import/Export: POST .../import/csv, GET .../export/csv
  • Public Sharing: GET /workspace/public/{id} (read-only, no auth)

Tests

  • 67/67 passent (+7 tests v2.0)

v1.9.0 (2026-07-10) — My Tasks Dashboard

Added

  • My Tasks: GET /my-tasks (HTML), GET /my-tasks/api (JSON)
  • Agrégation cross-collection, vues All/Today/Overdue/Next 7 days

v1.8.0 (2026-07-10) — Sub-items & Dependencies

Added

  • Sub-items: parent_id auto-référence, GET/POST sub-items
  • Status aggregate: GET .../status-aggregate
  • Dependencies: POST .../dependencies, POST .../check-deps

v1.7.0 (2026-07-10) — Vues Améliorées

Added

  • View config: PUT /db/views/{id}/config (group_by, card_size, cover, visible)
  • Save view as: POST /db/{id}/views/save-as
  • List views: GET /db/{id}/views/api

v1.6.0 (2026-07-10) — Vues Manquantes

Added

  • Calendar: grid mensuel, navigation mois
  • Gallery: cartes visuelles, card_size configurable
  • List: vue compacte avec preview
  • Timeline: barres horizontales Gantt
  • Table: rendu SSR avec colonnes properties
  • View tabs: navigation Table/Board/Calendar/Gallery/List/Timeline

v1.5.0 (2026-07-10) — Relations & Rollups

Added

  • FormulaEngine: 19 fonctions (prop, if, concat, round, now, today, dateAdd, replace, ...)
  • RollupEngine: 12 agrégations (count, sum, avg, min, max, range, unique, percent_checked)
  • Relation: lien bidirectionnel, API create/link

v1.4.0 (2026-07-10) — Propriétés Avancées

Added

  • Table collection_properties: 21 types Notion
  • Service property_types.py: validation, formatage, auto-values

v1.3.0 (2026-07-10) — Database Concept

Added

  • Tables: collections, collection_pages, collection_views
  • Router /db: CRUD collections + pages (16 endpoints)
  • GiteaBoardCompat: adaptateur boards legacy → Collections

v1.0.0 (2026-07-08) — Production

Added

  • FastAPI lifespan, 28 tests, CI/CD Gitea Actions, CSS 31KB, 10 templates
  • 5 vues: Kanban, Table, Status Overview, Team Load, Detailed Board
  • Filtres cumulables AND, tri multi-critères
  • Card detail modal, checklists, commentaires Gitea
  • Propriétés custom, AI keywords, sync API