Corrige deux régressions de /local-workspace :
- le chemin du header restait bloqué sur « Home / <workspace> » quel que
soit le dossier affiché : la route rend désormais breadcrumb_items
(Home / <workspace> / <dossier> / <sous-dossier>, niveaux cliquables,
collapse « … » au-delà de 4) et la navigation sans rechargement recalcule
le chemin via l'event flowdeck:breadcrumb-changed ;
- le clic sur un dossier du sidebar affichait TOUS les composants à la
fois : Alpine.data('wsInitData') retournait le même objet singleton, le
2e montage (navigation partielle) levait « Cannot redefine property:
\ » et initTree abandonnait, laissant tout le contenu au state
brut. La factory retourne désormais une enveloppe fraîche par montage
qui délègue à l'état réactif partagé. #lw-config est aussi relu à chaque
exécution (le 2e montage gardait le folder_id du 1er chargement).
Inclus également le travail en cours de l'arbre : Library (colonnes Last
visited/Source, ordre d'en-tête, favoris à icônes Workspace), Meeting
Notes (bloc, CSS, routes, docs), coloration de code hljs, badges
favori/publié dans l'arbre local-workspace, docs (DATA_MODEL,
architectures) et tests associés.
1074 lines
96 KiB
Markdown
1074 lines
96 KiB
Markdown
# Architecture FlowDeck — Document Complet v7.69.8
|
||
|
||
> **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.69.8** (`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.69.8](#25-historique-des-versions-v10--v7698)
|
||
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.69.8
|
||
|
||
Version courante : **7.69.8** (`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.69.8** — é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).
|
||
|
||
---
|
||
|
||
## 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.69.8 (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.69.8 (2026-10-09). Les affirmations marquées « factuel » ou « ⚠️ » signalent des écarts connus entre le code et les documents de pilotage plus anciens.*
|