Files
flowdeck/ARCHITECTURE.md
bruno fc8548194a
FlowDeck CI / lint (push) Failing after 1m32s
FlowDeck CI / test (push) Failing after 27m50s
FlowDeck CI / docker (push) Skipped
feat(templates): refonte complète des templates façon Notion + vues/agents-skills
Templates (v7.71.x) :

- registre unifié \	emplates\ (migrations 48-49) + TemplateService.instantiate unique (UI, API v2, agent, scheduler)

- sélecteur (pilule page vide, menu •••, commande /template), gestionnaire /templates, menu New ▾, From template, base inline dans un document

- 141 presets système (59 pages, 42 bases, 15 blocs, 25 lignes), titre auto depuis le template, variables title réservée

- récurrences RRULE + scheduler dédupliqué, agent apply_template/list_templates, API /api/templates + /api/v2/fd-templates

- correctifs : bouton Templates, centrage fenêtre, filtres CSP, flux de création, variable title

- tests : tests/test_fd_templates.py (19) et e2e/templates_picker.spec.js (8)

Inclut le travail déjà présent dans le working tree (vues Notion : view_query/view_aggregate/form_projection/geocoding, property_types, database_table, docs agents-skills) et ignore .playwright-mcp/.
2026-10-10 18:52:19 -04:00

