feat: v7.3.0 — cycle v6.8.0→v7.3.0 (Sites, Search, Automations, Calendar, SCIM, Wiki) + audit A9
FlowDeck CI / lint (push) Successful in 1m48s
FlowDeck CI / test (push) Failing after 21m19s
FlowDeck CI / docker (push) Skipped

- v6.8.0 Sites & Forms publics (migrations 24)
- v6.9.0 Recherche sémantique hybride + Ask AI (migration 25)
- v7.0.0 Automations v2 multi-étapes + Workers sandboxés (migration 26)
- v7.1.0 Calendar sync Google/CalDAV + Meeting Notes (migration 27)
- v7.2.0 Enterprise : SCIM 2.0, 2FA TOTP/passkeys, audit UI, agent approvals (migration 28)
- v7.3.0 Wiki/Teamspaces, verified pages, collab polish, charts, unfurl (migration 29)
- docs V68→V73, ROADMAP/CHANGELOG/WORKLOAD à jour, VERSION 7.3.0
- A9 : flowdeck.db, flowdeck_dev.db, test-commit.md, upload_test.txt et e2e/{node_modules,shots,test-results} désindexés + ignorés (.gitignore/.dockerignore)
This commit is contained in:
2026-09-30 20:02:57 -04:00
parent d074689b18
commit 1706ad1ee9
260 changed files with 10667 additions and 353788 deletions
+286
View File
@@ -1,5 +1,291 @@
# Changelog - FlowDeck
## v7.3.0 (2026-09-29) — Wiki / Teamspaces + Polish (dernière version du cycle v7)
> Connaissance vérifiée et finition collaborative : teamspaces, badge ✅ avec
> expiration, guests sans compte, réactions, follows, analytics de page et
> trois nouveaux blocs rendus côté serveur. Design : `docs/V73_Wiki_Teamspaces_Polish.md`.
### Added
- **Teamspaces** — `app/services/wiki.py` + `app/routers/wiki.py` : `teamspaces`
(`workspace_id`, `private`, `UNIQUE(workspace_id, name)`) et
`teamspace_members` (rôles `owner`/`editor`/`commenter`/`viewer`) ; CRUD +
listing par workspace, ajout/retour de membres ; `private=1` → **404** (pas
403) pour les non-membres, comme les collections restricted ; un teamspace
public reste cantonné au workspace (pas de `viewer` implicite pour un compte
qui n'en est pas membre) ; `pages.teamspace_id` + `collections.teamspace_id`
- **Verified pages** — `page_verifications` (badge ✅, `verified_by`, `note`,
`expires_at` 90 j par défaut, re-vérifier remplace) ; l'index
`GET /api/v2/wiki/verified` exclut les badges expirés et les pages de
teamspaces privés ; sweep `POST /api/v2/wiki/verify-expiry-sweep` (admin)
notifie le vérificateur à J-7 (`page.verification_expiring`) ; la
vérification exige un rôle editor/owner/admin, sinon 403
- **Guests sans compte** — `guest_shares` (`token`, `role viewer|commenter`,
`expires_at`, `revoked`) ; accès par `GET /g/{token}` **sans session**,
enregistrement d'une vue, révocation idempotente ; page 404 HTML dédiée au
lieu d'une redirection vers `/workspaces`
- **Collab polish** — `comment_reactions` (agrégation par emoji + users,
toggle), `page_follows` (toggle + followers), `page_views` (compteurs
journaliers, séries sans trou via `view_stats`)
- **Wiki Home** — `GET /api/v2/wiki/home` (teamspaces + verified + recents)
- **Blocs** — `app/services/wiki_blocks.py` : `mermaid` (SVG inline si
`mmdc` est installé, sinon `<pre class="mermaid">` rendu côté client),
`equation_inline` (KaTeX, source sanitizée : `<`, `>`, `\` retirés pour
empêcher la fermeture anticipée du délimiteur ou l'injection de balises),
`progress` (agrégation directe sur `property_values_json` → barre %) ; les
trois sont rendues par `export.blocks_to_html` (donc présentes dans
l'export HTML/PDF) et exposées via `POST /api/v2/wiki/blocks/preview`
- **Migration 29** — `teamspaces`, `teamspace_members`, `page_verifications`,
`comment_reactions`, `page_follows`, `guest_shares`, `page_views` +
colonnes `teamspace_id` sur `pages`/`collections`
- **Sidebar teamspaces** — section `Teamspaces` dans `base.html` (état Alpine
`teamspaces`/`loadTeamspaces`/`openTeamspace`, section ouverte par défaut,
icône/label/ordre, fallback du panneau de personnalisation) →
`GET /api/v2/wiki/teamspaces` accepte désormais l'omission de `workspace_id`
(listing cross-workspace avec `workspace_name`), page HTML
`GET /wiki/teamspaces/{id}` (pages + collections du teamspace, rôle affiché) ;
entrée `teamspaces` dans `sidebar_config.DEFAULT_CONFIG`
- **Notif `page.updated` aux followers** — `wiki.notify_followers_of_page_update`
(une par 10 min, `actor_id` exclu) branchée dans `automations.fire_event` ;
payloads `actor_id` ajoutés dans `board.update_page` et `board.save_page_blocks` ;
commenter une page = suivre (auto-follow `wiki.ensure_follow`, défaut ON)
- **Charts avancés** — type `number` (KPI) avec agrégats `count|sum|avg|min|max`
dans `collections._render_chart` (+ `_chart_aggregate`/`_fmt_number`) ; le « 0 »
n'est plus forcé à 1 ; les dashboards multi-DB se rendent via
`GET /db/{collection_id}/dashboards/{dashboard_id}` (widgets `collection_id`,
≤ 40 widgets, ≤ 200 lignes/chart) ; `view_collection` choisit maintenant la
config de vue correspondant au `view_type` demandé
- **Unfurl `gitea:`/`github:`** — `POST /board/api/og/metadata` résout les refs
`gitea:owner/repo` / `github:owner/repo` via `GiteaClient.get_repo_info`
(ajouté) ou `GitHubAdapter` (token optionnel, sinon API publique) sans
télécharger la page ; champs bookmark `url/title/description/image/site_name`
conservés dans l'autosave du bloc
- **UI Settings → Audit** — `settings.html` : l'onglet `admin-audit` interroge
désormais `/api/v2/audit/logs` (sources `api`/`permissions`/`sso`, filtres
`actor`/`action`, pagination « Load more », export CSV)
- **SSO 21 casses** — dépendances `python3-saml==1.16.0` + `authlib==1.8.0`
(plus `xmlsec`/`isodate`/`joserfc`) installées → `test_v67_sso.py` **38/38**
- **Sécurité** — `sanitize_equation` retire désormais `<`/`>`/`\` (pas de
breakout KaTeX/markup), `revoke_guest` 404 fondé sur l'existence, pas le
rowcount
### Tests
- `tests/test_v73_wiki_polish.py` : **58 → 72 tests** (sidebar cross-workspace +
page teamspace owner/outsider, auto-follow par commentaire, notif
`page.updated` throttlée + acteur exclu, `ensure_follow` idempotent, regex +
unfurl gitea/github + endpoint `/board/api/og/metadata`, KPI + `_chart_values`
(0 conservé) + dashboards multi-DB + 404)
- `ruff check app tests` OK · suite complète `python -m pytest -n auto` :
**1016 passed** (était : 981 passed / 21 failed en SSO avant install des deps)
---
## v7.2.0 (2026-09-29) — Enterprise admin : SCIM 2.0, 2FA, Audit, gouvernance agents
> Le socle administration d'une instance auto-hébergée en équipe : provisionnement
> SCIM depuis l'IdP, TOTP + passkeys, journal d'audit unifié et garde-fous
> d'exécution pour les agents. Design : `docs/V72_Enterprise_SCIM_2FA.md`.
### Added
- **SCIM 2.0** — `app/routers/scim.py` : `GET/POST /scim/v2/Users`,
`GET/PUT/PATCH/DELETE /scim/v2/Users/{id}` (Bearer `scim_tokens`, schémas
core:2.0:User, `active` → `users.is_active` + révocation `user_sessions`,
`Operations` PATCH `active`/`userName`) ; tokens SHA-256 stockés, affichés
une seule fois, révocables (`/api/v2/scim/tokens`) ; exempté CSRF (clients
IdP sans cookie) et 404 `application/scim+json` au lieu d'une redirection
- **TOTP 2FA** — `app/services/two_factor.py` : secret chiffré Fernet au repos,
10 codes de secours à usage unique (SHA-256), Défi `pending` signé 5 min ;
`POST /auth/local-login` renvoie `2fa_required` sans créer de session, puis
`POST /auth/local-verify` l'échange contre une session ; setup/activate/
disable/status côté utilisateur
- **Passkeys WebAuthn** — `app/routers/webauthn.py` : enregistrement
(`register/begin|finish`, attestation vérifiée, COSE stocké) et connexion
**sans mot de passe** (`login/begin|finish`, anti-rejeu `sign_count`,
vérif. origine/RP) ; listing + suppression des clés
- **Domain claims** — `POST /api/v2/domain-claims` (normalisation lowercase,
jeton `.well-known/flowdeck-verify.txt`), `POST .../verify` (fetch HTTPS du
domaine + comparaison), `enforce_sso` qui bloque le login local par domaine
dans `auth.local_login` (les admins gardent l'accès local) ; jeton jamais
renvoyé par le listing
- **Audit unifié** — `app/routers/audit.py` : `GET /api/v2/audit/logs` fusionne
`api_audit_log` + `permission_audit_log` + `sso_login_history` en un schéma
commun, filtres `source`/`actor`/`action`, pagination, export
`?format=csv` (admin ou Bearer `read:admin`)
- **Gouvernance des agents** — `app/services/agent_policies.py` + `app/routers/governance.py` :
`agent_policies` (liste d'outils autorisés par workspace, `max_steps`,
`require_approval`) consultée par `AgentEngine` **avant** les ACL, file
`agent_approvals` pour les écritures, décision admin
(`/api/v2/agent-approvals/{id}/decide`) + événement
`agent.run.approval_requested`
- **Migration 28** — `scim_tokens`, `domain_claims`, `webauthn_credentials`,
`agent_policies`, `agent_approvals` + `users.totp_secret_enc`,
`users.totp_backup_hashes`
### Fixed
- Les 404 sur `/scim/v2` et `/auth/webauthn` renvoyaient une redirection 302
vers `/workspaces` (handler global) : ils retournent désormais du JSON, et
`/scim/v2` répond en `application/scim+json`
- `/scim/v2` et les routes 2FA/WebAuthn ajoutées à la liste d'exclusion CSRF
(authentification Bearer, pas de cookie de session)
### Tests
- `tests/test_v72_enterprise.py` : **52 tests** (migration 28, SCIM tokens/CRUD/
suspend/duplicate/404, 2FA setup-activate-verify-backup-chiffrement-désactivation,
challenges, domain claims, WebAuthn, audit multi-source + filtres + CSV +
pagination, politiques et gate d'approbation)
- `ruff check app tests` OK · `tests/test_agent.py` + `tests/test_app.py` :
**261 verts** (la gouvernance ne casse pas l'engine)
---
## v7.1.0 (2026-09-28) — Calendar sync + Meeting Notes
> Calendrier bidirectionnel Google/CalDAV + transcription → résumé IA qui
> déclenche les agents (pattern Notion 07/2026). Design : `docs/V71_Calendar_Meetings.md`.
### Added
- **Sync bidirectionnelle** — `app/services/calendar_sync.py` : `calendar_links`
(tokens Fernet, `collection_id`, `date_property`), pull (event → ligne datée +
`external_event_id`) + push, boucle 15 min dans le lifespan ; conflits
(édité des 2 côtés → last-write-wins + notif `calendar.conflict`) ; lignes
touchées par le pull jamais repoussées ; API CRUD + `POST .../sync`
(`app/routers/meetings.py`, session ou Bearer `write`, tokens jamais leakés)
- **Sans dépendance** — Google Calendar REST (401 → « relink »), CalDAV brut
(REPORT + parseur multistatus, PUT) ; I/O module-level monkeypatchables
- **Meeting Notes v2** — `app/services/meetings.py` : upload audio 100 MB
(mp3/wav/m4a/ogg/flac/aac), transcription `STT_COMMAND` ou transcript manuel,
résumé `ai_writing.summarize` (offline-capable) → trigger `meeting.summarized`
(branché automations v7.0) ; endpoints upload/texte/summarize
- **Free/busy** — `GET /db/{id}/calendar/freebusy` (busy + free weekdays,
récurrences expandues, 1..370 j)
- **Migration 27** — `calendar_links`, `meeting_transcripts`,
`collection_pages.external_event_id` + index
### Tests
- `tests/test_v71_calendar_meetings.py` : **15 tests** (migration, links,
pull/push/idempotence/conflit + notif, 502, CalDAV, freebusy, meetings ×5)
- `ruff check app tests` OK · suite **871 verts** (21 échecs `test_v67_sso.py`
pré-existants — `onelogin` absent de l'environnement)
---
## v7.0.0 (2026-09-28) — Automations v2 + Workers lite
> Du if-this-then-that aux chaînes multi-triggers + custom code sandboxé,
> parité Notion Automations + Workers. Design : `docs/V70_Automations_Workers.md`.
### Added
- **Automations multi-étapes** — `automation_steps` (trigger/condition/delay/action
ordonnés) : CRUD `GET/POST /workspace/automations/{id}/steps`,
`PUT/DELETE /workspace/automations/steps/{id}` (session, validation serveur),
`PUT .../mode` (`any` défaut / `all` fenêtre 5 min) ; conditions AND réutilisant
`match_condition_props` ; `form.submitted` déclenchable ; legacy sans steps intact
(matcher legacy ignore les automatisations à steps → pas de double run)
- **Nouvelles actions** — `slack` (incoming webhook, URL chiffrée Fernet au repos,
décryptée à l'exécution), `email` (`user:<id>` résolu ou reply-to créateur,
repli propre sans SMTP), `forge_issue` (Gitea via `GiteaClient` / GitHub API,
token `user_oauth_tokens`), `agent_trigger` (conversation + run `AgentEngine`),
`delay` (0..86400s validé, sleep plafonné 300s) ; interpolation `[[prop]]` /
`{{title}}` conservée ; backends module-level = monkeypatchables
- **Bouton DB natif** — type `button` (`PROPERTY_TYPES`), `button_automation_id`,
`POST /api/automations/press-button` (CSRF-exempt, 400 explicites)
- **Workers lite** — `app/services/workers.py` (lint AST : imports réseau/OS,
`open/exec/eval`, dunders ; builtins restreints ; thread + timeout 30s ;
budget journalier ; fork des partagés) + `app/routers/workers.py`
(CRUD `/api/v2/workers*` session/Bearer `write`, code masqué aux non-owners,
run/runs/fork/usage) ; crons branchés sur la boucle scheduler 60s
- **Migration 26** — `automation_steps`, `workers`, `worker_runs`,
`automations.trigger_mode`, `collection_properties.button_automation_id`
### Tests
- `tests/test_v70_automations_workers.py` : **31 tests** (migration, steps,
any/all, chaînes, delay, 4 nouvelles actions, secret chiffré, button,
legacy, workers ×11)
- `ruff check app tests` OK · suite **855 verts** (21 échecs `test_v67_sso.py`
pré-existants — `onelogin` absent de l'environnement ; 1 flaky parallèle
`test_env_config_fallback…` qui passe isolé)
---
## v6.9.0 (2026-09-28) — Recherche sémantique + Ask AI
> Retrouver (hybride lexical + vectoriel) et demander (RAG avec citations),
> parité Notion Enterprise Search + AI Q&A. Design : `docs/V69_Search_Ask_AI.md`.
### Added
- **Moteur sémantique** — `app/services/semantic_search.py` : chunking chevauchant
(1200 cars / overlap 150, récursif dans `children`), encodeur hashed-TF `hash-256`
(md5 % 256, L2, déterministe, zéro dépendance, `embed_texts()` pluggable),
cosinus pur Python, fusion RRF (k=60), job incrémental `index_pending()` (batch 50,
scheduler 5 min dans le lifespan) + `purge_orphans()`
- **Recherche hybride** — `GET /api/v2/search/hybrid` (`app/routers/search_ai.py`,
session ou Bearer `read`) : lexical FTS5/LIKE + vecteurs, filtre `workspace_id`,
scope membership, `PermissionManager` (pages `restricted` masquées),
`search_excluded` respecté, pagination + `X-Total-Count`
- **Ask AI** — `POST /api/v2/search/ask` : top-8 chunks autorisés (ACL **avant** prompt),
`LLMClient.complete()` si provider configuré sinon extractif offline avec
`[[fdpage:ID]]`, citations résolues (titres, « Deleted page » gérée), cache 10 min,
rate-limit 30/min, `api_audit_log`
- **Observabilité** — `GET /api/v2/search/index-status` (ressources, vecteurs, modèle)
- **Migration 25** — `semantic_embeddings`, `semantic_index_state`,
`pages.search_excluded`
### Tests
- `tests/test_v69_search_ask.py` : **24 tests** (migration, chunk/overlap, déterminisme,
cosinus, index idempotent, exclusion, purge, rappel partiel, hybride ×7, ask ×6,
index-status)
- `ruff check app tests` OK · suite **825 verts** (seuls 21 échecs `test_v67_sso.py`
pré-existants — `onelogin` absent de l'environnement)
---
## v6.8.0 (2026-09-28) — Sites & Forms publics
> Parité Notion Sites + Forms : publier un mini-site multi-pages et collecter
> des réponses anonymes dans une database. Design : `docs/V68_Sites_Forms.md`.
### Added
- **Sites multi-pages** — `app/routers/sites.py` : CRUD `GET/POST/PATCH/DELETE /api/v2/sites`
(session ou Bearer + scope `write`, pagination `X-Total-Count`, `api_audit_log`),
`GET/POST/DELETE /api/v2/sites/{id}/pages` (arbre ordonné, racine protégée),
`GET /api/v2/sites/{id}/stats` (vues jour + total) ; rendu public `GET /s/<slug>` +
`GET /s/<slug>/<page-slug>` (nav latérale, thèmes light/dark, blocs/synced/wiki résolus)
- **Gating & SEO** — mot de passe (`password_utils` salé, cookie signé 24h,
`GET|POST /s/<slug>/auth`), expiry (410), `noindex`, OG/Twitter cards,
`GET /s/<slug>/sitemap.xml`, domaine custom via header `Host`
- **Forms publics** — `PUT/GET /api/v2/collections/{id}/form` (`enabled`, `public_token f_*`,
`fields`, `required`, `success_message`, `notify_user_ids`) ; `GET /f/<token>`
(formulaire no-auth, `?embed=1` sans chrome) + `POST /f/<token>` (anonyme :
rate-limit 20/h/IP, honeypot, validation `validate_property_rule`) ;
chaque soumission = `collection_pages` + `form_responses` (ip_hash jour, pas d'IP),
notif in-app + trigger `form.submitted`
- **Migration 24** — tables `sites`, `site_pages`, `site_views`, `form_responses`,
colonne `collections.form_config_json`
- CSRF exempte `/s/` + `/f/` (soumissions anonymes cross-site)
### Tests
- `tests/test_v68_sites_forms.py` : **20 tests** — migration 24, CRUD, slug/conflit,
auth 401, isolation inter-users, pages add/remove, rendu home/subpage/blocs, 404,
vues comptées, password gate, expiry 410, sitemap, noindex, form config/submit JSON,
required 400, 404, honeypot, rate-limit 429, embed
- `ruff check app tests` OK · pas de régression (801 verts ; seuls 21 échecs
`test_v67_sso.py` pré-existants — dépendance `onelogin` absente de l'environnement)
---
## v6.7.0 (2026-09-24) — SSO / SAML + OIDC entreprise (Enterprise Auth)
> Dernière feature de la roadmap v6.0.0 : authentification fédérée via un IdP