# Changelog - FlowDeck ## 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 `
` 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:` 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/` +
  `GET /s//` (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//auth`), expiry (410), `noindex`, OG/Twitter cards,
  `GET /s//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/`
  (formulaire no-auth, `?embed=1` sans chrome) + `POST /f/` (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 `

`) + é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/`). - **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/` / 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 `), `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). ## v5.12.0 (2026-09-14) — v5.11.0 Wiki-links & v5.12.0 Templates & lock > 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). ### v5.11.0 — Wiki-links & mentions de page - **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 `` 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/` (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-`, 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 `
`) 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 `
` : les sauts de ligne étaient perdus dès la sauvegarde du bloc. Ajout de `gt()` (basé sur `innerText`, qui rend bien les `
` 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 `
` ; `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 `
` (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 `
` 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": ""` 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:`/`page:` → `/pages/`, `collection:` → `/db/`. 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 `` 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 `, `/model ` — 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 `