1074 lines
96 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Architecture FlowDeck — Document Complet v7.70.0
> **FlowDeck** = clone complet de **Notion** intégré nativement à **Gitea**, servi par une application **FastAPI** monolithique (SSR Jinja2 + htmx + Alpine.js), avec **API REST publique v2**, **agent IA conversationnel**, **automatisations**, **SSO entreprise** et **édition collaborative temps réel**.
>
> Document d'architecture à jour de la version **7.70.0** (`VERSION`). Historique complet : voir §17 et `CHANGELOG.md`. Modèle de données détaillé table par table : `docs/DATA_MODEL.md`.
## Table des matières
1. [Vue d'ensemble](#1-vue-densemble)
2. [Architecture système](#2-architecture-système)
3. [Structure du backend](#3-structure-du-backend)
4. [Routage & API](#4-routage--api)
5. [Modèle de données](#5-modèle-de-données)
6. [Authentification & comptes](#6-authentification--comptes)
7. [SSO entreprise : SAML, OIDC, SCIM, 2FA, WebAuthn](#7-sso-entreprise-saml-oidc-scim-2fa-webauthn)
8. [Sécurité applicative](#8-sécurité-applicative)
9. [Autorisation : rôles, permissions granulaires, gouvernance](#9-autorisation-rôles-permissions-granulaires-gouvernance)
10. [Workspaces & collaboration multi-utilisateurs](#10-workspaces--collaboration-multi-utilisateurs)
11. [Databases : propriétés, formules, rollups, dépendances](#11-databases-propriétés-formules-rollups-dépendances)
12. [Système de vues](#12-système-de-vues)
13. [Pages, éditeur de blocs & temps réel](#13-pages-éditeur-de-blocs--temps-réel)
14. [Wiki, liens, blocs synchronisés & teamspaces](#14-wiki-liens-blocs-synchronisés--teamspaces)
15. [Partage, publication, Library, My Tasks & Trash](#15-partage-publication-library-my-tasks--trash)
16. [IA : agent, compétences, connecteurs & écriture](#16-ia-agent-compétences-connecteurs--écriture)
17. [Recherche : FTS5, sémantique hybride & Ask AI](#17-recherche-fts5-sémantique-hybride--ask-ai)
18. [AI Meeting Notes & calendrier](#18-ai-meeting-notes--calendrier)
19. [Automatisations, workers, rappels, notifications & webhooks](#19-automatisations-workers-rappels-notifications--webhooks)
20. [Intégration Gitea / GitHub](#20-intégration-gitea--github)
21. [Import & Export](#21-import--export)
22. [Frontend : rendu, CSP, PWA & Web Clipper](#22-frontend-rendu-csp-pwa--web-clipper)
23. [Déploiement & exploitation](#23-déploiement--exploitation)
24. [Outils de développement, tests & CI](#24-outils-de-développement-tests--ci)
25. [Historique des versions v1.0 → v7.70.0](#25-historique-des-versions-v10--v7700)
26. [Chantiers ouverts & suites](#26-chantiers-ouverts--suites)
27. [Références](#27-références)
---
## 1. Vue d'ensemble
FlowDeck est un **produit unique en un serveur** : une application FastAPI (Python 3.13) qui rend des pages HTML complètes (Jinja2), interagit via htmx et Alpine.js, persiste tout dans **une base SQLite unique** (mode WAL), et se branche sur un **Gitea externe** (OAuth2, API, webhooks) comme forge optionnel — tout en restant utilisable seul (comptes locaux, workspaces locaux, GitHub en option).
### Principes architecturaux
| Principe | Application |
|---|---|
| **Monolithe lisible** | Un seul processus uvicorn : routage, rendu, services, scheduler d'arrière-plan dans le `lifespan` FastAPI. Pas de worker Celery, pas de file externe. |
| **SSR d'abord** | Les pages sont rendues serveur ; JS = hydratation (Alpine) + interactions ciblées (htmx). Navigation partielle maison (`static/js/app.js`) sans rechargement du shell. |
| **SQLite + JSON document-oriented** | Pas d'ORM : SQL brut via `get_conn()`, ~35 colonnes `*_json` portent les structures riches (blocs, configs de vues, schémas de propriétés, tool calls). |
| **Zéro bundler front** | Aucun pipeline JS : les libs (Alpine build CSP, htmx, KaTeX, highlight.js, Chart.js, Leaflet, SortableJS) sont **vendorisées** dans `static/js/` ; CSP stricte par nonce, `unsafe-eval` banni (audit A20, achevé v7.43). |
| **Sécurité vérifiée par audit** | 43 items d'audit (A1–A43, `ROADMAP.md`) traités v7.3→v7.35 : 401 partout, CSRF unifié, CSP sans eval, SSRF, autoescape, rate limiting, SQLite hors event loop. |
| **Multi-tenant par workspace** | Isolation des données par `workspace_id`, rôles par workspace + ACL granulaires page/collection/propriété, moindre privilège par défaut. |
| **IA au service du produit** | Agent ReAct, AI Writing, Ask AI, Meeting Notes : tous branchés sur un client LLM unique à 23 fournisseurs + mode offline déterministe ; l'agent n'a **jamais plus de droits que l'utilisateur appelant**. |
| **Compat Gitea historique** | Boards Kanban legacy ↔ Collections Notion-like via un adaptateur (`GiteaBoardCompat`) ; le miroir dépôt/page privée coexiste avec le workspace local. |
### Stack technique
- **Backend** : Python 3.13, FastAPI 0.115, uvicorn[standard] 0.34, Jinja2, httpx, pydantic v2 + pydantic-settings, itsdangerous (sessions signées), cryptography (Fernet), python3-saml, authlib, pyotp, webauthn, WeasyPrint/xhtml2pdf (PDF), openpyxl/python-docx/pypdf/BeautifulSoup/PyYAML (imports).
- **Frontend** : htmx 2.0.4, `@alpinejs/csp` 3.17.4, SortableJS, Chart.js, Leaflet, KaTeX, highlight.js — tous self-hosted.
- **Persistance** : SQLite (WAL, FTS5), migrations versionnées maison (baseline v1 + 38 migrations).
- **Empaquetage** : Docker multi-stage Python (pas de stage node), verrouillage des dépendances avec **uv** (`uv.lock`).
---
## 2. Architecture système
```mermaid
graph TD
B[Navigateur / PWA + service worker] -->|HTML, htmx, fetch nonce-CSP| W[uvicorn :8080 FastAPI]
E[Extension Web Clipper MV3] -->|Bearer fd_ token| A2
X[Clients tiers / agents externes] -->|Bearer + scopes| A2[API publique /api/v2 + OpenAPI /docs]
W --> SSO[SSO : SAML / OIDC / SCIM]
W --> AG[Agent IA / LLM client]
AG --> P23[23 fournisseurs LLM + mode offline]
W --> GT[Gitea externe OAuth2, API, webhooks]
W --> GH[GitHub API]
W --> DB[(SQLite WAL /data/flowdeck.db + FTS5)]
W --> FS[/data uploads avatars backups]
W --> WS[WebSocket /ws/pages/id temps réel]
W --> OUT[Webhooks sortants HMAC]
SCH[lifespan : 9 schedulers asyncio] --> DB
```
### 2.1 Cycle de vie applicatif (`app/main.py`)
Le `lifespan` FastAPI orchestre, au démarrage :
1. `init_db()` (`app/db.py`) — baseline idempotent + `apply_migrations()` (`app/migrations.py`, version courante **38**) ;
2. démarrage des **schedulers asyncio** lancés via `_spawn(name, coro)` — un wrapper `A34` qui journalise toute exception et redémarre le scheduler au bout de 10 s :
| Scheduler | Service | Rôle |
|---|---|---|
| `agent_scheduler` | `app/routers/agent.py` | déclencheurs `schedule` des agents (60 s) |
| `automation_scheduler` | `app/services/automations.py` | crons + retries des automatisations, mutualisé avec `run_due_workers()` |
| `backup_scheduler` | `app/services/backup.py` | snapshot quotidien SQLite (checkpoint WAL avant copie) |
| `project_sync_scheduler` | `app/services/projects.py` | synchronisation du registre `projects` avec la forge |
| `trash_purge_scheduler` | `app/services/trash.py` | purge de la corbeille |
| `reminder_scheduler` | `app/services/reminders.py` | scan des rappels (60 s) |
| `semantic_index_scheduler` | `app/services/semantic_search.py` | indexation incrémentale des embeddings (300 s) |
| `calendar_sync_scheduler` | `app/services/calendar_sync.py` | resync bidirectionnelle Google/CalDAV |
| `webhook_retry_scheduler` | `app/services/webhook_outbound.py` | ledger de retry des webhooks sortants (condé par `WEBHOOK_RETRY_ENABLED`) |
3. arrêt gracieux de ces tâches au shutdown.
### 2.2 Chaîne de middleware (ordre d'exécution, requête entrante)
Ajoutées dans `app/main.py` dans l'ordre inverse de l'exécution (Starlette : la dernière ajoutée tourne en premier) :
```
Requête → CORS (A37, origines fermées + regex localhost/extension)
→ RateLimitMiddleware (fenêtre glissante par IP, config à chaud A33)
→ ContentSecurityPolicyMiddleware (nonce par requête, script-src fermé)
→ CSRFMiddleware (double soumission cookie→header, exemptions machine-to-machine)
→ SessionMiddleware (1 h, stockage transitoire état OAuth `state` uniquement)
→ route
```
La **vraie session applicative** n'est pas la SessionMiddleware : c'est la cookie signée `flowdeck_session` résolue par `app/auth/session.py` (§6.4).
### 2.3 Conventions de rendu
- Deux modes de réponse selon la surface : **HTML SSR** (pages, fragments htmx) ou **JSON** (API internes `/api/…` cookie-auth et API publiques `/api/v1`, `/api/v2` Bearer).
- Les erreurs API v2 suivent **RFC 7807** ; réponses standardisées `SuccessResponse {status:"ok", data}` / `ErrorResponse` (`app/models/responses.py`).
- `app/templating.py` : Jinja2 avec **`select_autoescape(["html"])`** (audit A10), globals `fd_icon`, `csrf_token`, `csp_nonce()`, `app_base_url()`.
---
## 3. Structure du backend
```
app/
├── main.py # FastAPI, middlewares, include_router ×40, lifespan/schedulers
├── config.py # pydantic-settings (.env), db_path, secrets, feature flags
├── db.py # baseline schema (105 tables au total avec migrations), get_conn()
├── migrations.py # @register(version) + schema_version, 1 migration = 1 transaction
├── templating.py # Jinja2 autoescape + globals
├── password_utils.py # hash SHA-256 + sel, validation force
├── auth/ # oauth.py, session.py, providers/ (oidc, saml)
├── middleware/ # csrf.py, security.py (CSP, rate limit, uploads)
├── models/ # requests.py / responses.py (Pydantic, partiel)
├── schemas/ # schémas API
├── templates/ # ~35 gabarits + fragments _*.html (§22)
├── routers/ # surface HTTP (§4) — découplés en packages par l'audit A28
│ ├── api_v2/ # API publique v2 : collections, properties, views, projects,
│ │ # planning, engagement, sharing, identity, workspaces,
│ │ # templates_io, webhooks, admin
│ ├── board/ # Kanban Gitea + pages legacy + wiki + synced + embed + import
│ ├── collections/ # Databases Notion-like (/db) : crud, properties, views,
│ │ # data_api, pages, linked, meta, boards, task_db, dashboards
│ ├── dashboard/ # Pages : local_workspace, workspaces, account_*, pages_*,
│ │ # public, settings
│ └── *.py # auth, sso, scim, webauthn, security, permissions, governance,
│ # audit, admin, agent, api_v2_agent, meetings, automations,
│ # workers, notifications, realtime, search, search_ai, sites,
│ # library, my_tasks, notes, projects, gitea, github_routes,
│ # workspace, sharing, sidebar_config, export, imports,
│ # web_clipper, sync, emoji, onboarding, wiki, collaboration,
│ # public_api, api, webhooks
└── services/ # logique métier (~80 modules, §11–21)
```
**Pattern** : les *routers* font l'authentification, la validation d'entrée et le rendu ; les *services* portent la logique (accès données, moteur de règles, intégrations). Les quatre plus gros domaines de routes ont été restructurés en **packages** (`api_v2`, `board`, `collections`, `dashboard`) par l'audit **A28** ; chaque package a un `_common.py` (dépendances partagées) et un `__init__.py` qui agrége ses sous-routeurs par scan de modules.
---
## 4. Routage & API
Carte des surfaces HTTP par préfixe (tous les routeurs sont déclarés dans `app/main.py`) :
| Préfixe | Fichiers | Nature | Contenu |
|---|---|---|---|
| `/` | `dashboard/` (pas de préfixe) | SSR | Pages : accueil/dashboard, `/library`, `/workspace…`, `/settings`, `/trash`, `/help`, `/welcome`, `/onboarding`, `accounts`, page éditeur `/pages/{id}` (+ variantes collection/embed), workspaces Gitea |
| `/board` | `board/` (13 modules) | SSR + API | Kanban Gitea (`/board/{owner}/{repo}/view/{kanban|detailed|table|status|teamload}`), pages legacy, médias, partage, sync, wiki, embed, import, `page-templates` |
| `/db` | `collections/` (13 modules) | SSR + API | CRUD bases, propriétés, vues, data API des lignes, pages de ligne, linked databases, meta, boards synchronisés, dashboards, conversion `task-db` |
| `/my-tasks` | `my_tasks.py` | SSR + `/my-tasks/api` | Dashboard transverses des tâches assignées (3 vues) |
| `/notes` | `notes.py` | SSR | Notes markdown par projet Gitea |
| `/workspace` | `workspace.py` | SSR + JSON | Workspaces locaux, arborescence, upload ; `public_router` exempté d'auth pour le partage |
| `/api` | `api.py`, `collaboration.py`, `sharing.py`, `security.py`, `sidebar_config.py`, `admin.py`, `agent.py`, `search.py`, `notifications.py`, `projects.py`, `gitea.py`, `github_routes.py`, `library.py`, `import.py`, `export.py`, `emoji.py`, `realtime.py`, `onboarding.py` | JSON cookie-auth | API internes du front : `/api/search`, `/api/settings/*` (tokens, sessions), `/api/agent/*` (panneau IA), `/api/nav/menu`, `/api/pages/{id}/comments|reactions|mentions|backlinks`, `/api/export/*`, `/api/health` |
| `/api/v1` | `public_api.py` | JSON Bearer | API publique v1 en **lecture seule** (compat) |
| `/api/v2` | `api_v2/` (14 modules), `permissions.py`, `api_v2_agent.py`, `sync.py`, `web_clipper.py` | JSON Bearer | **API publique v2 complète** : CRUD tous domaines + scopes, `/api/v2/agents/*` (run synchrone, rollback), `/api/v2/skills/*`, `/api/v2/search/{ask,hybrid}`, `/api/v2/sync/{batch,delta}`, `/api/v2/web-clipper/clip`, `/api/v2/sso/*`, `/api/v2/scim/tokens`, `/api/v2/audit/logs`, `/api/v2/groups`, `/api/v2/users`, OpenAPI `/docs` + `docs/openapi-v2.json` |
| `/api/webhook` | `webhooks.py` | JSON | **Webhooks entrants** Gitea (secret partagé, exemption CSRF) |
| `/auth` | `auth.py`, `webauthn.py`, `sso.py`, `scim.py` (en partie) | HTML/JSON | Login local/OAuth (`/auth/login`, `/auth/callback`), `/auth/2fa/*`, `/auth/webauthn/*`, `/auth/saml/*`, `/auth/oidc/*`, `/auth/logout` |
| `/scim/v2` | `scim.py` | JSON Bearer | Provisioning SCIM 2.0 (`/scim/v2/Users` GET/POST/PUT/PATCH/DELETE) |
| `/s/<slug>` | `sites.py` | HTML | **Sites publics** multi-pages (mot de passe, expiration, stats) |
| `/f/<token>` | `sites.py` | HTML/POST | **Formulaires publics** anonymes → collection |
| `/g/<token>` | `wiki.py` | HTML | Accès **guest** à une page sans compte |
| `/wiki` | `wiki.py` | JSON/SSR | Teamspaces, vérification, follows |
| `/api/v2/meetings` | `meetings.py` | JSON | AI Meeting Notes (upload audio, statut, résumé) |
| `/api/v2/workers` | `workers.py` | JSON | Workers Python sandboxés (cron/partage/fork) |
| `/workspace/automations` | `automations.py` | SSR/JSON | Éditeur visuel d'automations (steps) |
| `/static` | `StaticFiles("static")` | fichiers | JS/CSS vendorisés, manifest.json, sw.js, icônes |
### 4.1 API publique v2 — contrat
- **Auth** : Bearer `fd_<token>` (scopes hiérarchiques `read < write < admin`) **ou** session admin pour les routes sensibles (CSRF alors contrôlé *dans* le routeur, `/api/v2` étant exempté par la middleware).
- **Pagination / filtres / tri** systématiques, **erreurs RFC 7807**, **idempotence** par en-tête `Idempotency-Key` (table `idempotency_keys`, réponse rejouée), **rate limiting par jeton** (`API_V2_RATE_LIMIT_PER_TOKEN`), **audit** systématique (`api_audit_log`).
- **Ouverture** : OpenAPI servi sur `/docs` ; schéma exporté dans `docs/openapi-v2.json` ; guide complet `docs/API_GUIDE_V6.md`.
- **Sync offline** : `POST /api/v2/sync/batch` (rejeu des mutations PWA) + `GET /api/v2/sync/delta` (pull par `sync_version`) — backend `app/routers/sync.py`.
### 4.2 Plugins à effet réel sur le routage
Le registre `plugins` (`app/services/plugins.py`, v7.58) coupe **de vraies routes** quand un module est désactivé : `include_router(..., dependencies=[Depends(plugin_required("automations"))])` ou `("web-clipper")` dans `app/main.py` → toutes les routes du router renvoient 404, et le scheduler associé est arrêté. Les outils `web-tools` éteints retirent leurs outils du registre de l'agent (`PLUGIN_TOOLS`).
---
## 5. Modèle de données
> Référence exhaustive table par table (colonnes, FK, fichiers, numéros de ligne) : **`docs/DATA_MODEL.md`**. Cette section en donne la synthèse architecturale.
### 5.1 Moteur : SQLite uniquement
Contrairement à certaines notes anciennes, **FlowDeck ne supporte pas PostgreSQL**. `app/db.py` est du `sqlite3` stdlib typé `sqlite3.Connection`, sans ORM ni SQLAlchemy :
- une base par instance : `settings.db_path` (par défaut `sqlite:///…/flowdeck.db` sous `/data` en Docker) ;
- `PRAGMA journal_mode=WAL` (un writer, multi-readers), `foreign_keys=ON`, `busy_timeout=5000` pour la concurrence (tests xdist, schedulers) ;
- `get_conn()` = context-manager avec `row_factory = sqlite3.Row` ; les conversions dict↔JSON des colonnes `*_json` sont faites à la main dans les services ;
- `DATABASE_URL` n'est interprété que pour le préfixe `sqlite:///` (gestion des chemins Windows, bug UNC corrigé par l'audit A26) ;
- l'audit **A21** a sorti les accès SQLite bloquants de l'event loop (migration des ~510 call sites, achevée pour api_v2 et les routes principales v7.9–v7.26).
### 5.2 Migrations versionnées (`app/migrations.py`)
Alternative assumée à Alembic :
| Composant | Rôle |
|---|---|
| `init_db()` (`app/db.py`) | baseline « version 1 » : `CREATE TABLE IF NOT EXISTS` + ALTER `try/except OperationalError`, réexécutée à chaque boot, idempotente |
| `schema_version` | registre `(version, name, applied_at)` ; version max = version courante (actuellement **38**) |
| `@register(version, name)` | décorateur d'enregistrement, tri par version, doublons refusés |
| `apply_migrations()` | appelée en fin de `init_db()` ; applique exactement une fois chaque migration > version courante |
| `_apply_one()` | **1 migration = 1 transaction** (DDL tout-ou-rien, audit A31) |
| `fts5_available()` | détection FTS5, repli recherche `LIKE` sinon |
### 5.3 Inventaire : 105 tables + 1 table virtuelle, par domaine
| Domaine | Tables principales | Points notables |
|---|---|---|
| **Kanban & forge** (v0.x–v1.x) | `boards`, `cards`, `col_mapping`, `notes`, `checklists(_items)`, `project_properties`, `property_values`, `ai_keywords`, `gitea_private_pages`, `user_tokens`, `projects` | `projects` = registre forge-agnostique (gitea/github/builtin) synchronisé par cron |
| **Auth & identités** | `users`, `login_history`, `user_oauth_tokens`, `user_sessions`, `api_tokens`, `webauthn_credentials`, `scim_tokens`, `domain_claims`, `sso_config`, `sso_login_history`, `sso_requests` | `users.auth_method` : local / gitea / github / saml / oidc ; secrets chiffrés Fernet |
| **LLM** | `llm_config` (ligne unique `CHECK(id=1)`), `user_llm_keys` | clés API stockées serveur, jamais exposées en clair |
| **Workspaces & permissions** | `workspaces`, `workspace_members`, `teamspaces(_members)`, `user_groups`, `group_members`, `page/collection/property_permissions`, `permission_audit_log`, `page_shares`, `guest_shares` | ACL : grant sur user **XOR** group (`CHECK`) |
| **Collaboration** | `favorites`, `recents`, `notifications`, `comments`, `comment_reactions`, `text_reactions`, `page_follows`, `page_verifications`, `tags`, `page_tags`, `custom_emojis` | `comments` cible polymorphe (`page` ou `collection_page`) + ancre texte `anchor_start/end` (v7.64) |
| **Pages & éditeur** | `pages`, `page_versions`, `page_history`, `page_global_templates`, `synced_blocks`, `page_synced_blocks`, `page_views`, **`pages_fts`** (virtuelle FTS5) | `pages.content` = markdown legacy **ou** JSON de blocs selon `content_format` ; page-ombre de ligne via `pages.collection_row_id` (v6.5) ; trigger `sync_version` |
| **Collections (bases)** | `collections`, `collection_pages` (lignes), `collection_views`, `collection_properties`, `collection_data_sources`, `collection_dashboards`, `database_templates`, `page_templates`, `page_dependencies`, `sprints`, `sprint_pages`, `reminder_log` | `collections.schema_json` + `collection_pages.property_values_json` + `collection_views.config_json` = le trio documentaire du système de bases |
| **Automatisation** | `automations`, `automation_steps`, `automation_runs`, `workers`, `worker_runs`, `webhook_subscriptions` (créée hors migrations par `webhook_outbound.py`), `webhook_deliveries`, `idempotency_keys` | steps = chaîne trigger/condition/delay/action (v7.0) |
| **Agent IA** | `agents`, `agent_conversations`, `agent_messages`, `agent_actions`, `agent_skills`, `agent_triggers`, `agent_feedback`, `agent_policies`, `agent_approvals`, `agent_memory`, `agent_connectors`, `connector_tokens`, `plugins`, `semantic_embeddings`, `semantic_index_state` | `agent_actions.undo_snapshot_json` = journal annulable ; embeddings = vecteurs hashed-TF 256-dims en BLOB |
| **Sites & formulaires** (v6.8) | `sites`, `site_pages`, `site_views`, `form_responses` | RGPD : compteurs sans IP brute, `ip_hash` tournant |
| **Calendrier & réunions** (v7.1) | `calendar_links`, `meeting_transcripts`, `meeting_consent_log`, `meeting_distributions` | tokens OAuth calendrier chiffrés Fernet |
| **Import / offline / clipper** | `import_jobs`, `import_items`, `offline_sync_queue`, `extension_devices`, `extension_clips` | re-import idempotent par `(workspace, source, external_id)` |
| **API & audit** | `api_audit_log` | écriture « best-effort » : un échec d'audit ne casse jamais le chemin métier |
### 5.4 Relations clés
```mermaid
graph TD
USERS[users]
WS[workspaces] --> USERS
PAGES[pages] --> WS
PAGES -->|parent_id self| PAGES
COLS[collections] --> WS
CP[collection_pages lignes] --> COLS
CP -->|parent_id sub-items| CP
PAGES -->|collection_row_id page-ombre| CP
CV[collection_views] --> COLS
CPV[collection_properties] --> COLS
CPV -->|relation| COLS
CPV -->|button_automation_id| AUTO[automations]
AG[agents] --> WS
CONV[agent_conversations] --> AG
MSG[agent_messages + actions] --> CONV
GRP[user_groups] --> WS
ACL[page/collection/property_permissions] --> USERS
ACL --> GRP
SITES[sites] --> PAGES
TRANS[meeting_transcripts] --> PAGES
CAL[calendar_links] --> COLS
```
À retenir :
1. **`users` = hub d'identité** (30+ FK entrantes), **`workspaces` = hub d'isolation multi-tenant**.
2. **Deux univers de « pages »** : `pages` (éditeur wiki, soft-delete `deleted_at`, corbeille) et `collection_pages` (lignes de base). Le pont v6.5 : chaque ligne peut porter une **page-ombre** (`pages.collection_row_id`, CASCADE) qui héberge son contenu blocs — versions, synced blocks et temps réel opèrent sur elle.
3. **Chaînes de cascades** nettoyant l'éditorial ; les tables d'audit privilégient `SET NULL` sur l'acteur pour survivre à la suppression d'un compte.
4. **Sync Gitea par ID numérique** (`gitea_issue_id`, `gitea_owner/repo`) : jointures sans FK, système externe.
5. **Polymorphisme applicatif** : `notifications.resource_type/id`, `semantic_embeddings.resource_type/id`, `automations.collection_id` (TEXT) — cohérence maintenue par le code, pas par le moteur.
### 5.5 Couche Pydantic
`app/models/requests.py` / `responses.py` ne couvrent qu'une petite partie du contrat (FileSaveRequest, Issue{Create,Update}Request, CardMoveRequest, ColMappingRequest, enveloppes `SuccessResponse`/`ErrorResponse`) ; la majorité des handlers valide en dur.
---
## 6. Authentification & comptes
### 6.1 Types de comptes
Tous les comptes vivent dans la table unique `users` (colonne `auth_method`, défaut `local`) :
| Type | `auth_method` | Particularités |
|---|---|---|
| **Local** | `local` | `password_hash` (SHA-256 + sel, `app/password_utils.py`), min. 6 caractères ; le **premier inscrit local devient admin** |
| **OAuth forge** | `gitea` / `github` | login préfixé `{provider}_{login}` (ex. `gitea_alice`), upsert à chaque callback, token stocké dans `user_oauth_tokens` |
| **SSO provisioned** | `saml` / `oidc` | créé par `handle_sso_login()` si `auto_provision=1` ; **fusion par email** si le compte existe déjà |
| **SCIM** | `saml` | provisionné par `POST /scim/v2/Users` ; désactivation = `is_active=0` + révocation des sessions (jamais de suppression destructrice) |
| **Admin** | tout type | `is_admin=1` ; sur instance `sso_only`, seuls les admins conservent l'accès local |
| **Agent** | — | pas de compte de service dédié : l'agent agit **au plus avec les permissions de l'utilisateur appelant** ; la gouvernance passe par `agent_policies`/`agent_approvals` (§9.3) |
Un utilisateur sans rôle explicite retombe sur le rôle implicite **`viewer`** (`role_in_workspace()` dans `app/routers/dashboard/_common.py`).
### 6.2 Flux OAuth2 Gitea (+ GitHub)
- Providers déclarés dans `app/auth/providers/__init__.py` (`GiteaProvider`, `GitHubProvider`, registre `get_provider()`) + singleton legacy `GiteaOAuth` (`app/auth/oauth.py`). Un provider n'est « configuré » que si ses identifiants ne sont pas des placeholders (durcissement d'audit).
- `GET /auth/login?provider=gitea|github|local` → `{forge}/login/oauth/authorize?client_id&redirect_uri&response_type=code&state` ; `state` = `secrets.token_hex(32)` stocké dans la SessionMiddleware (1 h), avec le mode `link` (rattacher OAuth à un compte local depuis Réglages → Intégrations) encodé dans l'état.
- `redirect_uri` : override `OAUTH_REDIRECT_URI` sinon dérivé de la requête (`X-Forwarded-Proto/Host`), réutilisé tel quel au callback.
- `GET /auth/callback?code&state` : échange du code, profil via `GET {forge}/api/v1/user`, upsert `users`, stockage du token, émission de la cookie `flowdeck_session`, redirection `/workspaces`.
### 6.3 Comportement du sidebar selon le compte
La cookie `flowdeck_workspace` porte soit un id numérique (workspace local), soit `gitea:<owner>/<repo>` — auquel cas la sidebar affiche **l'arbre du dépôt Gitea *et* le miroir local du même nom en parallèle** (comportement conservé depuis v4.0, sections Home : Workspace / Repository / Meetings / Recents / Favorites / Agents / Teamspaces / Shared / Published / Private — voir §22.1).
### 6.4 Sessions (`app/auth/session.py`, v5.2.0)
- Cookie `flowdeck_session` : payload `{user, created_at, sid}` **signé** (`itsdangerous.URLSafeTimedSerializer`, `APP_SECRET_KEY`), non chiffré → `public_user()` strippe `password_hash` avant toute sortie ;
- `sid` = une ligne `user_sessions` (ip, user-agent, `revoked`, `last_seen_at`) : **révocation immédiate par session**, visible et gérable dans Réglages (`GET /api/settings/sessions`, `POST …/revoke`) ;
- expiration **7 jours** (cookie + signature cohérentes) ; contrôle `revoked` à chaque décodage, **fail-open** si la table est inaccessible (compat vieilles installations) ;
- verrouillage : 5 échecs → 15 min (`locked_until`, HTTP 423) ; `is_active=0` → 403 ;
- historique dans `login_history` ; déconnexion SSO → relais SLO si `_sso_name_id` présent.
### 6.5 Jetons API Bearer
- `POST /api/settings/tokens` (`app/routers/security.py`) crée `fd_<token_urlsafe(24)>`, affiché **une seule fois**, stocké **hashé SHA-256** (`api_tokens.token_hash`, `token_prefix` pour l'affichage) ;
- **scopes hiérarchiques** `read < write < admin` (`SCOPE_RANK`, `app/services/api_v2_helpers.py`) ; factory `require_scope()` (audit A30) ;
- `resolve_bearer_token()` accepte 3 sources : `api_tokens`, `extension_devices` (Web Clipper), `user_tokens` legacy (token Gitea en clair, portée implicite read+write) ;
- fallback dev `fd-public-key` uniquement si `PUBLIC_API_INSECURE_OK=true`.
---
## 7. SSO entreprise : SAML, OIDC, SCIM, 2FA, WebAuthn
(v6.7.0 SSO ; v7.2.0 SCIM/2FA/passkeys — doc `docs/V6_SSO_SAML_Enterprise_Auth.md`, `docs/V72_Enterprise_SCIM_2FA.md`)
### 7.1 SAML 2.0 (`app/auth/providers/saml_provider.py`, `app/routers/sso.py`)
SP basé `python3-saml` (OneLogin), mode strict :
- `GET /auth/saml/login?next=…` (AuthnRequest HTTP-Redirect, `RelayState = <id>.<jeton CSRF>`) ; `POST /auth/saml/callback` (ACS, exempté CSRF car POST cross-site mais protégé par le jeton single-use) ; `GET /auth/saml/metadata` (XML à coller dans l'IdP) ; `GET|POST /auth/saml/logout` (SLO).
- Validations : signature d'assertion (`wantAssertionsSigned`, RSA-SHA256), schéma XML, Conditions/Audience/Destination/Issuer/Status, `InResponseTo`, rejet des algorithmes dépréciés.
- Clé privée SP générée à la demande et **chiffrée au repos (Fernet)** dérivée de `APP_SECRET_KEY`.
### 7.2 OpenID Connect (`app/auth/providers/oidc_provider.py`)
Flow **authorization code + PKCE (S256)**, client confidentiel : découverte `.well-known/openid-configuration` (cache 1 h), `GET /auth/oidc/login` → `GET|POST /auth/oidc/callback` (form_post supporté). Vérification ID token contre la JWKS (sélection par `kid`) + contrôles explicites `iss/aud/exp/iat` (dérive 300 s), `nonce`, `sub`. Enrichissement best-effort via `userinfo_endpoint` (source habituelle des groupes). Logout : révocation locale + `end_session_endpoint` si présent.
### 7.3 Provisioning & config (`app/services/sso_provisioning.py`)
- Table `sso_config` (une ligne active) : type, mapping d'attributs, `groups_mapping`, `auto_provision`, `sso_only`, `default_workspace_id` ; la table prime sur les variables d'env `SSO_*` (fallback bootstrap) ; secrets chiffrés Fernet, jamais rendus par `GET /api/v2/sso/config` ;
- `handle_sso_login()` : résolution par **email (fusion)** puis **login** ; création si `auto_provision`, sinon rejet ;
- `sync_sso_groups()` : mapping groupe IdP → rôle `workspace_members` (owner/admin/editor/viewer), réappliqué à chaque login ; `POST /api/v2/sso/sync` force le re-sync global ;
- **anti-replay** : table `sso_requests` (TTL 600 s, consommation atomique) ; anti open-redirect : `safe_next_path()` n'accepte que `/chemin` ;
- `sso_only` : refuse login/enregistrement local pour les non-admins ; les **domaines vérifiés** `enforce_sso=1` (`domain_claims`) imposent le SSO aux comptes de ce domaine email ;
- chaque login/refus écrit `sso_login_history` ; rate limit dédiée 5 tentatives/min/IP ; API admin `GET /api/v2/sso/providers` (boutons de la page de login).
### 7.4 SCIM 2.0 (`app/routers/scim.py`)
`/scim/v2/Users` (GET liste, POST create, GET/PUT/PATCH/DELETE). Auth Bearer contre `scim_tokens.token_hash` (SHA-256, `revoked=0`). `active=false` ou DELETE = **suspension** (`is_active=0`) + révocation des sessions — contenu et audit préservés. Gestion des jetons : `POST|GET/DELETE /api/v2/scim/tokens` (préfixe `scim_`, valeur affichée une seule fois).
### 7.5 2FA TOTP (`app/services/two_factor.py`)
`POST /auth/2fa/setup` → secret `pyotp` + `otpauth://` (inactif tant que non vérifié) ; `/2fa/activate` (fenêtre ±1 pas) → `users.totp_secret_enc` chiffré Fernet + **10 backup codes** à usage unique (hashés) ; `/2fa/status`, `/2fa/disable`. Parcours login : `/auth/local-login` renvoie `{status:"2fa_required", pending}` (jeton signé 5 min, salt `totp-pending`), puis `/auth/local-verify` échange pending + code contre la cookie de session — **aucune session avant la seconde facteur**.
### 7.6 WebAuthn / passkeys (`app/routers/webauthn.py`)
`/auth/webauthn/register/begin|finish` (session requise, `excludeCredentials`), `/auth/webauthn/login/begin|finish` (**passwordless**), `GET /auth/webauthn/keys`, `DELETE …/keys/{id}`. Challenges en mémoire (TTL 300 s — déploiement mono-processus assumé, comme pour les rooms WebSocket) ; `rp_id` dérivé de l'hôte de la requête ; table `webauthn_credentials`, `sign_count` entretenu.
---
## 8. Sécurité applicative
Récapitulatif des garde-fous (la grande majorité provient de l'audit sécurité v7.3→v7.35, A1–A43).
### 8.1 CSRF (`app/middleware/csrf.py`)
Double soumission : cookie `csrf_token` (JS-readable, samesite=lax, 1 j) validée par `secrets.compare_digest` contre l'en-tête `X-CSRF-Token` sur POST/PUT/PATCH/DELETE. **A19 (v7.33)** : plus aucun préfixe cookie-auth exempté (46 sites du front équipés en v7.3.6) ; restent exemptés uniquement le machine-to-machine (`/api/webhook`, `/api/v1`, `/api/v2`, `/scim/v2`), les callbacks d'authentification (`/auth/*`), le public (`/s/`, `/f/`) et l'infrastructure (`/api/csrf-token`, `/api/frontend-error`). Compensations : jeton single-use `sso_requests`, validation complète assertion/ID token, et **contrôle CSRF dans le routeur** pour les routes `/api/v2` sensibles acceptant une session (mix session+Bearer). Le front porte le jeton via `<body hx-headers>` (htmx) et `fetch` manuel.
### 8.2 CSP (`app/middleware/security.py`) — audit A20
`script-src 'self' 'nonce-…'` — **ni `unsafe-inline` ni `unsafe-eval`** (achevé v7.43) :
- nonce par requête exposé aux templates via la ContextVar `CSP_NONCE` **avant** `call_next` (A43), global Jinja `csp_nonce()` ;
- `script-src-attr 'unsafe-inline'` **détaché** pour couvrir les ~74 handlers `onclick=` Alpine ;
- htmx neutralisé : `<meta name="htmx-config" content='{"allowEval": false, "inlineScriptNonce":…}'>` (v7.37) ;
- Alpine servi en **build officiel `@alpinejs/csp`** (0 `eval`/`new Function`) ;
- `connect-src 'self' ws://… wss://…` fermé (A20 phase 2 : CDN retiré, vendors localisés — test `test_csp_no_cdn_and_vendor`), `object-src 'none'`, `base-uri 'self'`, `form-action 'self'`.
### 8.3 Rate limiting & CORS
- fenêtre glissante in-memory par IP sur `/api/`, `/board/api/`, `/auth/`, `/scim/v2/`, `/workspace/`, `/db/` ; `RATE_LIMIT_REQUESTS` lu à chaud, purge du store à 5000 clés, `X-Forwarded-For` honoré uniquement derrière proxy privé (A33) ; pages publiques `/s/`, `/f/` : seuls les non-GET plafonnés ;
- CORS A37 : origines fermées issues de `APP_BASE_URL` + regex localhost/extensions navigateur, `allow_credentials=True`, jamais `*`+credentials ;
- rate limit par jeton API v2 (`API_V2_RATE_LIMIT_PER_TOKEN` req/min), 300 req/min pour le run agent synchrone, 30 req/min pour Ask AI.
### 8.4 SSRF, uploads, XSS
- garde-fou SSRF aux points de sortie HTTP (pas en middleware) : `_is_public_host()` (`app/services/importers/url_fetch.py`) rejette les hôtes `private/loopback/link-local/reserved/multicast` ; `app/services/og_fetcher.py` re-vérifie l'hôte **à chaque redirection** (A12) ; `app/services/connectors.py` impose la garde sur tout fetch ;
- uploads : liste `ALLOWED_EXTENSIONS` + 10 Mo max (`validate_upload`) ;
- XSS : autoescape Jinja2 global (A10, 326 interpolations crues corrigées) ; sérialisation markdown échappée de l'éditeur (`gtMd`/`mdEsc`).
### 8.5 Chiffrement au repos
Fernet (dérivé de `APP_SECRET_KEY`) sur : secrets SSO (`client_secret`, clé SP), `totp_secret_enc`, `calendar_links.tokens_enc`, `connector_tokens.tokens_enc`, `agent_connectors.secret_encrypted`. Hash SHA-256 pour : `api_tokens`, `scim_tokens`, `extension_devices`, backup codes. Note factuelle : le **mot de passe local est SHA-256 + sel** (rapide, pas bcrypt/argon2 — choix « SQLite simplicity » assumé en commentaire de `app/password_utils.py`).
### 8.6 Audit de sécurité A36 & exceptions muettes
Les `except: pass` silencieux ont été loggés (v7.3.9), l'API `/api/admin/*` est réservée aux admins, les logs d'exceptions sont tracés, et `scripts/audit_functional.py` rejoue un audit bout-en-bout sur base SQLite isolée (v7.46).
---
## 9. Autorisation : rôles, permissions granulaires, gouvernance
### 9.1 Couche 1 — rôles de workspace
`workspace_members.role` : **owner > admin > editor > commenter > viewer** (`app/services/permission_manager.py`). Ensembles sémantiques : `READ_ROLES`, `WRITE_ROLES = {editor, admin, owner}`, `DESTRUCTIVE_ROLES = {admin, owner}`. Le propriétaire d'un workspace a le rôle implicite owner ; tout utilisateur sans ligne explicite retombe sur **viewer**.
### 9.2 Couche 2 — ACL granulaires (v6.0/v6.1)
Trois ressources protégées par grant explicite, sur un **user OU un groupe** (`user_groups`/`group_members`, workspace-scopés, `CHECK user_id XOR group_id`) :
| Ressource | Rôles | Table |
|---|---|---|
| Page (éditeur) | viewer / commenter / editor / owner | `page_permissions` |
| Collection (base) | viewer / commenter / editor / owner | `collection_permissions` |
| Propriété d'une base | viewer / editor | `property_permissions` |
Chaque ressource porte un `permission_type` : `inherit` (suit la chaîne page → collection → workspace), `restricted`, `private` (seuls les grants explicites font foi). **Règle de résolution : moindre privilège** — un grant explicite surcharge la chaîne héritée, le meilleur rang gagne (`_explicit_grant_role`). Cache de résolution mémoïsé 60 s, invalidé à chaque mutation. Les rôles SSO se matérialisent par la ligne `workspace_members` du mapping groupe → rôle (`get_sso_roles`).
API (`app/routers/permissions.py`, sous `/api/v2`) : `GET|POST /pages/{id}/permissions` (+ `permissions/batch`, `DELETE …/{perm_id}`, `POST …/permission-type`), homologue pour `/collections/{id}/permissions` + `…/properties/{pid}/permissions` + `/properties/visible` (visibilité de propriétés) ; CRUD groupes `/api/v2/groups[/{id}[/members[/{user_id}]]]` (réservé admin/owner de workspace) ; sélecteur d'utilisateurs `/api/v2/users`. Chaque grant/revoke/type_change/group_*/member_* écrit une ligne **immuable** `permission_audit_log` (lecture `GET /api/v2/audit/permissions`, owner/admin, limit ≤ 500).
### 9.3 Gouvernance de l'agent (v7.2)
`app/services/agent_policies.py` + `app/routers/governance.py` : table `agent_policies` (ligne globale `workspace_id IS NULL` ou override par workspace) avec `allowed_tools_json` (liste blanche), `max_steps ≤ 50`, `require_approval`. `check_tool()` est consulté **avant** `assert_can()` par l'AgentEngine. En mode `require_approval`, un appel d'écriture crée une demande dans `agent_approvals` (pending/approved/rejected), **suspend l'action** et émet le webhook `agent.run.approval_requested` ; `decide_approval()` approuve/rejette. Le gate d'outil exige editor+ pour les `WRITE_TOOLS`, admin/owner **et mode confirm** (HTTP 428) pour les `DESTRUCTIVE_TOOLS`. API : `GET|POST /api/v2/agent-policies`, `GET /api/v2/agent-approvals`, `POST /api/v2/agent-approvals/{id}/decide` (owner du workspace ou admin).
### 9.4 Journal d'audit unifié (v7.2)
Quatre sources, toutes en écriture « best-effort » (un échec d'audit ne casse jamais le chemin métier) :
| Table | Écrit par | Contenu |
|---|---|---|
| `api_audit_log` | `audit_log()` (`api_v2_helpers.py`) | user_id, token_id, action, resource, ip, détail ≤ 1000 car. |
| `permission_audit_log` | `PermissionManager.log_permission_change()` | mutations d'ACL (§9.2) |
| `sso_login_history` | `sso_provisioning.log_sso_login()` | chaque tentative SSO réussie **ou rejetée** |
| `login_history` | `auth._log_login` | login local (ip + user-agent) |
API unifiée **`GET /api/v2/audit/logs`** (`app/routers/audit.py`, admin only — session admin ou Bearer scope `admin`) : fusion par `source=all|api|permissions|sso`, filtres `actor`/`action`, `limit ≤ 500`, **export CSV** `?format=csv` (10 000 lignes, rétention visée 365 j). Vues annexes : `GET /api/v2/sso/history`, `GET /api/v2/audit/permissions`, `GET /api/admin/audit`.
---
## 10. Workspaces & collaboration multi-utilisateurs
### 10.1 Modèle de workspace
- **`workspaces`** : espace de travail multi-utilisateur (`owner_id → users`, `settings_json`) ; la table `workspace_members` définit le rôle de chacun (§9.1).
- Deux familles exposées dans l'UI : **workspaces locaux** (explorateur `local_workspace.html` — arbre, fil d'Ariane de dossiers, sélection multi-fichiers Maj/Ctrl, uploads avec progression/vitesse, vues table/liste/détails/cartes — routes `/workspace…` de `app/routers/dashboard/local_workspace.py`) et **espaces Gitea** (un dépôt = pseudo-workspace `gitea:<owner>/<repo>`, navigateur `gitea_workspace.html`, pages privées miroir `gitea_private_pages`).
- **Teamspaces** (v7.3) : namespaces de pages + bases **au sein d'un workspace**, avec leurs propres membres/rôles (`teamspace_members`) et confidentialité (`private=1` → 404 pour les non-membres) — §14.4.
- Le workspace courant est mémorisé par la cookie `flowdeck_workspace` ; les listes de ressources résolvent le rattachement via `effective_workspace_sql`.
### 10.2 Collaboration
| Fonctionnalité | Implémentation |
|---|---|
| Commentaires threadés | `comments` (cible polymorphe `page`/`collection_page`, réponses par `parent_id`, ancre inline `anchor_block_id` + `anchor_start/end`) — endpoints `/pages/{id}/comments` dans `app/routers/collaboration.py` |
| Réactions | `comment_reactions` (emoji par commentaire) + `text_reactions` (plage de texte d'un bloc, v7.64) |
| Mentions & suivis | notifications `@`/commentaires/assignations ; abonnements `page_follows` → notification `page.updated` |
| Favoris / Récents | `favorites`, `recents` (`source_type` local/gitea/… — « Recents réels » sidebar v7.69.6) |
| Historique de page | `page_versions` (snapshot complet `blocks_json` par save, undo/restore) pour l'éditeur ; `page_history` (legacy) pour les lignes de base |
| Templates | `database_templates` (bases prêtes, seed `db_templates.py`), `page_templates` (lignes, éventuellement récurrentes), `page_global_templates` (blocs) |
| Tags | `tags` (`user_id=0` = globales) + `page_tags` |
| Icônes | `custom_emojis` uploadés par workspace (`app/routers/emoji.py`) |
| Verrouillage | `pages.is_locked`/`locked_by` + mode « Suggest edits » (v5.12) |
| Vues analytiques | `page_views` (compteur quotidien par page, v7.3) |
### 10.3 Gestion des conflits d'édition
Trois mécaniques selon la surface :
1. **Temps réel éditeur** : merge 3-voies versionné (§13.4) avec drapeau de conflit champ-par-champ ;
2. **Hors temps réel** : colonne `sync_version` entretenue par triggers SQLite (`*_sync_version_bu` sur `pages`/`collection_pages`/`collections`) = verrou optimiste ; l'API offline s'appuie dessus (`GET /api/v2/sync/delta`) et `POST /api/v2/sync/batch` rejoue les mutations en file avec résolution de conflits ;
3. **Lignes de base hors-ligne** : last-write-wins par cellule ; calendrier synchronisé : last-write-wins + notification `calendar.conflict`.
---
## 11. Databases : propriétés, formules, rollups, dépendances
Le moteur « base de données Notion-like » vit sous `/db` (`app/routers/collections/`, 13 modules), avec la logique dans `app/services/property_types.py`, `formula_engine.py`, `rollup_engine.py`, `collection_adapter.py`, `collection_lifecycle.py`, `row_pages.py`, `task_databases.py`.
### 11.1 Architecture : schéma déclaratif + valeurs JSON
`collection_properties` porte la **définition** (type, options, validation, format, cible de relation, expression, agrégat) ; chaque ligne (`collection_pages`) stocke ses **valeurs** dans `property_values_json` (`{property_name → valeur}`). Conséquences : ajouter une propriété ne demande **aucune migration de table** ; le requêtage se fait via `json_extract()` SQLite ; `collections.schema_json` garde une copie déclarative du schéma complet.
```
collection_properties (schéma) collection_pages.property_values_json
┌──────────────────────────┐ ┌──────────────────────────────┐
│ name: "Status" │ │ { "Status": "Done", │
│ prop_type: "status" │ ↔ │ "Priority": "P1", │
│ options_json: [Todo, │ │ "DueDate": "2026-07-15", │
│ Done] │ │ "Assignee": ["bruno"], │
│ │ │ "Tags": ["page_42"] } │
└──────────────────────────┘ └──────────────────────────────┘
```
### 11.2 21 types de propriétés
| Catégorie | Types | Stockage exemple |
|---|---|---|
| Simple | `title`, `text`, `number`, `select`, `multi_select`, `status` (+ couleur), `date` (ISO 8601), `person`, `checkbox`, `url`, `email`, `phone`, `files` | `"En cours"` ; `["Frontend","Backend"]` ; `[{"id":1,"login":"bruno"}]` ; `[{"url":"…","name":"img.png"}]` |
| Avancés | `unique_id` (auto-incrément) ; `relation` (bidirectionnel inter-collections, `related_collection_id` + `relation_property_id/target_property_id`) ; `rollup` (agrégat via relation, `rollup_function`) ; `formula` (`formula_expression`) ; `button` (déclenche une automatisation, `button_automation_id`) | `[42, 57]` ; valeurs calculées |
| Auto (calculées serveur) | `created_time`, `created_by`, `last_edited_time`, `last_edited_by` | horodatages/acteurs |
Vérification : `validate_property_value()` par type + contraintes `validation_json` (required / unique / min / max, mig. 4). Regroupement UI par `group_name`. Sous-ensemble `SIMPLE_TYPES` pour l'import/export CSV.
### 11.3 Formula Engine (`app/services/formula_engine.py`)
Moteur d'expressions JavaScript-like, **19 fonctions** : `prop`, `now`, `today`, `if`, `concat`, `round`, `contains`, `length`, `toNumber`, `formatDate`, `dateAdd`, `dateSubtract`, `replace`, `replaceAll`, `join`, `empty`, `and`, `or`, `not`. L'expression est stockée dans `collection_properties.formula_expression` et évaluée à la lecture.
### 11.4 Rollup Engine (`app/services/rollup_engine.py`)
Agrégats calculés au travers d'une propriété `relation` vers la collection cible : **12 fonctions** — `count`, `count_values`, `empty`, `not_empty`, `sum`, `average`, `median`, `min`, `max`, `range`, `unique`, plus les concaténations de listes. Le bloc `progress` de l'éditeur réutilise le RollupEngine pour sa barre.
### 11.5 Sub-items & dépendances (v1.8)
- **Sub-items** : `collection_pages.parent_id` (auto-référence, hiérarchie illimitée) ; agrégation de statut — le parent passe `Done` quand tous ses enfants le sont.
- **Dépendances** : `page_dependencies(page_id, dependency_id, dependency_type)` — Blocking/Blocked by, **contrainte à la transition de statut** (impossible de passer Done une tâche qui en bloque une ouverte) et `auto_shift` (décalage des dates en cas d'overlap).
### 11.6 Sprints & bases de tâches
- **Sprints** : `sprints` + `sprint_pages` (points de vélocité, statut au démarrage, `auto_complete`) par base de tâches.
- **Task databases** (v7.47) : une collection marquée `is_task` avec mapping explicite `task_assignee_prop` / `task_status_prop` / `task_due_prop` → colonnes existantes — c'est la clé de My Tasks (§15.3) : le front envoie des **champs logiques** (`status`, `due`, `title`), jamais d'identifiant de colonne ; le serveur résout.
- **Linked databases** (v4.1) : `collection_data_sources` — une page peut monter une vue liée d'une collection source (`is_linked`), schéma partagé.
---
## 12. Système de vues
### 12.1 Config JSON d'une vue
`collection_views` (`view_type` + **`config_json`**, `position`, `created_by` NULL = vue partagée sinon personnelle — mig. 10). Persistance via `PUT /db/views/{id}/config` et `POST /db/{id}/views/save-as`. Exemple :
```json
{
"group_by": "Status",
"sub_group_by": "Assignee",
"filters": [
{"property": "Status", "operator": "is_not", "value": "Archivé"},
{"property": "DueDate", "operator": "is_after", "value": "2026-01-01"}
],
"filter_conjunction": "and",
"sorts": [
{"property": "Priority", "direction": "asc"},
{"property": "DueDate", "direction": "desc"}
],
"visible_properties": ["Title", "Status", "Assignee", "DueDate"],
"card_size": "medium",
"cover_property": "Files",
"date_property": "DueDate",
"date_range_property": "EndDate"
}
```
### 12.2 Catalogue des vues (trois surfaces)
1. **Vues interactives client** (`static/js/database_table.js`, composant `DBInstance` — pages `/db/{id}`, `page_editor_collection.html`, blocs `database` du plan d'un document) : **Table** (par défaut), **Board/Kanban** (group par select/status/person/multi_select/text, **swimlanes** `sub_group_by`, WIP limits, taille et couvertures de carte), **Calendar** (grille mensuelle, drag & drop d'événements), **Gallery** (card size S/M/L, cover icon/color/property, chips de propriétés), **List** (compacte avec preview).
2. **Vues rendues serveur** (`app/routers/collections/_renderers.py`, dispatch `_render_view` appelé par `dashboard_views.py`, route `/db/{id}?view_type=…`) : les mêmes + **Timeline** (barres Gantt horizontales), **Gantt**, **Chart** (bar/line/pie/doughnut/scatter via Chart.js vendorisé + widgets **KPI** count/sum/avg/min/max, plafonné `CHART_MAX_GROUPS`), **Form** (formulaire générateur de lignes — alimente aussi les formulaires publics `/f/`), **Map** (Leaflet vendorisé, géocodage `lat,lng` depuis les propriétés), **Feed** (flux chronologique).
3. **Board Gitea legacy** (`app/routers/board/board_views.py`, `board.html`, SSR/htmx `GET /board/{owner}/{repo}/view/{view}`) : **kanban** (`board_fragment.html`), **detailed**, **table** (`table_view.html`), **status overview** (`status_overview.html` — donut SVG), **team load** (`team_load.html` — barres empilées), rendu depuis les issues du dépôt.
### 12.3 Chaîne de rendu
```
GET /db/{id} (vue sauvegardée)
→ charger collection_views (view_type + config_json)
→ charger collection_pages WHERE collection_id
→ appliquer config_json.filters (conjunction and/or)
→ appliquer config_json.sorts
→ grouper par group_by (sous-groupes sub_group_by pour les swimlanes)
→ projeter visible_properties (alléger le payload)
→ rendu : fragment client (database_table.js) ou SSR selon la surface
```
Les **dashboards** (`collection_dashboards.layout_json`) combinent plusieurs vues/widgets en colonnes sur une page ; les **vues de site** publiées (`site_views`) sont une projection en lecture des vues de collection.
---
## 13. Pages, éditeur de blocs & temps réel
### 13.1 Le modèle `pages`
`pages` est la table pivot du produit : hiérarchie (`parent_id` auto-référence, `sort_order`),rattachement (`workspace_id`, `collection_id` pour une base plein écran, `collection_row_id` pour la page-ombre d'une ligne de base, `teamspace_id`), contenu (`content` = markdown legacy **ou** JSON de blocs selon `content_format`), publication (`published`/`is_published`/`publish_slug`/`is_shared`), aspect (`cover_url`, `page_icon`, `full_width`, `font_small`), cycle de vie (`deleted_at` = corbeille), contrôle (`is_locked`/`locked_by`, `permission_type`, `search_excluded`, `sync_version`).
### 13.2 Types de blocs
Le menu slash et le « Turn into » de l'éditeur (`static/js/page_editor_scripts.js`, rendu serveur `app/services/wiki_blocks.py`) :
| Famille | Blocs |
|---|---|
| Texte | `paragraph`, `heading_1…4`, `bulleted_list`, `numbered_list`, `to_do`, `toggle` (enfants `children`, `expanded`), `quote`, `callout` (icône + couleurs), `divider` |
| Technique | `code` (sélecteur de langue, Prism/hljs), `table_of_contents` (rendu client), `math` (bloc KaTeX), `equation_inline` (`$$…$$`), `mermaid` (SVG via `mmdc` si installé, sinon rendu navigateur) |
| Données | `table` (grille éditable `.ftable-editor`, GFM sans pipe de tête optionnel), `database` (base inline), `button` (Bloc bouton → automatisation, `automation_id`), `progress` (barre calculée par le RollupEngine) |
| Médias | `image`, `embed` (upload/fichier, `embed_type:download`), `bookmark` (unfurl OG), `video`, `audio` |
| Structure | `columns` (2/3/4/5 colonnes, `.block-column-wrapper` ×N), `synced` (bloc synchronisé), `meeting` (AI Meeting Notes) |
### 13.3 Mécanique d'édition
- **Markdown en saisie continue** : `checkMd` (`# `, `- `, `1. `, `[] `, `> `, `---`, ``` `` ```…), Enter prolonge les listes, Backspace/Delete partagent la garde « bloc vide → supprimer le bloc » (jamais le dernier) ;
- **Sérialisation DOM→markdown `gtMd()`** : gras/ital/souligné/barré/code/surlignage `==…==`/liens ; les chips wiki gardent leurs tokens (`gtTok`) ;
- **Handle ⋮⋮ + réordonnancement SortableJS** ; **bouton « + »** à gauche du handle (insert below, v7.60) ; **Maj-clic = sélection multi-blocs** ;
- **Menu contextuel de bloc façon Notion** (v7.61) : recherche d'action + 10 actions (Turn into ›, Color ›, Copy link to block `Alt+⇧+L`, Duplicate, Move to, Delete, Comment, Suggest edits, Ask AI, Skills ›), pied « Last edited by… / N words » ; sous-menu Color mémorise le dernier usage (`localStorage`) ;
- **Toolbar de sélection sur texte surligné** (v7.62–7.67) : 4 lignes (Turn into ; A/B/I/U/T/Tx ; lien/code/équation ; 💬 Comment + réaction) + Skills + « Edit with AI » ;
- **Presse-papiers multi-blocs** (v7.68–7.69) : copier/couper/coller de plages partielles → blocs entiers → plage partielle, Ctrl+C/X intercepté au `keydown` capture (le navigateur ne sérialise pas une sélection traversant deux `contenteditable`) ;
- **Coller intelligent** : `handleSmartPaste` → `md2b`/`paste2b` (markdown/GFM → blocs structurés) ;
- **Undo/redo** par snapshots `page_versions` (`pushHistory`, une ligne par save) + **autoSave debouncé** ;
- **Menus mobiles** (v7.63, ≤768 px) : barre horizontale flottante au `/` (15 boutons) + feuille plein écran « Insert block » ;
- **IA dans l'éditeur** : slash commands AI, autocomplétion inline (`AIAC`), panneau « Notion AI » Ctrl+J, remplissage de propriétés (`POST /api/agent/writing*`, §16.4).
### 13.4 Temps réel (`app/services/realtime_server.py`, `realtime_merge.py`, `app/routers/realtime.py`)
Passerelle WebSocket **`/ws/pages/{page_id}`**, auth par cookie `flowdeck_session` (fermeture `4401` sinon). Rooms **en mémoire par page** (uvicorn mono-worker), persistance `page.content` avec debounce, file sortante + tâche writer par connexion (broadcast non-bloquant, curseurs coalescés 1/flush, éjection propre du client trop lent code `4413`, anti-flood 400 ops/10 s).
Protocole JSON (champ `t`) : client→serveur `hello`, `sync_req`, `ping`, `op` (insert|update|delete|move + `base` + `v`), `title`, `sel` ; serveur→client `sync`, `ack` (`v`, `stale`, `merged`, `conflict`), `op` broadcast (**le bloc fusionné est diffusé**, jamais la proposition brute), `sel`, `welcome`, `peer_join/leave`, `synced_update`.
**Merge « CRDT-lite » 3 voies** (`realtime_merge.py`) : au-delà du LWW, chaque `update` porte la version `base` dont il dérive ; règle champ-par-champ — inchangé d'un côté ⇒ l'autre gagne ; identiques ⇒ sans conflit ; régions disjointes ⇒ `merge_text_3way` garde **les deux saisies** (diff3-lite par trim préfixe/suffixe commun) ; chevauchement ⇒ LWW par champ **avec drapeau de conflit** renvoyé au client. Le client (`page_editor_realtime.js`) gère présence (avatars déterministes), curseurs live (offsets restaurés), resynchro complète si `stale`, et **repli polling 10 s** si le WS tombe.
---
## 14. Wiki, liens, blocs synchronisés & teamspaces
### 14.1 Liens wiki & mentions
Les références inline vivent dans le texte brut des blocs sous forme de **tokens** `[[fdpage:ID]]` et `[[fddate:YYYY-MM-DD[Thh:mm]]]` ; le label est résolu **au rendu** — renommer une page la met donc à jour partout. Le sélecteur de mentions (`WM`, `/api/wiki/pages`) remplace « [[ » tapé par une chip non éditable qui re-sérialise en token.
### 14.2 Backlinks
`GET /api/pages/{page_id}/backlinks` (`app/routers/board/pages.py`) : « Lié depuis… » en scannant blocs et markdown brut de toutes les pages non supprimées, tri par fraîcheur.
### 14.3 Blocs synchronisés (transclusion, v5.14 → prod v6.5)
`synced_blocks` = **source de vérité** du contenu ; `page_synced_blocks` = références (page, index). L'édition de la source se propage : rooms temps réel via le message `synced_update`, ou re-résolution à la lecture (`resolve_content_json`, garantie de fraîcheur). Source supprimée ⇒ état explicite `_synced_deleted` (plus de « Loading… » éternel).
### 14.4 Teamspaces & wiki d'équipe (v7.3)
`app/services/wiki.py` + `app/routers/wiki.py` (doc `docs/V73_Wiki_Teamspaces_Polish.md`) : namespaces de pages/bases partagées avec rôles et confidentialité (`private=1` → 404), **badges de vérification ✅ avec expiration 90 j** (`POST /api/v2/wiki/pages/{id}/verify`), **abonnements** (`page_follows` → notification `page.updated`), **réactions** (`comment_reactions`, `text_reactions`), **partages invités sans compte** `/g/<token>` (`guest_shares` : token, rôle, expiration, révocation), compteurs de vues journaliers (`page_views`).
---
## 15. Partage, publication, Library, My Tasks & Trash
### 15.1 Sharing & Publish
- **Partage de page** (`page_shares`) : vers un user, un groupe ou un email, permission view/edit/comment ; sections sidebar « Shared → Par moi / Avec moi » et onglet Library correspondant ;
- **Publication** : drapeaux unifiés `published` + `is_published` (+ `publish_slug` garanti, v7.69.6) → page publique rendue par `public_page.html` (thème sombre autonome, OG tags, `og:image` en URL absolue via le global `app_base_url()` v7.69.1) ;
- **Sites publics** (`app/routers/sites.py`, v6.8, doc `docs/V68_Sites_Forms.md`) : mini-sites multi-pages `/s/<slug>` (navigation, verrou **mot de passe + expiration**, SEO, `noindex`, analytics id, stats de consultation sans IP brute `site_views`, domaine personnalisé `custom_domain`) ;
- **Formulaires publics** `/f/<token>` : champs générés depuis `collection_properties` (hors formules), validation, **rate limiting**, soumissions anonymes vers la collection (`form_responses` + `collections.form_config_json`), notification aux membres, événement `form.submitted` pour les automations ;
- **Partage public de base** (`/workspace` public_router, `share_mode`) et API de partage `app/routers/sharing.py` + `api_v2/sharing.py`.
### 15.2 Library
`app/templates/library.html` + `static/js/library.js` (API `/api/library`, `app/routers/library.py`) — page « toutes mes ressources » à **onglets** : Recents, **AI Meeting Notes** (regroupées Today/Yesterday/This week/Last week/This month/Older, v7.69.6), Favorites, Shared (sous-filtre direction Tous/Par moi/Avec moi), Private, Published, Workspace, Repository (Gitea). Tableau à **colonnes pilotées par « Show columns »** (Created by, Last edited by/time, Last visited time, Source avec icône — Page name verrouillée, ordre des en-têtes corrigé v7.69.8) ; recherche, sélection multiple, publication unifiée ; ouverture en **side peek**.
### 15.3 My Tasks (v1.9 → refonte v7.47)
Dashboard transverse **multi-workspaces** des tâches assignées (`app/routers/my_tasks.py`, `static/js/my_tasks.js`) : **3 vues façon Notion** — tableau dense (statut/échéance éditables au clic), kanban (colonnes = options de statut de la base d'origine, drag = écriture du statut), calendrier (mois courant sur les échéances). Sources opt-in, mapping explicite via les collections `is_task` (§11.6) : le front envoie un champ logique, jamais un id de colonne. Conversion d'une base en base de tâches par la modale `task_db_link.js` (`POST /db/{id}/task-db/api`, max 10 bases).
### 15.4 Trash
Corbeille unifiée (`trash.html`, `app/services/trash.py`) : soft-delete `deleted_at`, restauration, **purge** (immédiate ou planifiée par `trash_purge_scheduler`), actions groupées, tri/filtres réels, fenêtre de confirmation thématisée (v7.45.5).
### 15.5 Sidebar & navigation (v7.46)
Sidebar façon Notion dans `base.html` : en-tête workspace + badge d'auth, **4 onglets compressibles Home / Chat / Meeting / Inbox** (badges non-lus ; le panneau actif garde son label, les autres se replient sur l'icône), sections du panneau Home : Workspace (arbre), Repository, Meetings, Recents, Favorites, Agents, Teamspaces, Shared, Published, Private — chacune avec menu contextuel ; panneau « Customize sidebar » (`/api/sidebar`, `users.sidebar_config` JSON) ; **zone peek de sidebar** (survol quand repliée, épinglable, redimensionnable) ; **side peek** global (Alt+Click / menu contextuel) : panneau latéral ouvrant le document **nu, sans sidebar ni barre** (`page_editor_embed.html` + `?embed=1`, redimensionnable, `postMessage fd-page-renamed` pour synchroniser les titres).
---
## 16. IA : agent, compétences, connecteurs & écriture
### 16.1 Fournisseurs LLM & configuration
**Client unifié** `app/services/llm_client.py` : abstraction `LLMClient` sur **23 fournisseurs + un mode `offline`**, tous en protocole OpenAI-compatible chat-completions (Anthropic, Google `/v1beta/openai`, Cohere `/compatibility/v1` exposent une surface compatible). `PROVIDERS` porte (base_url, modèle par défaut) ; `PROVIDER_MODELS` les presets curatés exposés par `GET /api/agent/providers`. Ollama local (`http://localhost:11434/v1`, sans clé). Le mode **`offline`** est un *mock planner* déterministe — l'agent et les tests restent fonctionnels sans appel externe.
**Précédence de configuration en 4 niveaux** (`app/services/llm_config.py`) :
1. `provider`/`model` passés explicitement à l'endpoint de run ;
2. clé de l'utilisateur pour ce provider (`user_llm_keys` : api_key, api_base, default_model, `models_json`, flag `verified`) ;
3. ligne globale `llm_config` (id=1) gérée par l'UI admin ;
4. `settings.llm_*` (`.env`).
Les clés sont **stockées en base serveur**, jamais exposées (masquées) ; le changement de clé réinitialise `verified` ; les providers à catalogue sur-vendu (NVIDIA, Mistral) sont **validés par sonde chat** avec filtrage des modèles non-chat (embeddings, TTS, génération d'images…). Routes : `/api/agent/keys[/{provider}]`, `/test`, `/models`, `/api/agent/providers`.
### 16.2 Agent conversationnel — moteur ReAct (`app/services/agent_engine.py`)
Boucle `objectif → compréhension → contexte → raisonnement ↔ action → résultat` ; `MAX_ITERATIONS = 12` (plafonnée par la politique workspace et `AGENT_MAX_ITERATIONS`), budget tokens, timeout par passe. Le LLM n'émet que des **intentions d'outils** (function calls) ; chaque appel passe par `AgentPolicies.check_tool()` puis `PermissionManager.assert_can()` avant exécution par le `ToolRegistry`. Chaque action est **journalisée dans `agent_actions` avec snapshot d'annulation** (`undo_snapshot_json`) → rollback unitaire (`POST /api/agent/actions/{id}/undo`). Le run émet un **flux SSE** (`reasoning`, `action`, `notice`, `final`, `error`) et déclenche les webhooks `agent.run.started|finished|failed`.
**Panneau & conversations** (`app/routers/agent.py`, `/api/agent/*` ; UI `agent_panel.html` + `static/js/agent_panel_*.js`) : CRUD agents personnalisés (`agents` : instructions système, `scope_json` d'outils autorisés, `approval_mode`, modèle), conversations (provider/model persistés, toggle `memory_enabled`), feedback 👍/👎 (`agent_feedback`), mentions `@`/`+`, run SSE, **trigger externe** (`POST /api/agent/{id}/trigger` manuel SSE ; pendant synchrone JSON `POST /api/v2/agents/{id}/trigger` Bearer+scope write), **agents planifiés** (`agent_triggers`, scheduler 60 s).
**API publique agent** (`app/routers/api_v2_agent.py`, v6.6) : mince wrapper Bearer sur le même moteur — `POST /api/v2/agents/conversations/{id}/run` **tamponne le SSE et renvoie un JSON unique** (status, final, reasoning, actions, events, durée), rate limit 300/min, idempotency-key, audit. Une seule implémentation, jamais re-développée.
### 16.3 Contexte, outils, mémoire, skills
- **Context builder** (`context_builder.py`) : snapshot Markdown **filtré par permissions** — collections (+schéma, verrou), pages récentes, documents, espaces, mentions résolues (`@document:`, `@collection:`, `@page:`, `@folder:`), fichiers épinglés, plus un guide in-app `APP_GUIDE` pour les questions « comment faire ».
- **Outils** (`tool_registry.py`) : **26 outils statiques** — fins wrappers des opérations des routers humains, chaque mutateur renvoyant un snapshot d'undo. Lecture : `search_workspace`, `read_collection/page/workspaces/document` ; écriture : `create_collection/view/page/document`, `add_property/relation/sub_item/dependency`, `update_page`, `write_blocks`, `apply_template`, `delete_*` ; Gitea : `read_gitea_issues`, `sync_gitea`, `create_gitea_issue` ; **web** (v7.46) : `web_search` (provider Exa, repli DuckDuckGo sans clé), `fetch_url` ; **GitHub** : `search_code` ; **connecteurs** : `connector_fetch`. S'y ajoutent les **outils MCP dynamiques** : fusion à chaque run du cache `agent_connectors.tools_json` (noms `mcp_<serveur>_<outil>`) ; retrait des outils des plugins éteints.
- **Mémoire** (`agent_memory.py`, v7.54) : par conversation, une ligne résumé upsert dans `agent_memory` (20 échanges max, injection plafonnée 4 000 car.), ré-injectée au run suivant ; toggle `memory_enabled` = aucune lecture/écriture ; ne fait jamais échouer un run.
- **Skills** (`skill_gallery.py`, v6.6) : prompts paramétrés + liste d'outils autorisés (`agent_skills`) ; CRUD/apply/édition (`/api/agent/skills`, `PATCH` v7.52) ; **galerie de 17 presets** installables (rapport-hebdo, compte-rendu-reunion, base-crm, okr, sprint-review, veille-techno, triage-incident, briefing-quotidien…) ; **format portable `flowdeck-skill` v1** (export/import JSON rejouable) ; doubles routes cookie et `/api/v2/skills/*`.
### 16.4 Hub « Menu + » de l'assistant (v7.51→v7.58, 8 phases livrées)
Design `docs/V74_Agent_Plus_Menu.md`. Le bouton + du panneau devient un menu à sections à deux niveaux :
| Phase | Version | Section | Réalisation |
|---|---|---|---|
| 1 | v7.51 | Fichiers/répertoires (hub de contexte) | parcours d'arborescence `GET /api/nav/menu?parent_id=`, jeton `folder:<id>` |
| 2 | v7.52 | Compétences | « Gérer » (CRUD) + « Parcourir » (galerie) |
| 3 | v7.53 | Design System – Canevas | canevas `design_system` (`block_templates.py`), insertion de blocs |
| 4 | v7.54 | Mémoire | `agent_memory` + toggle persisté |
| 5 | v7.55 | Connecteurs (socle) | `connectors.py` : catalogue natifs (gitea, github, web, google, ms365) + connecteurs personnels `agent_connectors` (kind custom|discord|telegram|mcp), fetch gardé SSRF, secrets Fernet |
| 6 | v7.56 | Google + Microsoft 365 | `oauth_connectors.py` : OAuth PKCE S256, scopes **lecture seule** (Drive/Gmail/Calendar ; Graph Files/Mail/Calendars), tokens chiffrés, refresh auto |
| 7 | v7.57 | Discord, Telegram, Teams, MCP | presets d'auth ; client MCP complet `mcp_client.py` (JSON-RPC 2.0 `initialize → tools/list → tools/call`, réponse JSON ou SSE, cache en base) |
| 8 | v7.58 | Add plugins | `plugins.py` : catalogue on/off **à effet réel** (§4.2) |
> ⚠️ `docs/FLOWDECK_MCP_SERVER_GUIDE.md` est une conception pour exposer FlowDeck **comme serveur MCP** externe (parité `mcp.notion.com`) ; le code livré côté FlowDeck est le **client** MCP. Un serveur externe s'appuierait sur l'API v2 + les tokens existants.
### 16.5 AI Writing (`app/services/ai_writing.py`, v5.9)
Service **headless sans outils**, 6 actions : `write`, `summarize`, `translate`, `continue`, `autocomplete`, `properties`. Consommé par l'éditeur (slash commands, autocomplétion inline) et par les bases (remplissage de propriétés par suggestions structurées, défauts offline déterministes). Entrées HTTP : `POST /api/agent/writing`, `/writing/properties`, `/agent/generate` (génération sur document).
---
## 17. Recherche : FTS5, sémantique hybride & Ask AI
- **Palette Ctrl+K / Ctrl+P** (`#fd-command-palette` dans `base.html`, v5.0, doc `docs/V6…`) : recherche **FTS5** (`pages_fts`, table virtuelle synchronisée par triggers `AFTER INSERT/UPDATE/DELETE` sur `pages`, backfill au boot, repli `LIKE` si FTS5 absent) portée aux espaces accessibles (`app/services/search.py`, `/api/search`) ; **onglet « ✨ Réponses IA »** depuis v7.44.
- **Recherche sémantique + Ask AI** (v6.9, `app/services/semantic_search.py`, doc `docs/V69_Search_Ask_AI.md`) : retrieval **hybride** = lexical + similarité cosinus vectorielle, fusion **RRF (k=60)**, filtrage ACL `PermissionManager` (un chunk non autorisé n'entre jamais dans un prompt). Vecteurs : encodeur **TF hashé maison** (`hash-256`, 256 dims, zéro dépendance pip) ; chunking 1 200 car. / overlap 150 / 50 chunks max par ressource ; tables `semantic_embeddings` + `semantic_index_state`, scheduler d'indexation incrémental 300 s.
- **Ask AI** : `POST /api/v2/search/ask` — RAG (top 8 chunks autorisés) avec **citations cliquables** `[[fdpage:ID]]`, cache 10 min, rate limit 30/min, réponse extractive offline sinon ; `GET /api/v2/search/hybrid`.
- `flag pages.search_excluded` pour sortir une page de l'index.
---
## 18. AI Meeting Notes & calendrier
### 18.1 AI Meeting Notes (v7.1 → v7.69, bloc `meeting` de l'éditeur)
`app/services/meetings.py` + `app/routers/meetings.py` (`/api/v2/meetings/*`) ; spec détaillée `docs/architecture-meeting-notion-flowdeck.md`, doc `docs/V71_Calendar_Meetings.md`.
- **Machine à états** du bloc : `idle / recording / paused / processing / done / failed` ; modes de capture `mic_only / tab_plus_mic / import` ; audio (mp3/wav/m4a/ogg/flac/aac, 100 Mo max) dans `data/uploads/meetings/`.
- **Transcription** : backend externe par variable `STT_COMMAND` (ex. Whisper local, timeout 10 min) ou transcript collé ; **segments groupés par locuteur**, ré-attribution des speakers.
- **Consentement bloquant journalisé** (`meeting_consent_log`, 6 méthodes : attestation de démarrage, verbal, chat, audio, add-on Meet, force workspace).
- **Résumé façon Notion** : pipeline `run_processing()` en 8 étapes *Thinking* persistées (lecture → analyse → références personnes → compréhension → **classification de complétude** → rédaction → **titre généré** avec diagnostic honnête si réunion brève → **validation des citations obligatoires**) ; presets d'instructions (auto, standup, team, sales, 1:1, interview) ; résumé structuré JSON via `AIWritingService` (overview / décisions / action items), repli offline déterministe ; rendu Markdown → blocs sur la page.
- **Partage tracé** (`meeting_distributions` : copy_link/email/slack) ; événement **`meeting.summarized`** → déclenche les automations ; **index unique partiel `event_occurrence_id`** : une occurrence calendrier = au plus une note.
- UI : onglet Library dédié (§15.2), section Meeting de la sidebar, setup visible avec test micro et retry sans perte (v7.69.5).
### 18.2 Sync calendrier (`app/services/calendar_sync.py`, v7.1)
Synchronisation **bidirectionnelle** collection ↔ **Google Calendar** (REST) ou **CalDAV** générique (REPORT/PUT bruts, zéro dépendance). Liens par utilisateur dans `calendar_links` (tokens chiffrés Fernet, `date_property` de la collection), matching des événements par `collection_pages.external_event_id`, conflit = last-write-wins + notification `calendar.conflict`, `GET /db/{id}/calendar/freebusy`, resync périodique par scheduler.
---
## 19. Automatisations, workers, rappels, notifications & webhooks
### 19.1 Moteur d'automatisations (`app/services/automations.py`, v5.1 → v2 multi-step v7.0)
*If-this-then-that* : triggers **`event` / `cron` / `button`** (tables `automations`, `automation_runs`), conditions AND sur propriétés (`eq, neq, contains, not_contains, is_empty, is_not_empty, changed`), actions séquentielles.
**v7.0 — steps chaînés** (`automation_steps`) : kinds `trigger|condition|delay|action` ; multi-trigger mode `any/all` (fenêtre 300 s) ; delay plafonné 300 s ; **8 types d'actions** : `webhook`, `set_property`, `create_page`, `notify`, `slack`, `email`, `forge_issue` (issue Gitea/GitHub) et **`agent_trigger`** (lance un agent FlowDeck dans sa propre conversation journalisée). Bouton natif de base (`press_button()` sur une propriété `button`, `button_automation_id`). `fire_event()` est le point d'entrée des événements `page.*`, `collection.*`, `form.submitted`, `meeting.summarized`… ; scheduler cron 60 s mutualisé avec les workers. **Éditeur visuel v7.45** : pipeline de cartes ordonnées dans Settings → Automations (`/workspace/automations/{id}/steps`), config typée par kind, réordonnancement, « ✨ Convertir le JSON en pipeline ».
### 19.2 Workers (`app/services/workers.py`, v7.0)
Extraits **Python custom exécutés sur l'infrastructure FlowDeck** (parité Notion Workers). Exécution manuelle, **cron**, ou **partagée en équipe avec fork**. Sandbox best-effort single-process : lint **AST blacklist** (`import os/sys/subprocess/socket…`, `open/exec/eval/compile/__import__`, attributs dunder), pas de réseau ni filesystem, builtins restreints, API limitée à `log()`, `ctx`, `result` ; **timeout 30 s** et **budget journalier secondes/workspace** (`daily_budget_s`). Tables `workers`/`worker_runs`, routes `/api/v2/workers*`.
### 19.3 Récurrence & rappels (`recurrence.py`, `reminders.py`, v5.8)
- **Récurrence** : sous-ensemble RRULE (`daily/weekly/monthly`, interval, COUNT, UNTIL, BYDAY, fuseau `zoneinfo`) stocké dans `property_values_json` sous la clé `__recurrence__` ; **occurrences virtuelles calculées à la volée**, jamais persistées.
- **Rappels** : règle `__reminder__` (lead minutes/heures/jours par propriété date) ; `scan_and_fire()` (60 s) calcule la prochaine occurrence (timezone-aware), envoie notification in-app + email si opt-in ; dédupliqué via `reminder_log` par (page, occurrence) — **il n'existe pas de table `reminders`**, les dates vivent dans les propriétés `due`.
### 19.4 Notifications (`app/services/notifications.py`, v4.9)
Table `notifications` (mentions `@`, commentaires, changements de page, assignations `notify_assignment()`, `page.updated`, `calendar.conflict`…) ; préférences utilisateur + fuseau (`/api/notifications/prefs`) ; livraison email via `app/services/mailer.py` (SMTP optionnel) ; UI cloche `_notification_bell.html` + onglet Inbox de la sidebar.
### 19.5 Webhooks sortants (`app/services/webhook_outbound.py`, v2.1 → prod v6.4)
**~50 événements** au catalogue (pages, commentaires, collections, sprints, partages, `automation.fired/failed/retrying`, `agent.run.*` y compris `approval_requested`, workspaces, fichiers, imports, `ping`). Signature **HMAC-SHA256** (`X-FlowDeck-Signature`), **retry 2 s/10 s/60 s** par ledger `webhook_deliveries`, abonnements avec wildcards (`page.*`, `*`) dans `webhook_subscriptions`, CRUD `/api/v2/webhooks` + endpoint entrant de test. Gestion admin `app/routers/webhooks.py` (webhooks **entrants** Gitea : `POST /api/webhook/gitea` avec secret partagé).
---
## 20. Intégration Gitea / GitHub
### 20.1 Flux de données Gitea
```
Création Collection Sync bidirectionnelle
────────────────── ──────────────────────
1. Collection vide 5. POST /board/api/sync/{o}/{r}
(pas de lien Gitea) ├─ GET issues Gitea
├─ Map issues → collection_pages
2. Collection liée │ (via _issue_column)
(gitea_owner + gitea_repo) ├─ Extract AI keywords
└─ Upsert cards en DB
3. Pull initial
GET /api/v1/repos/{o}/{r}/issues
6. Webhook Gitea entrant
4. Mapping colonnes POST /api/webhook (secret partagé)
col_mapping: ├─ issue.opened → create page
column_name → gitea_label ├─ issue.closed → archive page
└─ issue.labeled → move column
```
- `app/services/gitea_client.py` : wrapper API REST (token admin `GITEA_TOKEN` ou token OAuth de l'utilisateur) ;
- Le dépôt est un **pseudo-workspace** (`gitea:<owner>/<repo>`) avec son navigateur `gitea_workspace.html`, ses pages privées miroir (`gitea_private_pages`) et son kanban legacy (`boards`/`cards`/`col_mapping`) ;
- L'agent peut lire/créer des issues (`read_gitea_issues`, `create_gitea_issue`, `sync_gitea`).
### 20.2 Adaptateur de compatibilité
```python
class GiteaBoardCompat:
"""Adaptateur : board Gitea legacy → Collection."""
@staticmethod
def from_board(board_row) -> dict:
return {
"collection_id": f"gitea:{board_row.id}",
"name": f"{board_row.project_owner}/{board_row.project_name}",
"gitea_owner": board_row.project_owner,
"gitea_repo": board_row.project_name,
"schema": [
{"name": "Title", "type": "title"},
{"name": "Status", "type": "select",
"options": json.loads(board_row.columns_json)},
{"name": "Priority", "type": "select",
"options": ["P1", "P2", "P3", "P4"]},
],
"is_gitea_linked": True,
}
@staticmethod
def from_collection_page(page_row) -> dict:
"""Convertit collection_page → card (pour templates legacy)."""
return {
"id": str(page_row.get("gitea_issue_number", page_row["id"])),
"title": page_row["title"],
"status": _derive_status(page_row),
...
}
```
(`app/services/collection_adapter.py`, `app/routers/board/import_.py` pour l'import de boards legacy.)
### 20.3 GitHub
Support forge secondaire : `app/services/github_adapter.py`, `app/routers/github_routes.py` (`/api/github`), OAuth de connexion (`GITHUB_OAUTH_CLIENT_ID/SECRET`), registre unifié `projects` (`proj_type` gitea|github|builtin, `app/services/projects.py` + cron de sync), outil agent `search_code` (API REST), `forge_issue` en action d'automatisation, import de dépôts (`importers/forge_repo.py`).
---
## 21. Import & Export
### 21.1 Import (`app/services/importers/`, `app/routers/imports.py`, `/api/import` + page `import.html`)
Pipeline unifié en 6 phases (v5.6) : socle (`base.py`, `pipeline.py`, `jobs.py` avec `import_jobs`/`import_items` pour statut, rapport et **dédup idempotent** par `(workspace, source, external_id)` → re-import incrémental), puis formats :
| Importer | Source |
|---|---|
| `notion.py` | export ZIP Notion (pages + databases) |
| `markdown.py`, `html_notes.py`, `standard_notes.py`, `apple_notes? (via html)` | notes et dossiers de fichiers |
| `obsidian.py`, `outline.py` | vaults Obsidian, espaces Outline |
| `docx.py`, `pdf.py` | documents bureautiques (python-docx, pypdf) |
| `tabular.py` | CSV/TSV/XLSX (openpyxl) → collections |
| `bookmarks.py`, `opml.py` | signets |
| `calendar.py` | ICS |
| `url_fetch.py` | import par URL (gardienné SSRF) |
| `forge.py`, `forge_repo.py` | données de forge (issues, dépôts) |
### 21.2 Export (`app/services/export.py`, `/api/export`, v4.7)
Export **Markdown** (avec images), **PDF** (WeasyPrint + xhtml2pdf), **HTML** standalone, **site statique `.zip`** ; CSV des bases (sous-ensemble `SIMPLE_TYPES` des propriétés) ; export/import **skill portable** `flowdeck-skill` v1 (§16.3).
---
## 22. Frontend : rendu, CSP, PWA & Web Clipper
### 22.1 Gabarits (`app/templates/`)
Répertoire **plat** — la distinction page/fragment est portée par la convention du préfixe `_` :
- **`base.html`** (~3 000 l.) = shell de toutes les pages authentifiées : `<head>` PWA/OG/CSS/JS vendors, `<body hx-headers>` (CSRF porté par htmx), **sidebar Notion à 4 onglets** (§15.5), topbar `{% block topbar %}{% include '_header.html' %}`, `#main-content` swappé par la navigation partielle, menu contextuel global, **palette Ctrl+K**, enregistrements `Alpine.data` obligatoires (build CSP), inclusion de `agent_panel.html` + service worker + `offline.js`.
- **`_header.html`** : topbar unifiée avec fil d'Ariane piloté par JSON embarqué (`#fd-breadcrumb-data`) + composant Alpine `fdBreadcrumb()`.
- Fragments : `_page_editor_content/scripts/realtime.html`, `_database_table(+_scripts).html`, `_ctx_menu.html`, `_icons.html`, `_icon_picker.html`, `_notification_bell.html`, `_workspace_tree_macro.html`.
- Pages : `dashboard`, `library`, `local_workspace`, `gitea_workspace`, `page_editor(_collection|_embed)`, `workspace(s)`, `notes`, `board`, `settings`, `help` (géométrie `settings-overlay` partagée avec Settings, v7.69.3), `trash`, `accounts`, `import`, `agent_message`.
- Autonomes (hors `base.html`) : `landing` (page de présentation pré-login), `welcome` (onboarding), `public_page` (rendu public thème sombre), `card(_detail)`, fragments SSR échangés par htmx (`board_fragment`, `detailed_board`, `table_view`, `status_overview`, `team_load`).
### 22.2 Modules JS (`static/js/`)
| Module | Rôle |
|---|---|
| `page_editor_scripts.js` (~3 500 l.) | cœur de l'éditeur : blocs, sérialisation `gtMd`, menu slash, toolbars, tableaux, colonnes, synced blocks, undo, autoSave, uploads, collage intelligent, presse-papiers multi-blocs, mentions `WM`, complétion IA `AIAC` |
| `page_editor_realtime.js` | client WebSocket présence/curseurs/ops (§13.4) |
| `database_table.js` | composant `DBInstance` : vues, cellules, filtres/tris, config persistée |
| `library.js`, `local_workspace.js`, `my_tasks.js` | pages §15 |
| `app.js` | **navigation partielle maison** : interception des liens « clic gauche simple », fetch + swap de `.main-wrapper` seul + `history.pushState` + remontée explicite d'Alpine (`Alpine.initTree`) ; `window.fdNavigate(url)` point d'entrée programmatif ; tooltips, helpers peek |
| `agent_panel_1/2.js` | panneau agent, Menu + hub (§16.4) |
| `meeting_block.js` | UI du bloc AI Meeting Notes |
| `settings.js`, `workspaces.js`, `board.js`, `gitea_workspace.js`, `import.js`, `help.js`, `task_db_link.js`, `offline.js`, `code-highlight.js`, `_ctx_menu.js`, `_icon_picker_*.js` | modules de page |
| vendors | `htmx.min.js` (2.0.4), **`alpine.csp.min.js`** (build officiel CSP 3.17.4), `sortable.min.js`, `katex`, `highlight(.extra)/prism`, `vendor/chart.umd.js`, `vendor/leaflet/` |
Gardes d'idempotence (`window.__fdEditorScriptsLoaded`, v7.45.2) pour survivre aux re-jeux de scripts lors des swaps ; anti-FOUC au chargement (v7.45.3-4). **Aucun bundler** : pas de `package.json` racine ni d'esbuild — les libs sont vendorisées telles quelles (`scripts/vendor_hljs.py` pour highlight.js) ; cache-busting par `?v=VERSION`.
### 22.3 PWA offline
- `static/manifest.json` : `standalone`, icônes 72→512 régénérées depuis le logo (`scripts/generate_pwa_icons.py`), `lang: fr` ;
- `static/sw.js` : deux caches — statique `flowdeck-v*` (pré-cache CSS/JS/vendors/icônes, bump à chaque release) et données `flowdeck-data-v*` ; stratégies : **mutations → always network** (la file est côté client), HTML → network-first (timeout 4 s, repli cache puis **page `/offline.html` fabriquée**), GET API → network-first cache données, statique → cache-first ; Background Sync si disponible ;
- `static/js/offline.js` (`window.FlowOffline`) : IndexedDB `flowdeck-offline` (`pages_offline`, `collections_offline`, `sync_queue`, `sync_meta`) ; écritures locales optimistes hors ligne, **file de mutations rejouée sur reconnexion via `POST /api/v2/sync/batch`** (lots ≤ 100, rétention 30 j), pull via `GET /api/v2/sync/delta`, id d'appareil `fd_device_id`.
### 22.4 Web Clipper (`extension/`, v6.2)
Extension navigateur **Manifest V3** (« FlowDeck Web Clipper », Chrome/Edge/Firefox) : menus contextuels + **bouton flottant draggable** (sélection/capture), sélecteur Readability ; captures **article / sélection / bookmark / screenshot** envoyées à `POST /api/v2/web-clipper/clip` (Bearer token d'appareil `extension_devices`, device id `dev_…` persisté) ; popup de configuration serveur/token + vérification de connexion ; notification des onglets FlowDeck ouverts après un clip. Coupée par le plugin `web-clipper` (§4.2).
---
## 23. Déploiement & exploitation
### 23.1 Docker
```yaml
# docker-compose.yml — un seul service ; Gitea reste externe
services:
flowdeck:
build: .
container_name: flowdeck
ports:
- "${APP_PORT:-8080}:8080"
volumes:
- flowdeck_data:/data
env_file:
- .env
restart: unless-stopped
volumes:
flowdeck_data:
```
- `Dockerfile` : **2 stages Python** (`builder` : `pip wheel` → `/wheels` ; `runtime` : `python:3.13-slim` + libs système WeasyPrint pango/harfbuzz/gdk-pixbuf, fonts dejavu + noto-color-emoji, curl) ; **aucun stage node** (front non compilé) ; `EXPOSE 8080` ; `HEALTHCHECK` sur `/api/health` (30 s) ; CMD `uvicorn app.main:app --host 0.0.0.0 --port 8080 --proxy-headers --forwarded-allow-ips '*'`.
- Démarrage local : `cp .env.example .env` (token Gitea) puis `docker compose up -d` → http://localhost:8080 ; ou venv + `uvicorn app.main:app --reload --port 8082` (CONTRIBUTING).
### 23.2 Variables d'environnement (`app/config.py`, pydantic-settings, `extra="ignore"`)
| Groupe | Variables |
|---|---|
| App | `APP_SECRET_KEY`, `APP_HOST`, `APP_PORT`, `APP_BASE_URL`, `LOG_LEVEL`, `DEFAULT_LANG`, `FLOWDECK_DATA_DIR` |
| Forge/OAuth | `GITEA_URL`, `GITEA_TOKEN`, `GITEA_OAUTH_CLIENT_ID/SECRET`, `GITEA_WEBHOOK_SECRET`, `GITHUB_OAUTH_CLIENT_ID/SECRET`, `OAUTH_REDIRECT_URI`, `WEBHOOK_BASE_URL` |
| Base / sync | `DATABASE_URL` (sqlite uniquement), `SYNC_INTERVAL`, `GITEA_CACHE_TTL` |
| Rate/API | `RATE_LIMIT_ENABLED`, `RATE_LIMIT_REQUESTS`, `PUBLIC_API_INSECURE_OK`, `API_V2_RATE_LIMIT_PER_TOKEN` |
| Sauvegardes | `BACKUP_ENABLED`, `BACKUP_DIR`, `BACKUP_INTERVAL_HOURS`, `BACKUP_KEEP` |
| Fonds | `PROJECT_SYNC_ENABLED`, `PROJECT_SYNC_INTERVAL_HOURS`, `REMINDERS_ENABLED`, `REMINDER_SCAN_INTERVAL_SECONDS`, `WEBHOOK_RETRY_ENABLED`, `WEBHOOK_RETRY_INTERVAL_SECONDS` |
| SMTP | `SMTP_HOST/PORT/USER/PASSWORD/FROM/USE_TLS` |
| SSO | `SSO_PROVIDER`, `SSO_NAME`, `SSO_ENTITY_ID`, `SSO_SSO_URL`, `SSO_SLO_URL`, `SSO_X509_CERTIFICATE`, `SSO_ISSUER_URL`, `SSO_CLIENT_ID/SECRET`, `SSO_SCOPE`, `SSO_ATTRIBUTE_MAPPING`, `SSO_GROUPS_MAPPING`, `SSO_AUTO_PROVISION`, `SSO_ONLY`, `SSO_SIGN_REQUESTS`, `SSO_DEFAULT_WORKSPACE_ID` |
| Agent/IA | `AGENT_ENABLED`, `LLM_PROVIDER/MODEL/API_KEY/API_BASE`, `AGENT_MAX_ITERATIONS`, `AGENT_MAX_TOKENS_BUDGET`, `AGENT_RUN_TIMEOUT_SECONDS`, `AGENT_MEMORY_DEFAULT`, `GOOGLE_CLIENT_ID/SECRET`, `MS_CLIENT_ID/SECRET`, `WEB_SEARCH_PROVIDER`, `EXA_API_KEY`, `GITHUB_TOKEN`, `WEB_FETCH_MAX_CHARS`, `STT_COMMAND` (transcription réunions) |
### 23.3 Données & sauvegardes
Volume nommé `flowdeck_data` monté sur `/data` : base SQLite, uploads (dont `data/uploads/meetings/`), avatars, backups. **Service de backup interne** (`app/services/backup.py`, v5.2) : snapshot quotidien du fichier (checkpoint WAL avant copie), nommage `flowdeck-<horodatage>.db`, rétention `BACKUP_KEEP`. `docker-backups/` à la racine = copies hôtes manuelles de checkpoints avant opérations risquées (convention d'équipe, pas automatisée).
---
## 24. Outils de développement, tests & CI
### 24.1 Environnement
- **Python 3.13** aligné partout (Docker, `pyproject.toml` target, CI — audit A35, v7.35) ; dépendances verrouillées avec **uv** (`uv.lock`, `requirements*.txt`) ;
- Lint : **Ruff** (`pyproject.toml` : line-length 110, règles E/F/I/UP/B/W, isort first-party `app`/`tests`) + **black** ; **ESLint v9** flat config (`eslint.config.mjs`, 4 règles souples en warn sur `static/js`, ignores `*.min.js`/`vendor/`, via `npx --yes eslint static/js`) ;
- Conventions (`CONTRIBUTING.md`) : Conventional Commits ; **code et commentaires en anglais, docs en français**.
### 24.2 Modèle de branches (`BRANCHING.md`)
Git-flow simplifié : `main` (prod, tags `vX.Y.Z`, Docker auto sur :8080) ← `develop` (intégration) ← `feat/<nom>` / `fix/<nom>` ; **jamais de push direct** sur main/develop (PR only), CI verte avant merge, kebab-case, branche supprimée après merge ; hotfix depuis main puis resync `main → develop`. (Note : `CONTRIBUTING.md` dit encore `feature/xxx → main` — divergence à harmoniser.)
### 24.3 Pyramide de tests
| Niveau | Outil | Volume |
|---|---|---|
| Unitaires / intégration | **pytest** (+ pytest-xdist `-n auto`, pytest-asyncio) dans `tests/` — TestClient FastAPI sur SQLite temporaire, isolation verrouillée par `conftest.py` (audit A1) | **~1 394 tests verts** (v7.68) ; couverture des routers portée de 0 à l'identifiable par l'audit **A32** |
| E2E navigateur | **Playwright** (`e2e/`, `@playwright/test ^1.63`) ; specs nommées par version (`v768_columns_clipboard.spec.js`, `pwa_offline.spec.js`, `mobile_regression.spec.js`…) ; exigence de **vrais gestes** (Ctrl+C, drag) et **contrôles négatifs** (même spec contre ancien build) | fondations posées v7.36 |
| Audit fonctionnel | `scripts/audit_functional.py` — bout-en-bout sur base isolée ; `scripts/_routescan.py` — inventaire des routes (677 routes auditées en v7.46) | récurrent |
| Grille visuelle manuelle | `TESTING.md` (61 cas ; document daté v2.1, à rafraîchir) | — |
### 24.4 CI (`/.gitea/workflows/ci.yml`)
Gitea Actions, 3 jobs sur push + PR → main/develop : `lint` (ruff + eslint), `test` (pytest `-n auto` + coverage, deps système WeasyPrint), `docker` (build + smoke `from app.main import app`).
### 24.5 Versionnage
`scripts/bump_version.py` réécrit `version=` dans `app/main.py`, bump `VERSION` et crée le tag git. Le `CHANGELOG.md` est la source d'évolution de référence (les bannières README/WORKLOAD sont figées plus tôt — voir §26).
---
## 25. Historique des versions v1.0 → v7.70.0
Version courante : **7.70.0** (`VERSION`). Bandes thématiques vérifiées contre les titres réels du `CHANGELOG.md` (⚠️ le CHANGELOG saute v2.8→v4.5, documentées uniquement dans `ROADMAP.md` ; plusieurs versions portent un titre « bande roadmap » décalé — ex. `## v5.11.7 — v5.8.0 Calendrier & Rappels` — et trois 7.46.0 distinctes coexistent).
### Socle v1.0 – v2.x (résumé, inchangé du doc v4.0)
- **v1.0–v1.2** — clone de board Kanban Gitea (boards, cartes, colonnes, notes) ; UI Notion-style.
- **v1.3–v1.5** — Collections indépendantes, 21 types de propriétés, relations, rollups (12 agrégations), formules (19 fonctions), GiteaBoardCompat.
- **v1.6–v1.7** — vues multiples (Table/Board/Calendar/Gallery/List/Timeline/Status Overview/Team Load).
- **v1.8** — sub-items + dépendances Blocking/Blocked by. **v1.9** — My Tasks.
- **v2.0** — workspaces + rôles, commentaires, historique de page, favoris, templates, import/export CSV, partage public. **v2.1–v2.7** — polish, pages privées Gitea, workspaces multiples.
### v4.x — Plateforme comptes & partage
- **v4.0.0** — Accounts, Integrations & Sharing (MVP) : types de comptes, matrice d'intégrations, Library, share/publish, trash (§6, §15).
- **v4.0.1–v4.0.2** — Onboarding & polish ; qualité/robustesse. **v4.1–v4.2** — data sources & linked databases ; templates & dashboards. **v4.3** — Database Views complètes (10 types). **v4.4–v4.5** — tasks/sub-items/dependencies ; sprints & My Tasks.
- **v4.6–v4.9** — callouts, TOC, KaTeX self-hosté, multi-colonnes, toggles ; **export MD/PDF/HTML/site-zip** ; bloc tableau ; commentaires inline + @mentions + notifications (+ SMTP).
- **v4.10–v4.15** — **naissance de l'agent** : moteur ReAct natif (§16.2), config LLM dans l'UI, clés par utilisateur + multi-fournisseurs dynamiques, panneau « Notion AI » Ctrl+J, contexte universel, AI Meeting Note (ébauche), mentions @ et skills `/` dans le panneau.
### v5.x — Socle moderne
- **v5.0–v5.2** — palette Ctrl+K + **FTS5** ; **migrations versionnées** `app/migrations.py` ; collage intelligent, inline databases, templates de bases, validation de propriétés ; moteur **d'automatisations if-this-then-that** ; sessions révocables + tokens API ; backup interne.
- **v5.3–v5.9** — **temps réel WebSocket** (v5.3, ébauche LWW + polling) ; interactions de bloc (undo/redo, menu ⋮, drag multi-sélection) ; partage réorganisé ; **database avancée** (personnes, vues sauvegardées, swimlanes + WIP limits, calendar drag & drop, gallery) ; **calendrier & rappels** (récurrences RRULE, fuseaux) ; **import 6 phases** ; **AI Writing** dans l'éditeur.
- **v5.11–v5.15** — wiki-links `[[` + chips, mentions page/date ; templates et verrouillage de page, pleine largeur ; **synced blocks** ; webhooks v2.
### v6.x — Plateforme ouverte & entreprise
- **v6.0–v6.2** — **PWA offline** (SW + IndexedDB + sync) ; **permissions granulaires** (ACL page/collection/propriété + groupes + audit) ; **Web Clipper MV3**.
- **v6.3–v6.6** — **API publique REST `/api/v2`** complète (Bearer + scopes, RFC 7807, idempotence, OpenAPI) ; **temps réel en production** (merge 3-voies) ; synced blocks en prod (page-ombre de ligne) ; **API agent publique** (run synchrone JSON, rollback, trigger externe) + **marketplace de skills**.
- **v6.7–v6.9** — **SSO entreprise SAML + OIDC** (provisioning, mapping groupes, SSO only) ; **Sites & Forms** publics ; **recherche hybride + Ask AI** (embeddings maison, RAG citations).
### v7.x — Automatiser, gouverner, polir
- **v7.0–v7.3** — **Automations v2** (steps chaînés) + **Workers** sandboxés ; **Calendar sync** Google/CalDAV + **AI Meeting Notes** complètes ; **enterprise admin** (SCIM 2.0, 2FA TOTP, passkeys WebAuthn, domain claims, audit unifié, **gouvernance agent**) ; **Wiki/Teamspaces** (badges vérifiés, follows, guests, réactions, analytics).
- **v7.3.1–v7.35** — **cycle d'audit** (43 items A1–A43, `ROADMAP.md`) : sécurité P0→P2 (401 partout, CSRF A19/A38, SSRF A12, autoescape A10, logs A36, CSP A20/A43), A21 SQLite hors event loop, A32 tests routers, A27 extraction JS (−85 %), A28 routers → packages, A35 Python 3.13, A42 httpx partagé.
- **v7.36–v7.50** — fondations **E2E Playwright** ; **A20 terminé : Alpine build CSP, `unsafe-eval` retiré** (v7.43) ; éditeur visuel d'automations (v7.45) ; palette avec onglet Réponses IA ; navigation partielle anti-FOUC ; **web tools de l'agent** + 11 skills (v7.46) ; **sidebar à onglets** Home/Chat/Meeting/Inbox ; **My Tasks 3 vues transverse** ; **side peek** plein écran.
- **v7.51–v7.58** — **Menu + de l'assistant** en 8 phases : hub de contexte, skills gérer/parcourir, canevas Design System, mémoire d'agent, connecteurs (socle → Google/M365 OAuth lecture seule → Discord/Telegram/Teams/**MCP**), plugins catalogue on/off à effet réel.
- **v7.59–v7.70.0** — éditeur **façon Notion** : mobile (drawer réglable, menus au `/`), bouton + d'insertion, menus contextuels de bloc et de sélection refondus, **commentaires ancrés sur la sélection de texte** (surlignage jaune, tiroir refondu), **réactions sur texte + tiroir**, **copier/couper/coller multi-blocs**, colonnes réellement rendues, tables GFM, identité visuelle (logo/bannière/icônes PWA/`og:image`), **Library** (onglet AI Meeting Notes groupé par date, Recents réels, Publish unifié, colonnes visited/source) ; **projet Gitea** : miroir local ouvert en parallèle du dépôt, sidebar Repository distincte.
---
## 26. Chantiers ouverts & suites
Extraits de `ROADMAP.md` (audit du 2026-09-30) et `WORKLOAD.md` :
- **Cycle v6 et cycle v7 (v6.8→v7.3) livrés intégralement** ; `WORKLOAD.md` : 52/52 features, tableau de bord **figé à v7.3.0** (les ~36 versions suivantes ne sont suivies que par le CHANGELOG).
- **Audit A1–A43 quasi clos**. Restes ouverts / décisions assumées :
- **A38 phase 3** : fusion des `workspace-tree.js` Library / local_workspace **reportée** (pas de couverture E2E suffisante pour risquer la refactorisation) ;
- **A39** : htmx **conservé** (décision explicite — pas de remplacement) ;
- migration complète des call sites SQLite sync → async (A21) encore partielle hors chemins chauds ;
- mono-processus assumé : rooms temps réel, challenges WebAuthn, store SSO env → multi-worker non supporté sans refonte.
- **Faiblesses documentaires connues** : bannières `README.md` (cite 7.3.9 / 764 tests) et `TESTING.md` (v2.1) à rafraîchir ; divergence BRANCHING ↔ CONTRIBUTING sur la cible de merge des features.
- **Pistes naturelles de la suite du produit** (non planifiées formellement) : serveur MCP officiel (`docs/FLOWDECK_MCP_SERVER_GUIDE.md`), export de masse enrichi, collaboration avancée (suggestions d'édition étendues), PostgreSQL si un jour un dual-engine est décidé (aujourd'hui **non supporté**, §5.1).
---
## 27. Références
| Document | Contenu |
|---|---|
| `docs/DATA_MODEL.md` | **modèle de données exhaustif** (105 tables, colonnes, FK, JSON, relations) |
| `docs/API_GUIDE_V6.md` + `docs/openapi-v2.json` + `/docs` | API publique v2 (guide + spécification OpenAPI vivante) |
| `docs/Guide_Complet_Notion_database.md`, `NOTION_DATABASE_TASKS_GUIDE.md` | systèmes de bases et de tâches |
| `docs/Guide_Complet_Notion_AI.md`, `Guide_Complet_Notion_Agent_2026.md`, `Flowdeck_Agent_integration.md` | benchmark IA, design agent |
| `docs/Guide_Complet_Notion_sharing_collaborartion.md` | partage & collaboration |
| `docs/V6_SSO_SAML_Enterprise_Auth.md`, `V6_Granular_Permissions.md`, `V6_PWA_Progressive_Web_App.md`, `V6_Web_Clipper.md` | features v6.x |
| `docs/V68_Sites_Forms.md`, `V69_Search_Ask_AI.md`, `V70_Automations_Workers.md`, `V71_Calendar_Meetings.md`, `V72_Enterprise_SCIM_2FA.md`, `V73_Wiki_Teamspaces_Polish.md`, `V74_Agent_Plus_Menu.md` | features v6.8–v7.4 |
| `docs/architecture-meeting-notion-flowdeck.md`, `docs/FLOWDECK_MCP_SERVER_GUIDE.md` | spec Meeting Notes ; conception serveur MCP (non implémentée) |
| `CHANGELOG.md` | journal détaillé v7.36→v7.70.0 (et au-delà, désordonné — voir §25) |
| `ROADMAP.md` | plans v2.8–v4.5 + **audit A1–A43** + suivi des cycles v6/v7 |
| `WORKLOAD.md`, `TESTING.md`, `CONTRIBUTING.md`, `BRANCHING.md` | pilotage, grille visuelle, conventions, branches |
---
*Ce document décrit l'état du projet à la version 7.70.0 (2026-10-09). Les affirmations marquées « factuel » ou « ⚠️ » signalent des écarts connus entre le code et les documents de pilotage plus anciens.*