feat: v7.3.0 — cycle v6.8.0→v7.3.0 (Sites, Search, Automations, Calendar, SCIM, Wiki) + audit A9
- 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:
+286
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user