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:
@@ -0,0 +1,121 @@
|
||||
# V6.8.0 — Sites & Forms publics
|
||||
|
||||
> **Statut** : ✅ Livré en v6.8.0 (2026-09-28) — `app/routers/sites.py`, migration 24, 20 tests verts · **Roadmap** : `ROADMAP.md § v6.8.0`
|
||||
> **Référence Notion** : Sites (multi-pages, domaine custom, SEO, analytics, password/expiry) + Forms (soumission anonyme → DB, embed, notifs).
|
||||
> **Existant réutilisé** : page publique `/p/<slug>` (`dashboard.py`), `_render_blocks_public`, `export.py` (récursif + images), `validate_property_rule` (`property_types.py`), `notifications` + `mailer.py`, vue Form (`_database_table_scripts.html`).
|
||||
|
||||
---
|
||||
|
||||
## 1. Vision
|
||||
|
||||
Passer de « une page publiée isolée » à « publier un mini-site » et « collecter des réponses » :
|
||||
`page privée → site (arbre + thème + gating) → URL publique /s/<slug>` et
|
||||
`collection → form public /f/<token> → ligne collection_pages`.
|
||||
|
||||
Hors scope : builder drag&drop marketing, paiement, commentaires publics.
|
||||
|
||||
## 2. Sites multi-pages
|
||||
|
||||
### 2.1 Schéma (migration 24)
|
||||
|
||||
```sql
|
||||
CREATE TABLE sites (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
slug TEXT NOT NULL UNIQUE, -- /s/<slug>
|
||||
root_page_id INTEGER NOT NULL REFERENCES pages(id) ON DELETE CASCADE,
|
||||
title TEXT NOT NULL DEFAULT '',
|
||||
theme TEXT NOT NULL DEFAULT 'dark', -- light|dark
|
||||
custom_domain TEXT UNIQUE, -- Host header match
|
||||
password_hash TEXT, -- NULL = public
|
||||
expires_at TIMESTAMP, -- NULL = jamais
|
||||
noindex BOOLEAN NOT NULL DEFAULT 0,
|
||||
analytics_id TEXT DEFAULT '', -- Plausible/GA, pas d'IP brute
|
||||
created_by INTEGER REFERENCES users(id),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE TABLE site_pages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
site_id INTEGER NOT NULL REFERENCES sites(id) ON DELETE CASCADE,
|
||||
page_id INTEGER NOT NULL REFERENCES pages(id) ON DELETE CASCADE,
|
||||
position INTEGER NOT NULL DEFAULT 0,
|
||||
UNIQUE(site_id, page_id)
|
||||
);
|
||||
CREATE TABLE site_views (
|
||||
site_id INTEGER NOT NULL REFERENCES sites(id) ON DELETE CASCADE,
|
||||
day TEXT NOT NULL, -- YYYY-MM-DD UTC
|
||||
views INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (site_id, day)
|
||||
);
|
||||
```
|
||||
|
||||
Règles : arbre construit depuis `site_pages` ordonné ; page hors site → 404 ; page supprimée → exclue + état « Deleted » ; `resolve_content_json()` (synced) appliqué comme `/p/<slug>`.
|
||||
|
||||
### 2.2 Gating & SEO
|
||||
|
||||
- Mot de passe : bcrypt, cookie signé `site_auth_<id>` 24h, `GET/POST /s/<slug>/auth`.
|
||||
- Expiry : `expires_at < now` → 410 « Site expiré ».
|
||||
- SEO : `<title>`, meta description (1er paragraphe), OG/Twitter (cover+icône), `sitemap.xml`, `robots.txt` (`noindex` → `noindex,nofollow`).
|
||||
- Stats : `+1/jour/site` sur chaque vue (upsert), `GET /api/v2/sites/{id}/stats?days=30`.
|
||||
|
||||
### 2.3 Routes
|
||||
|
||||
```
|
||||
GET /s/<slug> → home (root_page + nav)
|
||||
GET /s/<slug>/<page-slug> → page du site (nav active)
|
||||
GET /s/<slug>/sitemap.xml
|
||||
GET|POST /s/<slug>/auth → gate password
|
||||
GET|POST|PATCH|DELETE /api/v2/sites
|
||||
GET|POST|DELETE /api/v2/sites/{id}/pages
|
||||
GET /api/v2/sites/{id}/stats
|
||||
```
|
||||
|
||||
Domaine custom : si `Host == custom_domain` → monte le site sans `/s/<slug>`.
|
||||
|
||||
## 3. Forms publics
|
||||
|
||||
### 3.1 Config (colonne `collections.form_config_json`)
|
||||
|
||||
```json
|
||||
{
|
||||
"enabled": true, "public_token": "f_abc123",
|
||||
"title": "Contact", "success_message": "Merci !",
|
||||
"fields": ["Name", "Email", "Message"],
|
||||
"required": ["Name", "Email"],
|
||||
"notify_user_ids": [1, 2]
|
||||
}
|
||||
```
|
||||
|
||||
Table `form_responses (id, collection_id, row_id, ip_hash, created_at)` — `ip_hash` = sha256(IP+jour), jamais d'IP brute.
|
||||
|
||||
### 3.2 Soumission anonyme
|
||||
|
||||
```
|
||||
GET /f/<token> → formulaire thèmable (no-auth, `?embed=1` sans chrome)
|
||||
POST /f/<token> → valide via validate_property_rule → 400 + messages
|
||||
rate-limit 20/h/IP → 429, honeypot __hp + Turnstile optionnel
|
||||
→ collection_pages + form_responses + notif in-app/email
|
||||
```
|
||||
|
||||
Trigger automation `form.submitted` (v7.0.0 s'y branchera).
|
||||
|
||||
Embed : `<iframe src="https://host/f/<token>?embed=1" width="100%" height="600">`.
|
||||
|
||||
## 4. UI
|
||||
|
||||
- Éditeur « … » → `Share → Publish → Site` : créer site, choisir pages, thème, password/expiry/noindex, copier URL + snippet.
|
||||
- Collection → `Views → Form → Share form` : toggle public, champs affichés, message succès, liste réponses (compteur + lien lignes filtrées `form:true`).
|
||||
- Settings → Sites (liste, stats sparkline, domaine custom, revoke).
|
||||
|
||||
## 5. Sécurité / perfs
|
||||
|
||||
- Password bcrypt cost 12, cookie `itsdangerous` signé, CSRF exempt uniquement `POST /f/<token>` (rate-limit + honeypot compensent).
|
||||
- `custom_domain` validé (hostname, pas d'IP privée) ; `slug` `^[a-z0-9-]{3,50}$`.
|
||||
- Cache rendu public 60s (`Cache-Control: public, max-age=60`), stats en upsert (pas de ligne/vue).
|
||||
|
||||
## 6. Tests (`tests/test_v68_sites_forms.py`, ~25)
|
||||
|
||||
CRUD site, nav ordonnée, 404 hors-site, password OK/KO + cookie, expiry 410, noindex meta, sitemap, stats +1/jour, domaine custom (Host override), form GET anonyme, POST valide → ligne, POST invalide → 400, rate-limit 429, honeypot 400, embed `?embed=1`, notif créée, ACL (non-owner 403/404).
|
||||
|
||||
## 7. Rollout
|
||||
|
||||
1. Migration 24 + CRUD API + tests. 2. Rendu `/s/` + gating + SEO. 3. Forms + embed + notifs. 4. UI Share/Site + docs `/help` + OpenAPI.
|
||||
@@ -0,0 +1,69 @@
|
||||
# V6.9.0 — Recherche sémantique + Ask AI
|
||||
|
||||
> **Statut** : ✅ Livré en v6.9.0 (2026-09-28) — `app/services/semantic_search.py`, `app/routers/search_ai.py`, migration 25, 24 tests verts · **Roadmap** : `ROADMAP.md § v6.9.0`
|
||||
> Note d'implémentation : table générique `semantic_embeddings(resource_type, resource_id, …)` au lieu de `page_embeddings(page_id, …)` — couvre aussi les collections ; encodeur hashed-TF `hash-256` (pas d'appel `/embeddings` externe) ; pièces jointes et onglets palette en follow-up.
|
||||
> **Référence Notion** : Enterprise Search (Notion + Slack/Jira/Drive) + AI Q&A avec citations.
|
||||
> **Existant** : FTS5 (`services/search.py`, `GET /api/search`, `GET /api/v2/search`), palette `Ctrl+K`, `AgentEngine` (ReAct 12 tours), `PermissionManager`, `llm_config` + `LLMClient` (9 providers).
|
||||
|
||||
---
|
||||
|
||||
## 1. Vision
|
||||
|
||||
`question → top-k chunks autorisés → réponse avec [[fdpage:ID]] cliquables`.
|
||||
Hybride lexical + vectoriel (RRF), jamais de fuite ACL (filtrage avant prompt).
|
||||
|
||||
Hors scope : index temps réel < 1s, connecteurs Slack/Jira (v7.x si besoin forge d'abord).
|
||||
|
||||
## 2. Indexation
|
||||
|
||||
### 2.1 Schéma (migration 25)
|
||||
|
||||
```sql
|
||||
CREATE TABLE page_embeddings (
|
||||
page_id INTEGER NOT NULL REFERENCES pages(id) ON DELETE CASCADE,
|
||||
chunk_id INTEGER NOT NULL, -- index du chunk (0..N)
|
||||
chunk_text TEXT NOT NULL,
|
||||
embedding BLOB NOT NULL, -- float32 serialisé
|
||||
model TEXT NOT NULL DEFAULT 'local-tfidf',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (page_id, chunk_id)
|
||||
);
|
||||
CREATE TABLE search_index_state (page_id INTEGER PRIMARY KEY, indexed_at TIMESTAMP);
|
||||
ALTER TABLE pages ADD COLUMN search_excluded BOOLEAN NOT NULL DEFAULT 0;
|
||||
```
|
||||
|
||||
- Chunking : blocs → texte brut (~500 tokens, overlap 50), ignore `embed`/images (légende seule).
|
||||
- Providers : `openai-compatible /embeddings` via `LLMClient` si clé user/workspace, sinon **TF-IDF local** (zéro dépendance, FR+EN stopwords) — même interface `embed(texts) -> vectors`.
|
||||
- Job : scheduler 5 min, batch 50 (`updated_at > indexed_at`), `DELETE` embeddings si page trashée/exclue.
|
||||
|
||||
### 2.2 Contenus indexés
|
||||
|
||||
Pages `blocks` + `markdown`/`file` (résolution déjà `export.py`), pièces jointes texte (pdf via `pypdf`, docx via `python-docx`, txt/md — cap 1 MB/fichier), titres de lignes DB (sans valeurs privées si `property_permissions` restreint → chunk « titre seul »), issues Gitea/GitHub liées (titre + labels, pas de body privé sans grant).
|
||||
|
||||
## 3. Recherche hybride
|
||||
|
||||
`GET /api/v2/search/hybrid?q=&workspace_id=&type=&limit=20` :
|
||||
1. FTS5 score (existants) ; 2. cosine top-50 ; 3. fusion RRF `k=60` ; 4. filtre ACL (`can_view_page`/`can_view_collection`) ; 5. `X-Total-Count`.
|
||||
Filtres : `verified:1`, `type:page|collection|file`, `after:YYYY-MM-DD`.
|
||||
Palette `Ctrl+K` : onglets `Pages / Fichiers / ✨ Réponses IA`.
|
||||
|
||||
## 4. Ask AI
|
||||
|
||||
`POST /api/v2/search/ask {question, workspace_id}` → `{answer_markdown, citations: [{page_id, title}]}` :
|
||||
- top-k=8 chunks autorisés → prompt `system: réponds en français, cite [[fdpage:ID]]` → `LLMClient.chat()` (clé user, sinon mock déterministe « hors-ligne » comme `ai_writing.py`).
|
||||
- Budget : 4k tokens contexte, timeout 60s, cache `(hash(question)+workspace)` 10 min.
|
||||
- Citations résolues via `wiki/titles` (renommage propagé, « Deleted page » gérée).
|
||||
|
||||
## 5. UI / perfs / sécu
|
||||
|
||||
- Badge `✨` + temps de réponse, feedback 👍/👎 (`ask_feedback` log, futur fine-tune).
|
||||
- ACL avant LLM (jamais de chunk interdit dans le prompt) ; `search_excluded` respecté partout ; audit `api_audit_log`.
|
||||
- Perfs : embeddings lazy (pas au save, job fond), cosine en numpy si dispo sinon pur Python (DB < 10k pages OK).
|
||||
|
||||
## 6. Tests (`tests/test_v69_search_ask.py`, ~20)
|
||||
|
||||
Index chunks, hybride RRF ordre, ACL (page restricted exclue), `verified` filtre, exclusion flag, pièces jointes cap, ask citations valides, ask sans clé → fallback, cache hit, rate-limit, `X-Total-Count`.
|
||||
|
||||
## 7. Rollout
|
||||
|
||||
1. `embed()` + TF-IDF + job. 2. `/hybrid` + palette. 3. `/ask` + cache + feedback. 4. Docs `/help` + OpenAPI.
|
||||
@@ -0,0 +1,71 @@
|
||||
# V7.0.0 — Automations v2 + Workers
|
||||
|
||||
> **Statut** : ✅ Livré en v7.0.0 (2026-09-28) — `automation_steps` + 4 nouvelles actions + `services/workers.py` + `routers/workers.py`, migration 26, 31 tests verts · **Roadmap** : `ROADMAP.md § v7.0.0`
|
||||
> Notes d'implémentation : legacy sans steps = fallback inchangé (pas de migration de données) ; `delay` = sleep plafonné 300s (pas de re-queue) ; `agent_trigger` = run synchrone (timeout `agent_run_timeout_seconds`) ; éditeur visuel canvas en follow-up.
|
||||
> **Référence Notion** : Database automations (multi-triggers any/all, chaînes) + Buttons + Workers (custom code, credits dashboard, partage équipe, 07/2026).
|
||||
> **Existant** : `automations` + `automation_runs` (scheduler 60s), triggers `page.*/collection.*` + cron + bouton, conditions `eq/neq/contains/...`, actions `webhook/set_property/create_page/notify`, bloc `button`, webhooks v2 HMAC + retry.
|
||||
|
||||
---
|
||||
|
||||
## 1. Vision
|
||||
|
||||
De `si X alors Y` à `quand (A ou B) et si C alors [Y1 → délai → Y2]`, plus `code custom` sandboxé :
|
||||
`trigger(s) → conditions → steps → runs avec logs par step`.
|
||||
|
||||
## 2. Modèle (migration 26)
|
||||
|
||||
```sql
|
||||
CREATE TABLE automation_steps (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
automation_id INTEGER NOT NULL REFERENCES automations(id) ON DELETE CASCADE,
|
||||
kind TEXT NOT NULL, -- trigger|condition|delay|action
|
||||
position INTEGER NOT NULL DEFAULT 0,
|
||||
config_json TEXT NOT NULL DEFAULT '{}'
|
||||
-- trigger: {event: page.created|form.submitted|meeting.summarized|site.viewed|...,
|
||||
-- mode: any|all (au niveau automation)}
|
||||
-- condition: {property, operator, value} (AND entre steps condition)
|
||||
-- delay: {seconds|minutes|hours|days}
|
||||
-- action: {type: webhook|set_property|create_page|notify|slack|email|forge_issue|agent_trigger,
|
||||
-- ...params + interpolation [[prop]]/{{title}}}
|
||||
);
|
||||
CREATE TABLE workers (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
slug TEXT NOT NULL UNIQUE, workspace_id INTEGER REFERENCES workspaces(id),
|
||||
name TEXT NOT NULL, code_py TEXT NOT NULL DEFAULT '',
|
||||
schedule_cron TEXT, shared BOOLEAN NOT NULL DEFAULT 0,
|
||||
daily_budget_s INTEGER NOT NULL DEFAULT 60,
|
||||
created_by INTEGER REFERENCES users(id), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE TABLE worker_runs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, worker_id INTEGER NOT NULL REFERENCES workers(id) ON DELETE CASCADE,
|
||||
status TEXT NOT NULL, -- ok|error|timeout|over_budget
|
||||
logs TEXT NOT NULL DEFAULT '', duration_ms INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
ALTER TABLE collection_properties ADD COLUMN button_automation_id INTEGER REFERENCES automations(id);
|
||||
```
|
||||
|
||||
Compat : automations v5.1.0 existantes migrées en 1 step trigger + 1 step action.
|
||||
|
||||
## 3. Sémantique d'exécution
|
||||
|
||||
- `mode any` (défaut) : n'importe quel step trigger déclenche ; `all` : tous les events doivent arriver dans la fenêtre 5 min (état `automation_runs`).
|
||||
- Nouveaux triggers : `form.submitted` (v6.8), `meeting.summarized` (v7.1), `site.viewed` (seuil 100 vues/jour), `agent.run.failed`.
|
||||
- Actions : `slack` (incoming webhook URL chiffrée), `email` (via `mailer.py`), `forge_issue` (Gitea/GitHub `owner/repo`, titre/body/labels), `agent_trigger` (`agent_id` + input), `delay` (re-queue run, pas de sleep).
|
||||
- Scheduler existant étendu : évalue steps en ordre, log `automation_runs` enrichi `{step_id, in, out, ms}`.
|
||||
|
||||
## 4. Bouton DB natif
|
||||
|
||||
Propriété `button` : `{label, automation_id}` ; rendu cliquable en table/board/gallery ; `POST /api/automations/{id}/run` (déjà CSRF-exempt) + garde ACL écriture ligne ; optimistic toast.
|
||||
|
||||
## 5. Workers lite
|
||||
|
||||
- Code Python restreint : pas d'import réseau/`os`/`subprocess`, timeout 30s, mémoire cap, allowlist `httpx` vers `Gitea/GitHub` + webhooks sortants uniquement.
|
||||
- Déclenchement : cron (`schedule_cron`) ou manuel ou `agent_trigger` ; `shared=1` → fork 1-clic (comme Notion Workers partagés).
|
||||
- Compteur usage : `SUM(duration_ms)/jour/workspace` affiché façon credits dashboard.
|
||||
|
||||
## 6. UI / sécu / tests
|
||||
|
||||
- Settings → Automations : canvas vertical triggers → conditions → actions (Alpine.js, composants `components.css`), test-run avec payload sample, historique par step repliable.
|
||||
- Secrets (Slack URL, tokens) chiffrés Fernet comme `sso_config` ; champ vide = conserver.
|
||||
- Tests `tests/test_v70_automations_workers.py` (~25) : any/all, chaîne + delay (clock injectée), slack/email/forge/agent mocks, button ACL, worker timeout/over_budget/partage, migration v5 compat.
|
||||
@@ -0,0 +1,43 @@
|
||||
# V7.1.0 — Calendar sync + Meeting Notes
|
||||
|
||||
> **Statut** : ✅ Livré en v7.1.0 (2026-09-28) — `services/calendar_sync.py`, `services/meetings.py`, `routers/meetings.py`, migration 27, 15 tests verts · **Roadmap** : `ROADMAP.md § v7.1.0`
|
||||
> Notes d'implémentation : pas de lib CalDAV/Google (REST + XML bruts) ; refresh OAuth hors scope (relink explicite sur 401) ; conflit = LWW auto + notif (pas de picker « garder ma version ») ; free/busy au jour (pas de créneaux horaires).
|
||||
> **Référence Notion** : Notion Calendar (Google sync) + AI Meeting Notes → trigger Custom Agents (07/2026).
|
||||
> **Existant** : vues jour/semaine/mois, moteur `recurrence.py`, `reminders.py` (scan 60s, dédup), timezones, `notify_assignment()`, template Meeting Notes.
|
||||
|
||||
---
|
||||
|
||||
## 1. Sync externe bidirectionnelle
|
||||
|
||||
```sql
|
||||
CREATE TABLE calendar_links (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
provider TEXT NOT NULL, -- google|caldav
|
||||
tokens_enc TEXT NOT NULL DEFAULT '', -- Fernet (refresh + access)
|
||||
calendar_id TEXT NOT NULL DEFAULT 'primary',
|
||||
sync_token TEXT DEFAULT '',
|
||||
last_sync TIMESTAMP, UNIQUE(user_id, provider, calendar_id)
|
||||
);
|
||||
ALTER TABLE collection_pages ADD COLUMN external_event_id TEXT;
|
||||
CREATE TABLE meeting_transcripts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, page_id INTEGER NOT NULL REFERENCES pages(id) ON DELETE CASCADE,
|
||||
audio_path TEXT NOT NULL DEFAULT '', transcript TEXT NOT NULL DEFAULT '',
|
||||
summary TEXT NOT NULL DEFAULT '', language TEXT DEFAULT 'fr',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
- OAuth Google (`app/auth/providers/google_calendar.py`, scope `calendar.events`) + CalDAV générique (URL/user/pass chiffrés).
|
||||
- Pull 15 min : event externe → page (titre + `date` + `__external_id__`) ; push : occurrence FlowDeck → event (si `sync_enabled` par collection).
|
||||
- Conflit (modifié des 2 côtés depuis `last_sync`) → LWW + notif `calendar.conflict` + lien « garder ma version / prendre externe ».
|
||||
- `GET /db/{id}/calendar/freebusy?attendees=&from=&to=` : créneaux libres (jour ouvré 9h-18h, fuseau user).
|
||||
|
||||
## 2. Meeting Notes v2
|
||||
|
||||
Upload `.mp3/.wav/.m4a` (100 MB) sur page Meeting → `meeting_transcripts` → transcription (Whisper local si binaire présent, sinon API via `llm_config`, timeout 10 min, job fond + polling) → résumé `ai_writing.py` (`Décisions`, `Next steps [[person]]`, `Action items`) inséré en blocs + `Agenda/Notes` existants complétés.
|
||||
Émet `meeting.summarized` → automations v7.0 (update tracker, post Slack, file tickets).
|
||||
|
||||
## 3. Tests (`tests/test_v71_calendar_meetings.py`, ~15)
|
||||
|
||||
OAuth mock (tokens chiffrés roundtrip), pull/push, conflit LWW + notif, freebusy, transcription mock → résumé blocs, trigger émis, quota 100 MB refusé.
|
||||
@@ -0,0 +1,35 @@
|
||||
# V7.2.0 — Enterprise Admin : SCIM + 2FA + Audit UI
|
||||
|
||||
> **Statut** : 📝 Planifié · **Roadmap** : `ROADMAP.md § v7.2.0`
|
||||
> **Référence Notion** : SCIM, audit log, domain verification, agent permissions/governance.
|
||||
> **Existant** : SSO SAML/OIDC + provisioning + `sso_only` (v6.7.0), `api_audit_log` + `permission_audit_log` + `sso_login_history` (API seule), `SessionManager`, `PermissionManager`.
|
||||
|
||||
---
|
||||
|
||||
## 1. SCIM 2.0 (provisioning auto)
|
||||
|
||||
`GET|POST /scim/v2/Users`, `GET|PUT|PATCH|DELETE /scim/v2/Users/{id}` (Bearer `scim_tokens`, admin) :
|
||||
mapping `userName→login`, `emails[0]→email`, `name→full_name`, `active→users.is_active`.
|
||||
`active=false` → `is_active=0`, sessions révoquées, login refusé (401 « suspended ») sauf admin.
|
||||
Groupes IdP → `sync_sso_permissions()` existant réutilisé.
|
||||
|
||||
## 2. 2FA + passkeys
|
||||
|
||||
- TOTP : `users.totp_secret_enc` (Fernet), setup QR (`otpauth://`), vérif au login après password (fenêtre ±1), 10 backup codes (`sha256`, usage unique).
|
||||
- WebAuthn : `webauthn_credentials (id, user_id, credential_id, public_key, sign_count)` ; login `password + key` ou `passkey seule` si `passwordless_allowed`.
|
||||
- Combinable avec `sso_only` (local password refusé, TOTP conservé si `require_2fa=1`).
|
||||
|
||||
## 3. Domain claim + audit UI
|
||||
|
||||
- `domain_claims (domain UNIQUE, txt_token, verified BOOL, auto_join_role, enforce_sso BOOL)` ; vérif DNS TXT ; `enforce_sso` → login local du domaine redirigé IdP.
|
||||
- Settings → Audit : table unifiée (`api_audit_log` + `permission_audit_log` + `sso_login_history` + `worker_runs`), filtres acteur/ressource/date, export CSV (10k max), rétention 365j (purge scheduler).
|
||||
|
||||
## 4. Gouvernance agents
|
||||
|
||||
`agent_policies (workspace_id, allowed_tools_json, max_steps, require_approval BOOL)` + `agent_approvals (action_id, status pending/approved/rejected, approver_id)` :
|
||||
si `require_approval` et outil write → `agent.run.approval_requested` (webhook + cloche) → exécution après approve, sinon 403. File Settings → Agents.
|
||||
|
||||
## 5. Migrations 28 + tests (~25)
|
||||
|
||||
`scim_tokens`, `domain_claims`, `webauthn_credentials`, `agent_policies`, `agent_approvals`, `users.is_active/totp_secret_enc`.
|
||||
Tests : SCIM CRUD + suspend, TOTP ±fenêtre + backup, WebAuthn mock, domain TXT mock, audit filtres/export, policies approve/reject.
|
||||
@@ -0,0 +1,72 @@
|
||||
# V7.3.0 — Wiki / Teamspaces + Polish
|
||||
|
||||
> **Statut** : ✅ Implémenté (2026-09-29) — 72 tests · suite complète **1016 verts** · **Roadmap** : `ROADMAP.md § v7.3.0`
|
||||
> **Référence Notion** : Wikis + Teamspaces + Verified pages + Charts/Number + réactions/follow/guests + page analytics.
|
||||
> **Existant** : workspaces + rôles, backlinks, wiki-links `[[`, `collection_dashboards`, Chart.js, comments, `site_views`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Teamspaces + Verified
|
||||
|
||||
```sql
|
||||
CREATE TABLE teamspaces (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, workspace_id INTEGER NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL, private BOOLEAN NOT NULL DEFAULT 0, UNIQUE(workspace_id, name));
|
||||
CREATE TABLE teamspace_members (teamspace_id INTEGER NOT NULL REFERENCES teamspaces(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id), role TEXT NOT NULL DEFAULT 'editor', UNIQUE(teamspace_id, user_id));
|
||||
ALTER TABLE pages ADD COLUMN teamspace_id INTEGER REFERENCES teamspaces(id);
|
||||
ALTER TABLE collections ADD COLUMN teamspace_id INTEGER REFERENCES teamspaces(id);
|
||||
CREATE TABLE page_verifications (
|
||||
page_id INTEGER PRIMARY KEY REFERENCES pages(id) ON DELETE CASCADE,
|
||||
verified_by INTEGER REFERENCES users(id), note TEXT DEFAULT '',
|
||||
verified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP);
|
||||
```
|
||||
|
||||
- `private=1` sans membership → 404 (comme collections restricted) ; `PermissionManager.get_teamspace_role()`.
|
||||
- Badge ✅ (owner/editor vérifie, expiry 90j défaut, job notif owner à J-7) ; filtre `verified:1` en search + palette ; page `/wiki` auto (verified + recents + teamspaces).
|
||||
|
||||
## 2. Blocs & charts
|
||||
|
||||
- Bloc `mermaid` : code → SVG serveur si `mmdc` présent, sinon fallback bloc code + rendu client (déjà Prism) ; export HTML/PDF embarque le SVG.
|
||||
- `equation_inline` : KaTeX inline (lib déjà self-hostée `static/js/katex.min.js`).
|
||||
- Bloc `progress` : `{rollup_ref}` → barre % (réutilise `RollupEngine`).
|
||||
- Charts : type `number` (big KPI), dashboards multi-DB en page (grille `collection_dashboards` + source `any`, limite 200 groupes documentée), « ouvrir la ligne » depuis chart.
|
||||
- **Livré (follow-up)** : `number` = KPI `count|sum|avg|min|max` (`_chart_aggregate`/`_fmt_number`) ; rendu grille `GET /db/{collection_id}/dashboards/{dashboard_id}` — widgets à `collection_id` (= source `any`), ≤ 40 widgets, ≤ 200 lignes/chart (`CHART_MAX_GROUPS`) ; la config de vue est choisie selon le `view_type` demandé (bug corrigé : la première vue était toujours utilisée) ; un widget pointant sur une DB restreinte est sauté (pas de leak).
|
||||
|
||||
## 3. Collab polish
|
||||
|
||||
- `comment_reactions (comment_id, user_id, emoji, UNIQUE)` + picker emoji.
|
||||
- `page_follows (page_id, user_id)` → notif `page.updated` aux followers (pref `follows`, défaut ON si commenté).
|
||||
- **Livré (follow-up)** : `wiki.notify_followers_of_page_update` (throttle 10 min, `actor_id` exclu) branché dans `automations.fire_event` ; `actor_id` dans les payloads `board.update_page`/`save_page_blocks` ; auto-follow à la création d'un commentaire (`wiki.ensure_follow`).
|
||||
- Guests : `guest_shares (page_id, email, token, role viewer|commenter, expires_at)` — accès sans compte via `/g/<token>` (session guest limitée, auditée).
|
||||
- `page_views (page_id, day, views PK)` — même pattern `site_views`, exposé `GET /board/api/pages/{id}/views` (owner only).
|
||||
|
||||
## 4. Tests (`tests/test_v73_wiki_polish.py`, 72 ✅)
|
||||
|
||||
Teamspace private 404/grant, verify badge/expiry/notif, `/wiki` home, blocs
|
||||
mermaid/equation/progress + export, réactions, follow, guest token/expiry,
|
||||
page views. **Follow-ups** : sidebar cross-workspace (sans `workspace_id`),
|
||||
page HTML `/wiki/teamspaces/{id}` (owner 200 / outsider 404 API + non-leak
|
||||
HTML), auto-follow par commentaire, notif `page.updated` throttlée + acteur
|
||||
exclu, `ensure_follow` idempotent, regex + unfurl gitea/github (tokens
|
||||
mockés) + endpoint `/board/api/og/metadata`, KPI `number` + `_chart_values`
|
||||
(0 conservé) + dashboards multi-DB + 404.
|
||||
|
||||
## 5. Implémentation réelle vs. design initial
|
||||
|
||||
- **Livré** : schema §1 à l'identique ; routes sous `/api/v2/wiki/*` (le design
|
||||
parlait de `/wiki` comme page HTML : l'agrégat est exposé en JSON, le
|
||||
front Tri-state reste à faire) ; blocs §2 (`mermaid`, `equation_inline`,
|
||||
`progress`) rendus **côté serveur** par `app/services/wiki_blocks.py` et
|
||||
branchés sur `export.blocks_to_html` ; collab §3 complet.
|
||||
- **Follow-ups livrés** : sidebar par teamspace (section `base.html` +
|
||||
listing cross-workspace + page HTML), charts `number` + dashboards multi-DB,
|
||||
notif `page.updated` déclenchée à l'édition + auto-follow, unfurl
|
||||
`gitea:`/`github:` dans `POST /board/api/og/metadata` (champs bookmark
|
||||
persistés), UI Settings → Audit branchée sur `/api/v2/audit/logs`. Les
|
||||
21 casses SSO (`test_v67_sso.py`) sont corrigées (install
|
||||
`python3-saml==1.16.0` + `authlib==1.8.0`).
|
||||
- **Écart assumé** : `progress` agrège directement sur
|
||||
`collection_pages.property_values_json` plutôt que via `RollupEngine`, dont
|
||||
la signature exige une chaîne de relations + une page source — ce qu'un bloc
|
||||
de progression embarqué dans une page n'a pas.
|
||||
Reference in New Issue
Block a user