Templates (v7.71.x) : - registre unifié \ emplates\ (migrations 48-49) + TemplateService.instantiate unique (UI, API v2, agent, scheduler) - sélecteur (pilule page vide, menu •••, commande /template), gestionnaire /templates, menu New ▾, From template, base inline dans un document - 141 presets système (59 pages, 42 bases, 15 blocs, 25 lignes), titre auto depuis le template, variables title réservée - récurrences RRULE + scheduler dédupliqué, agent apply_template/list_templates, API /api/templates + /api/v2/fd-templates - correctifs : bouton Templates, centrage fenêtre, filtres CSP, flux de création, variable title - tests : tests/test_fd_templates.py (19) et e2e/templates_picker.spec.js (8) Inclut le travail déjà présent dans le working tree (vues Notion : view_query/view_aggregate/form_projection/geocoding, property_types, database_table, docs agents-skills) et ignore .playwright-mcp/.
96 KiB
Architecture FlowDeck — Document Complet v7.70.0
FlowDeck = clone complet de Notion intégré nativement à Gitea, servi par une application FastAPI monolithique (SSR Jinja2 + htmx + Alpine.js), avec API REST publique v2, agent IA conversationnel, automatisations, SSO entreprise et édition collaborative temps réel.
Document d'architecture à jour de la version 7.70.0 (
VERSION). Historique complet : voir §17 etCHANGELOG.md. Modèle de données détaillé table par table :docs/DATA_MODEL.md.
Table des matières
- Vue d'ensemble
- Architecture système
- Structure du backend
- Routage & API
- Modèle de données
- Authentification & comptes
- SSO entreprise : SAML, OIDC, SCIM, 2FA, WebAuthn
- Sécurité applicative
- Autorisation : rôles, permissions granulaires, gouvernance
- Workspaces & collaboration multi-utilisateurs
- Databases : propriétés, formules, rollups, dépendances
- Système de vues
- Pages, éditeur de blocs & temps réel
- Wiki, liens, blocs synchronisés & teamspaces
- Partage, publication, Library, My Tasks & Trash
- IA : agent, compétences, connecteurs & écriture
- Recherche : FTS5, sémantique hybride & Ask AI
- AI Meeting Notes & calendrier
- Automatisations, workers, rappels, notifications & webhooks
- Intégration Gitea / GitHub
- Import & Export
- Frontend : rendu, CSP, PWA & Web Clipper
- Déploiement & exploitation
- Outils de développement, tests & CI
- Historique des versions v1.0 → v7.70.0
- Chantiers ouverts & suites
- 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/csp3.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
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 :
init_db()(app/db.py) — baseline idempotent +apply_migrations()(app/migrations.py, version courante 38) ;- démarrage des schedulers asyncio lancés via
_spawn(name, coro)— un wrapperA34qui 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) |
- 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/v2Bearer). - Les erreurs API v2 suivent RFC 7807 ; réponses standardisées
SuccessResponse {status:"ok", data}/ErrorResponse(app/models/responses.py). app/templating.py: Jinja2 avecselect_autoescape(["html"])(audit A10), globalsfd_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 |
/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 |
/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érarchiquesread < 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(tableidempotency_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é dansdocs/openapi-v2.json; guide completdocs/API_GUIDE_V6.md. - Sync offline :
POST /api/v2/sync/batch(rejeu des mutations PWA) +GET /api/v2/sync/delta(pull parsync_version) — backendapp/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éfautsqlite:///…/flowdeck.dbsous/dataen Docker) ; PRAGMA journal_mode=WAL(un writer, multi-readers),foreign_keys=ON,busy_timeout=5000pour la concurrence (tests xdist, schedulers) ;get_conn()= context-manager avecrow_factory = sqlite3.Row; les conversions dict↔JSON des colonnes*_jsonsont faites à la main dans les services ;DATABASE_URLn'est interprété que pour le préfixesqlite:///(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
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 :
users= hub d'identité (30+ FK entrantes),workspaces= hub d'isolation multi-tenant.- Deux univers de « pages » :
pages(éditeur wiki, soft-deletedeleted_at, corbeille) etcollection_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. - Chaînes de cascades nettoyant l'éditorial ; les tables d'audit privilégient
SET NULLsur l'acteur pour survivre à la suppression d'un compte. - Sync Gitea par ID numérique (
gitea_issue_id,gitea_owner/repo) : jointures sans FK, système externe. - 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, registreget_provider()) + singleton legacyGiteaOAuth(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 modelink(rattacher OAuth à un compte local depuis Réglages → Intégrations) encodé dans l'état.redirect_uri: overrideOAUTH_REDIRECT_URIsinon 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 viaGET {forge}/api/v1/user, upsertusers, stockage du token, émission de la cookieflowdeck_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()strippepassword_hashavant toute sortie ; sid= une ligneuser_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_idprésent.
6.5 Jetons API Bearer
POST /api/settings/tokens(app/routers/security.py) créefd_<token_urlsafe(24)>, affiché une seule fois, stocké hashé SHA-256 (api_tokens.token_hash,token_prefixpour l'affichage) ;- scopes hiérarchiques
read < write < admin(SCOPE_RANK,app/services/api_v2_helpers.py) ; factoryrequire_scope()(audit A30) ; resolve_bearer_token()accepte 3 sources :api_tokens,extension_devices(Web Clipper),user_tokenslegacy (token Gitea en clair, portée implicite read+write) ;- fallback dev
fd-public-keyuniquement siPUBLIC_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'envSSO_*(fallback bootstrap) ; secrets chiffrés Fernet, jamais rendus parGET /api/v2/sso/config; handle_sso_login(): résolution par email (fusion) puis login ; création siauto_provision, sinon rejet ;sync_sso_groups(): mapping groupe IdP → rôleworkspace_members(owner/admin/editor/viewer), réappliqué à chaque login ;POST /api/v2/sso/syncforce 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ésenforce_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 adminGET /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_NONCEavantcall_next(A43), global Jinjacsp_nonce(); script-src-attr 'unsafe-inline'détaché pour couvrir les ~74 handlersonclick=Alpine ;- htmx neutralisé :
<meta name="htmx-config" content='{"allowEval": false, "inlineScriptNonce":…}'>(v7.37) ; - Alpine servi en build officiel
@alpinejs/csp(0eval/new Function) ; connect-src 'self' ws://… wss://…fermé (A20 phase 2 : CDN retiré, vendors localisés — testtest_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_REQUESTSlu à chaud, purge du store à 5000 clés,X-Forwarded-Forhonoré 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_TOKENreq/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ôtesprivate/loopback/link-local/reserved/multicast;app/services/og_fetcher.pyre-vérifie l'hôte à chaque redirection (A12) ;app/services/connectors.pyimpose 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 tableworkspace_membersdé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…deapp/routers/dashboard/local_workspace.py) et espaces Gitea (un dépôt = pseudo-workspacegitea:<owner>/<repo>, navigateurgitea_workspace.html, pages privées miroirgitea_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 viaeffective_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 :
- Temps réel éditeur : merge 3-voies versionné (§13.4) avec drapeau de conflit champ-par-champ ;
- Hors temps réel : colonne
sync_versionentretenue par triggers SQLite (*_sync_version_busurpages/collection_pages/collections) = verrou optimiste ; l'API offline s'appuie dessus (GET /api/v2/sync/delta) etPOST /api/v2/sync/batchrejoue les mutations en file avec résolution de conflits ; - 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 passeDonequand 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) etauto_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_taskavec mapping explicitetask_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 :
{
"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)
- Vues interactives client (
static/js/database_table.js, composantDBInstance— pages/db/{id},page_editor_collection.html, blocsdatabasedu plan d'un document) : Table (par défaut), Board/Kanban (group par select/status/person/multi_select/text, swimlanessub_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). - Vues rendues serveur (
app/routers/collections/_renderers.py, dispatch_render_viewappelé pardashboard_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éocodagelat,lngdepuis les propriétés), Feed (flux chronologique). - Board Gitea legacy (
app/routers/board/board_views.py,board.html, SSR/htmxGET /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
keydowncapture (le navigateur ne sérialise pas une sélection traversant deuxcontenteditable) ; - 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_sluggaranti, v7.69.6) → page publique rendue parpublic_page.html(thème sombre autonome, OG tags,og:imageen URL absolue via le globalapp_base_url()v7.69.1) ; - Sites publics (
app/routers/sites.py, v6.8, docdocs/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 brutesite_views, domaine personnalisécustom_domain) ; - Formulaires publics
/f/<token>: champs générés depuiscollection_properties(hors formules), validation, rate limiting, soumissions anonymes vers la collection (form_responses+collections.form_config_json), notification aux membres, événementform.submittedpour les automations ; - Partage public de base (
/workspacepublic_router,share_mode) et API de partageapp/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) :
provider/modelpassés explicitement à l'endpoint de run ;- clé de l'utilisateur pour ce provider (
user_llm_keys: api_key, api_base, default_model,models_json, flagverified) ; - ligne globale
llm_config(id=1) gérée par l'UI admin ; 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-appAPP_GUIDEpour 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 cacheagent_connectors.tools_json(nomsmcp_<serveur>_<outil>) ; retrait des outils des plugins éteints. - Mémoire (
agent_memory.py, v7.54) : par conversation, une ligne résumé upsert dansagent_memory(20 échanges max, injection plafonnée 4 000 car.), ré-injectée au run suivant ; togglememory_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,PATCHv7.52) ; galerie de 17 presets installables (rapport-hebdo, compte-rendu-reunion, base-crm, okr, sprint-review, veille-techno, triage-incident, briefing-quotidien…) ; format portableflowdeck-skillv1 (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 |
| 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.mdest 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-palettedansbase.html, v5.0, docdocs/V6…) : recherche FTS5 (pages_fts, table virtuelle synchronisée par triggersAFTER INSERT/UPDATE/DELETEsurpages, backfill au boot, repliLIKEsi 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, docdocs/V69_Search_Ask_AI.md) : retrieval hybride = lexical + similarité cosinus vectorielle, fusion RRF (k=60), filtrage ACLPermissionManager(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 ; tablessemantic_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_excludedpour 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 capturemic_only / tab_plus_mic / import; audio (mp3/wav/m4a/ogg/flac/aac, 100 Mo max) dansdata/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 viaAIWritingService(overview / décisions / action items), repli offline déterministe ; rendu Markdown → blocs sur la page. - Partage tracé (
meeting_distributions: copy_link/email/slack) ; événementmeeting.summarized→ déclenche les automations ; index unique partielevent_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, fuseauzoneinfo) stocké dansproperty_values_jsonsous 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é viareminder_logpar (page, occurrence) — il n'existe pas de tablereminders, les dates vivent dans les propriétésdue.
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 adminGITEA_TOKENou token OAuth de l'utilisateur) ;- Le dépôt est un pseudo-workspace (
gitea:<owner>/<repo>) avec son navigateurgitea_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é
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-contentswappé par la navigation partielle, menu contextuel global, palette Ctrl+K, enregistrementsAlpine.dataobligatoires (build CSP), inclusion deagent_panel.html+ service worker +offline.js._header.html: topbar unifiée avec fil d'Ariane piloté par JSON embarqué (#fd-breadcrumb-data) + composant AlpinefdBreadcrumb().- 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étriesettings-overlaypartagé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 — statiqueflowdeck-v*(pré-cache CSS/JS/vendors/icônes, bump à chaque release) et donnéesflowdeck-data-v*; stratégies : mutations → always network (la file est côté client), HTML → network-first (timeout 4 s, repli cache puis page/offline.htmlfabriquée), GET API → network-first cache données, statique → cache-first ; Background Sync si disponible ;static/js/offline.js(window.FlowOffline) : IndexedDBflowdeck-offline(pages_offline,collections_offline,sync_queue,sync_meta) ; écritures locales optimistes hors ligne, file de mutations rejouée sur reconnexion viaPOST /api/v2/sync/batch(lots ≤ 100, rétention 30 j), pull viaGET /api/v2/sync/delta, id d'appareilfd_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
# 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;HEALTHCHECKsur/api/health(30 s) ; CMDuvicorn app.main:app --host 0.0.0.0 --port 8080 --proxy-headers --forwarded-allow-ips '*'.- Démarrage local :
cp .env.example .env(token Gitea) puisdocker 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.tomltarget, 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-partyapp/tests) + black ; ESLint v9 flat config (eslint.config.mjs, 4 règles souples en warn surstatic/js, ignores*.min.js/vendor/, vianpx --yes eslint static/js) ; - Conventions (
CONTRIBUTING.md) : Conventional Commits ; code et commentaires en anglais, docs en français.
24.2 Modèle de branches (BRANCHING.md)
Git-flow simplifié : main (prod, tags vX.Y.Z, Docker auto sur :8080) ← develop (intégration) ← feat/<nom> / fix/<nom> ; jamais de push direct sur main/develop (PR only), CI verte avant merge, kebab-case, branche supprimée après merge ; hotfix depuis main puis resync main → develop. (Note : CONTRIBUTING.md dit encore feature/xxx → main — divergence à harmoniser.)
24.3 Pyramide de tests
| Niveau | Outil | Volume |
|---|---|---|
| Unitaires / intégration | pytest (+ pytest-xdist -n auto, pytest-asyncio) dans tests/ — TestClient FastAPI sur SQLite temporaire, isolation verrouillée par conftest.py (audit A1) |
~1 394 tests verts (v7.68) ; couverture des routers portée de 0 à l'identifiable par l'audit A32 |
| E2E navigateur | Playwright (e2e/, @playwright/test ^1.63) ; specs nommées par version (v768_columns_clipboard.spec.js, pwa_offline.spec.js, mobile_regression.spec.js…) ; exigence de vrais gestes (Ctrl+C, drag) et contrôles négatifs (même spec contre ancien build) |
fondations posées v7.36 |
| Audit fonctionnel | scripts/audit_functional.py — bout-en-bout sur base isolée ; scripts/_routescan.py — inventaire des routes (677 routes auditées en v7.46) |
récurrent |
| Grille visuelle manuelle | TESTING.md (61 cas ; document daté v2.1, à rafraîchir) |
— |
24.4 CI (/.gitea/workflows/ci.yml)
Gitea Actions, 3 jobs sur push + PR → main/develop : lint (ruff + eslint), test (pytest -n auto + coverage, deps système WeasyPrint), docker (build + smoke from app.main import app).
24.5 Versionnage
scripts/bump_version.py réécrit version= dans app/main.py, bump VERSION et crée le tag git. Le CHANGELOG.md est la source d'évolution de référence (les bannières README/WORKLOAD sont figées plus tôt — voir §26).
25. Historique des versions v1.0 → v7.70.0
Version courante : 7.70.0 (VERSION). Bandes thématiques vérifiées contre les titres réels du CHANGELOG.md (⚠️ le CHANGELOG saute v2.8→v4.5, documentées uniquement dans ROADMAP.md ; plusieurs versions portent un titre « bande roadmap » décalé — ex. ## v5.11.7 — v5.8.0 Calendrier & Rappels — et trois 7.46.0 distinctes coexistent).
Socle v1.0 – v2.x (résumé, inchangé du doc v4.0)
- v1.0–v1.2 — clone de board Kanban Gitea (boards, cartes, colonnes, notes) ; UI Notion-style.
- v1.3–v1.5 — Collections indépendantes, 21 types de propriétés, relations, rollups (12 agrégations), formules (19 fonctions), GiteaBoardCompat.
- v1.6–v1.7 — vues multiples (Table/Board/Calendar/Gallery/List/Timeline/Status Overview/Team Load).
- v1.8 — sub-items + dépendances Blocking/Blocked by. v1.9 — My Tasks.
- v2.0 — workspaces + rôles, commentaires, historique de page, favoris, templates, import/export CSV, partage public. v2.1–v2.7 — polish, pages privées Gitea, workspaces multiples.
v4.x — Plateforme comptes & partage
- v4.0.0 — Accounts, Integrations & Sharing (MVP) : types de comptes, matrice d'intégrations, Library, share/publish, trash (§6, §15).
- v4.0.1–v4.0.2 — Onboarding & polish ; qualité/robustesse. v4.1–v4.2 — data sources & linked databases ; templates & dashboards. v4.3 — Database Views complètes (10 types). v4.4–v4.5 — tasks/sub-items/dependencies ; sprints & My Tasks.
- v4.6–v4.9 — callouts, TOC, KaTeX self-hosté, multi-colonnes, toggles ; export MD/PDF/HTML/site-zip ; bloc tableau ; commentaires inline + @mentions + notifications (+ SMTP).
- v4.10–v4.15 — naissance de l'agent : moteur ReAct natif (§16.2), config LLM dans l'UI, clés par utilisateur + multi-fournisseurs dynamiques, panneau « Notion AI » Ctrl+J, contexte universel, AI Meeting Note (ébauche), mentions @ et skills
/dans le panneau.
v5.x — Socle moderne
- v5.0–v5.2 — palette Ctrl+K + FTS5 ; migrations versionnées
app/migrations.py; collage intelligent, inline databases, templates de bases, validation de propriétés ; moteur d'automatisations if-this-then-that ; sessions révocables + tokens API ; backup interne. - v5.3–v5.9 — temps réel WebSocket (v5.3, ébauche LWW + polling) ; interactions de bloc (undo/redo, menu ⋮, drag multi-sélection) ; partage réorganisé ; database avancée (personnes, vues sauvegardées, swimlanes + WIP limits, calendar drag & drop, gallery) ; calendrier & rappels (récurrences RRULE, fuseaux) ; import 6 phases ; AI Writing dans l'éditeur.
- v5.11–v5.15 — wiki-links
[[+ chips, mentions page/date ; templates et verrouillage de page, pleine largeur ; synced blocks ; webhooks v2.
v6.x — Plateforme ouverte & entreprise
- v6.0–v6.2 — PWA offline (SW + IndexedDB + sync) ; permissions granulaires (ACL page/collection/propriété + groupes + audit) ; Web Clipper MV3.
- v6.3–v6.6 — API publique REST
/api/v2complè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-evalretiré (v7.43) ; éditeur visuel d'automations (v7.45) ; palette avec onglet Réponses IA ; navigation partielle anti-FOUC ; web tools de l'agent + 11 skills (v7.46) ; sidebar à onglets Home/Chat/Meeting/Inbox ; My Tasks 3 vues transverse ; side peek plein écran. - v7.51–v7.58 — Menu + de l'assistant en 8 phases : hub de contexte, skills gérer/parcourir, canevas Design System, mémoire d'agent, connecteurs (socle → Google/M365 OAuth lecture seule → Discord/Telegram/Teams/MCP), plugins catalogue on/off à effet réel.
- v7.59–v7.70.0 — éditeur façon Notion : mobile (drawer réglable, menus au
/), bouton + d'insertion, menus contextuels de bloc et de sélection refondus, commentaires ancrés sur la sélection de texte (surlignage jaune, tiroir refondu), réactions sur texte + tiroir, copier/couper/coller multi-blocs, colonnes réellement rendues, tables GFM, identité visuelle (logo/bannière/icônes PWA/og:image), Library (onglet AI Meeting Notes groupé par date, Recents réels, Publish unifié, colonnes visited/source) ; projet Gitea : miroir local ouvert en parallèle du dépôt, sidebar Repository distincte.
26. Chantiers ouverts & suites
Extraits de ROADMAP.md (audit du 2026-09-30) et WORKLOAD.md :
- Cycle v6 et cycle v7 (v6.8→v7.3) livrés intégralement ;
WORKLOAD.md: 52/52 features, tableau de bord figé à v7.3.0 (les ~36 versions suivantes ne sont suivies que par le CHANGELOG). - Audit A1–A43 quasi clos. Restes ouverts / décisions assumées :
- A38 phase 3 : fusion des
workspace-tree.jsLibrary / 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.
- A38 phase 3 : fusion des
- Faiblesses documentaires connues : bannières
README.md(cite 7.3.9 / 764 tests) etTESTING.md(v2.1) à rafraîchir ; divergence BRANCHING ↔ CONTRIBUTING sur la cible de merge des features. - Pistes naturelles de la suite du produit (non planifiées formellement) : serveur MCP officiel (
docs/FLOWDECK_MCP_SERVER_GUIDE.md), export de masse enrichi, collaboration avancée (suggestions d'édition étendues), PostgreSQL si un jour un dual-engine est décidé (aujourd'hui non supporté, §5.1).
27. Références
| Document | Contenu |
|---|---|
docs/DATA_MODEL.md |
modèle de données exhaustif (105 tables, colonnes, FK, JSON, relations) |
docs/API_GUIDE_V6.md + docs/openapi-v2.json + /docs |
API publique v2 (guide + spécification OpenAPI vivante) |
docs/Guide_Complet_Notion_database.md, NOTION_DATABASE_TASKS_GUIDE.md |
systèmes de bases et de tâches |
docs/Guide_Complet_Notion_AI.md, Guide_Complet_Notion_Agent_2026.md, Flowdeck_Agent_integration.md |
benchmark IA, design agent |
docs/Guide_Complet_Notion_sharing_collaborartion.md |
partage & collaboration |
docs/V6_SSO_SAML_Enterprise_Auth.md, V6_Granular_Permissions.md, V6_PWA_Progressive_Web_App.md, V6_Web_Clipper.md |
features v6.x |
docs/V68_Sites_Forms.md, V69_Search_Ask_AI.md, V70_Automations_Workers.md, V71_Calendar_Meetings.md, V72_Enterprise_SCIM_2FA.md, V73_Wiki_Teamspaces_Polish.md, V74_Agent_Plus_Menu.md |
features v6.8–v7.4 |
docs/architecture-meeting-notion-flowdeck.md, docs/FLOWDECK_MCP_SERVER_GUIDE.md |
spec Meeting Notes ; conception serveur MCP (non implémentée) |
CHANGELOG.md |
journal détaillé v7.36→v7.70.0 (et au-delà, désordonné — voir §25) |
ROADMAP.md |
plans v2.8–v4.5 + audit A1–A43 + suivi des cycles v6/v7 |
WORKLOAD.md, TESTING.md, CONTRIBUTING.md, BRANCHING.md |
pilotage, grille visuelle, conventions, branches |
Ce document décrit l'état du projet à la version 7.70.0 (2026-10-09). Les affirmations marquées « factuel » ou « ⚠️ » signalent des écarts connus entre le code et les documents de pilotage plus anciens.