Files
flowdeck/ARCHITECTURE.md
T
bruno 0218d8f5e5
FlowDeck CI / lint (push) Successful in 1m37s
FlowDeck CI / test (push) Failing after 24m19s
FlowDeck CI / docker (push) Skipped
fix: /local-workspace — chemin du header + clic sur un dossier du sidebar (v7.69.9)
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.
2026-10-09 16:58:04 -04:00

96 KiB
Raw Blame History

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
  2. Architecture système
  3. Structure du backend
  4. Routage & API
  5. Modèle de données
  6. Authentification & comptes
  7. SSO entreprise : SAML, OIDC, SCIM, 2FA, WebAuthn
  8. Sécurité applicative
  9. Autorisation : rôles, permissions granulaires, gouvernance
  10. Workspaces & collaboration multi-utilisateurs
  11. Databases : propriétés, formules, rollups, dépendances
  12. Système de vues
  13. Pages, éditeur de blocs & temps réel
  14. Wiki, liens, blocs synchronisés & teamspaces
  15. Partage, publication, Library, My Tasks & Trash
  16. IA : agent, compétences, connecteurs & écriture
  17. Recherche : FTS5, sémantique hybride & Ask AI
  18. AI Meeting Notes & calendrier
  19. Automatisations, workers, rappels, notifications & webhooks
  20. Intégration Gitea / GitHub
  21. Import & Export
  22. Frontend : rendu, CSP, PWA & Web Clipper
  23. Déploiement & exploitation
  24. Outils de développement, tests & CI
  25. Historique des versions v1.0 → v7.69.8
  26. Chantiers ouverts & suites
  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

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)
  1. 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
/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é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

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 :

{
  "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.

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
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é

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

# 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.