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.
39 KiB
FlowDeck — Modèle de données (v7.69.8)
Document de référence du schéma de persistance, généré à partir du code réel :
app/db.py— schéma de base (baseline, « version 1 »), créé de façon idempotente parinit_db()(lignes 14–839) et connexion SQLite viaget_conn()(lignes 843–858) ;app/migrations.py— migrations versionnées (1–38, tableschema_version) ;app/services/webhook_outbound.py— tablewebhook_subscriptions(créée au boot, l. 272–286) ;app/models/requests.py/app/models/responses.py— couche Pydantic de l'API.
Total : 105 tables relationnelles + 1 table virtuelle FTS5 (pages_fts), réparties entre le baseline (app/db.py) et 33 migrations effectuant des créations de tables (app/migrations.py).
1. Moteur de base de données
1.1 SQLite uniquement (pas de dual-engine)
Correction vis-à-vis de certaines notes d'architecture : FlowDeck ne supporte PAS PostgreSQL. Le code est exclusivement SQLite :
app/db.pyl.4 :import sqlite3(stdlib) — aucune dépendancepsycopg/pg8000/SQLAlchemy nulle part (requirements.txt,pyproject.tomlvérifiés) ;- le module entier est typé
sqlite3.Connection(y comprisapp/migrations.pyl.16) ; - fonctionnalités SQLite spécifiques utilisées :
PRAGMA journal_mode=WAL,PRAGMA foreign_keys=ON,PRAGMA busy_timeout=5000(get_conn, l.846–850),INTEGER PRIMARY KEY AUTOINCREMENT,ALTER TABLE … ADD COLUMN(sansIF NOT EXISTS, d'où le patterntry/except sqlite3.OperationalError), déclencheurs (TRIGGER), extension FTS5,DROP TABLE/RENAMEpour les reconstructions (comments,tags) ; app/config.pyl.58 :database_url: str = "sqlite:////data/flowdeck.db"— seul le préfixesqlite:///est interprété par la propriétédb_path(l.151–163, avec gestion:memory:et des chemins Windows typeC:\…) ; toute autre valeur retombe sur/data/flowdeck.db.
La « compatibilité » mentionnée dans certains documents est donc en réalité : une base SQLite unique par instance, en mode WAL (un writer, multi-readers), avec PRAGMA busy_timeout=5000 pour supporter la concurrence (tests xdist, scheduler d'automations). Les backups (v5.2.0, backup_* dans app/config.py) sont des copies du fichier SQLite.
1.2 Stratégie de migrations
Décrite dans l'en-tête de app/migrations.py (l.1–13) :
| Composant | Rôle |
|---|---|
app/db.py::init_db() |
Schéma baseline (« version 1 ») : CREATE TABLE IF NOT EXISTS + ALTER garde-fous (try/except OperationalError). Réexécuté à chaque boot, idempotent. |
schema_version (version INTEGER PK, name TEXT, applied_at) |
Registre des migrations appliquées ; version maximale lue par current_version() (l.66–71). Scellée à BASELINE_VERSION = 1 au premier démarrage (l.86–101). |
@register(version, name) (l.28–38) |
Décorateur d'enregistrement dans MIGRATIONS, trié par version, doublons refusés. |
apply_migrations(conn) (l.86–111) |
Appelée en fin de init_db() (app/db.py l.829–831) ; applique exactement une fois chaque migration > version courante. |
_apply_one() (l.113–130) |
Une migration = une transaction (BEGIN explicite, rollback complet à l'échec, puis INSERT INTO schema_version) — DDL tout-ou-rien (audit A31). |
columns(conn, table) (l.53–64) |
Helper unique PRAGMA table_info pour tester l'existence de colonnes avant un ALTER. |
fts5_available() (l.74–83) |
Détection de FTS5 ; sinon repli sur recherche LIKE. |
Version de schéma actuelle : 38 (v7.1.1 meeting block, dernier @register). Les migrations 4 et 17 et 23–38 ont été enregistrées hors ordre chronologique de version (versions 4, 17, 23 insérées après 21) — le tri à l'enregistrement garantit l'ordre d'application.
2. Tables par domaine
Légende : PK = clé primaire, FK = clé étrangère (avec cascade quand indiquée). Sauf mention, PK = id INTEGER PK AUTOINCREMENT.
2.1 Kanban & intégration Gitea (héritage v0.x–v1.x)
Créées dans app/db.py (l.22–140, 430–478).
| Table | But | Clés / colonnes clés |
|---|---|---|
boards |
Board Kanban par dépôt Gitea | UNIQUE(project_owner, project_name) ; columns_json (liste des colonnes), wip_limits_json |
cards |
Position d'une issue Gitea dans une colonne | FK board_id → boards ; gitea_issue_id, column_name, position, priority, due_date |
col_mapping |
Mapping colonne ↔ label Gitea | FK board_id → boards ; UNIQUE(board_id, column_name) ; close_issue |
notes |
Notes par projet | UNIQUE(project_owner, project_name, title) ; content |
checklists |
Checklists par issue | board_id, gitea_issue_id (pas de FK déclarée), title, position, UNIQUE(board_id, gitea_issue_id, title) |
checklist_items |
Lignes de checklist | FK checklist_id → checklists ON DELETE CASCADE ; content, checked, position |
project_properties |
Propriétés par projet (ancêtre de collection_properties, conservé) |
UNIQUE(project_owner, project_name, name) ; prop_type, options_json |
property_values |
Valeurs de ces propriétés par issue | FK property_id → project_properties ON DELETE CASCADE ; UNIQUE(property_id, gitea_issue_id) |
ai_keywords |
Fréquentité des mots-clés détectés par l'IA | UNIQUE(project_owner, project_name, keyword) ; color, usage_count |
gitea_private_pages |
Pages privées rattachées à un dépôt Gitea (v2.7) | FK user_id → users ; gitea_owner, gitea_repo, content |
user_tokens |
Token Gitea par utilisateur | gitea_user_id INTEGER UNIQUE, gitea_token |
projects |
Registre forge-agnostique des projets (gitea/github/builtin), synchronisé par cron (mig. 6, migrations.py l.260–331) |
UNIQUE(proj_type, owner, name) ; clone_url, default_branch, last_synced_at |
2.2 Utilisateurs & authentification
| Table | But | Clés / colonnes clés | Emplacement |
|---|---|---|---|
users |
Compte local ou fédéré | login UNIQUE, email, password_hash, is_admin, is_active, login_attempts, locked_until, auth_method (local|gitea), avatar_url/color, sidebar_config (JSON), notification_prefs (JSON), timezone, totp_secret_enc, totp_backup_hashes (JSON) |
db.py l.22 ; ALTER db.py l.328–470, mig. 10/11/28 |
login_history |
Journal des connexions | FK user_id → users ; ip_address, user_agent |
db.py l.40 |
user_oauth_tokens |
Tokens OAuth par provider (Gitea, Google, M365…) | FK user_id → users ; UNIQUE(user_id, provider) ; access_token, refresh_token, expires_at |
db.py l.55 |
user_sessions |
Sessions cookie révocables (une ligne = un cookie signé) | PK id TEXT (payload du cookie) ; FK user_id → users ON DELETE CASCADE ; revoked, last_seen_at |
mig. 6 (l.294–316) |
api_tokens |
Tokens Bearer publics (sha256 stocké) | FK user_id → users ON DELETE CASCADE ; token_hash UNIQUE, token_prefix, scopes, expires_at, revoked |
mig. 6 + mig. 20 (l.884–900) |
webauthn_credentials |
Passkeys / 2FA WebAuthn | FK user_id → users ON DELETE CASCADE ; credential_id UNIQUE, public_key, sign_count |
mig. 28 (l.1311–1324) |
sso_config |
Config SSO entreprise (SAML 2.0 ou OIDC), la table prime sur .env dès qu'un admin sauvegarde |
FK workspace_id → workspaces ; secrets chiffrés au repos (client_secret, sp_private_key — app/services/sso_provisioning.py) ; attribute_mapping/groups_mapping (JSON), active |
mig. 23 (l.1495–1523) |
sso_login_history |
Audit de chaque tentative SSO (succès + rejets) | FK user_id → users ; success, error_message, sso_identifier |
mig. 23 (l.1525–1545) |
sso_requests |
Store mono-usage anti-replay (AuthnRequest ID, état OIDC, PKCE, relay state) | PK id TEXT ; kind, code_verifier, next_path, used |
mig. 23 (l.1547–1555) |
scim_tokens |
Tokens Bearer de provisioning SCIM (hash unique) | token_hash UNIQUE, revoked, FK created_by → users |
mig. 28 (l.1285–1294) |
domain_claims |
Domaines d'entreprise vérifiés (DNS/txt), auto-jonction + enforce_sso |
domain UNIQUE, txt_token, FK workspace_id → workspaces |
mig. 28 (l.1296–1307) |
llm_config |
Config LLM globale de l'instance — ligne unique CHECK (id = 1) |
provider, model, api_key, api_base, verified, verified_model, last_error ; précédence : ligne DB > settings.llm_* |
db.py l.769–784 |
user_llm_keys |
Clés API LLM par utilisateur et provider | FK user_id → users ON DELETE CASCADE ; UNIQUE(user_id, provider) ; models_json (cache des modèles), default_model, verified* |
db.py l.788–812 |
2.3 Workspaces, collaboration & permissions
| Table | But | Clés / colonnes clés | Emplacement |
|---|---|---|---|
workspaces |
Espace de travail multi-utilisateur | FK owner_id → users ; settings_json |
db.py l.222 |
workspace_members |
Adhésion + rôle (owner/admin/editor/viewer/commenter) |
FK workspace_id → workspaces CASCADE, user_id → users ; UNIQUE(workspace_id, user_id) |
db.py l.230 |
teamspaces |
Namespace de pages/bases dans un workspace (v7.3.0) ; private=1 = invisibilité 404 |
FK workspace_id → workspaces CASCADE ; UNIQUE(workspace_id, name) |
mig. 29 (l.1380–1391) |
teamspace_members |
Membres + rôle d'un teamspace | FK teamspace_id → teamspaces CASCADE, user_id → users ; UNIQUE(teamspace_id, user_id) |
mig. 29 (l.1393–1403) |
user_groups |
Groupes réutilisables par workspace (ACL) | FK workspace_id → workspaces CASCADE ; UNIQUE(workspace_id, name) |
mig. 18 (l.721–730) |
group_members |
Adhésion à un groupe | FK group_id → user_groups CASCADE, user_id → users CASCADE ; UNIQUE(group_id, user_id) |
mig. 18 (l.732–740) |
page_permissions |
Grant explicite sur une page de l'éditeur | FK page_id → pages CASCADE ; user_id XOR group_id (CHECK) ; role (viewer/commenter/editor/owner), grant_type, granted_by |
mig. 18 (l.745–760) |
collection_permissions |
Idem pour les bases (collections) | FK collection_id → collections CASCADE ; mêmes colonnes |
mig. 18 (l.766–780) |
property_permissions |
Grant par propriété de collection (view/edit) | FK collection_id, property_id → collection_properties CASCADE |
mig. 18 (l.782–797) |
permission_audit_log |
Traînée immuable grant/revoke | resource_type, resource_id, action, old_role, new_role, performed_by → users, ip_address |
mig. 18 (l.799–815) |
page_shares |
Partage de page (user, groupe ou email) | FK page_id → pages CASCADE, shared_with_user_id → users, shared_with_group_id → user_groups ; permission (view/edit/comment) |
db.py l.451–478 |
guest_shares |
Accès sans compte via /g/<token> (v7.3.0) |
FK page_id → pages CASCADE ; token UNIQUE, role, expires_at, revoked |
mig. 29 (l.1440–1452) |
favorites |
Favoris d'un utilisateur (reconstruite en v2.2 pour pointer pages) |
FK user_id → users, page_id → pages ; UNIQUE(user_id, page_id) |
db.py l.260–270 |
recents |
Pages récemment accédées (vue « Recents ») | FK user_id → users, page_id → pages ; source_type (local/gitea…), UNIQUE(user_id, page_id) |
db.py l.481–493 |
notifications |
Notifications in-app (mention/commentaire/page) | FK user_id → users CASCADE, actor_id → users ; ntype, resource_type/resource_id, is_read ; index (user_id, is_read) |
db.py l.592–613 |
comments |
Commentaires (fil, résolus) ancrés sur pages ou lignes de base — reconstruite v4.9 : cible polymorphe | FK user_id → users ; target_type (page | collection_page) + target_id, parent_id (réponses), anchor_block_id/anchor_start/anchor_end (ancre inline) |
db.py l.241 (v2.0) puis l.620–649 (v4.9) |
comment_reactions |
Réactions emoji d'un commentaire | FK comment_id → comments CASCADE, user_id → users CASCADE ; UNIQUE(comment_id, user_id, emoji) |
mig. 29 (l.1418–1428) |
text_reactions |
Réactions ancrées sur une plage de texte d'un bloc (façon Notion, v7.64) | FK page_id → pages CASCADE, user_id → users CASCADE ; block_id TEXT, offsets anchor_start/anchor_end, UNIQUE(page_id, block_id, start, end, emoji, user_id) |
mig. 37 (l.1683–1707) |
page_follows |
Abonnement aux changements d'une page | PK composite (page_id → pages CASCADE, user_id → users CASCADE) |
mig. 29 (l.1430–1437) |
page_verifications |
Badge « page vérifiée ✅ » avec expiration | PK page_id → pages CASCADE (1 par page) ; verified_by → users, expires_at |
mig. 29 (l.1407–1416) |
tags |
Étiquettes par utilisateur (user_id=0 = globales) |
FK user_id → users ; UNIQUE(name, user_id) ; color |
db.py l.282–290 |
page_tags |
Association page ↔ tag | PK composite (page_id → pages CASCADE, tag_id → tags CASCADE) |
db.py l.292–299 |
custom_emojis |
Emojis personnalisés uploadés par workspace | FK workspace_id (défaut 1, non déclaré) ; name, url |
mig. 8 (l.371–388) |
2.4 Pages & éditeur de blocs
| Table | But | Clés / colonnes clés | Emplacement |
|---|---|---|---|
pages |
Page de l'éditeur (wiki) — table pivot du produit | FK workspace_id → workspaces, parent_id → pages (hiérarchie), collection_id → collections (base plein écran, v4.6), collection_row_id → collection_pages CASCADE (page-ombre de contenu d'une ligne de base, v6.5, mig. 22 l.997–1020), locked_by → users ; colonnes notables : content (markdown legacy ou JSON de blocs selon content_format), content_format, parent_section, share_mode, published/is_published/publish_slug/is_shared, deleted_at (corbeille), sort_order, cover_url, page_icon, is_locked, full_width, font_small, search_excluded, permission_type (inherit/restricted/private), teamspace_id (ALTER, sans FK déclarée), sync_version (trigger de concurrence optimiste, mig. 17) |
db.py l.154–166 + ALTER l.328–470 ; mig. 7/13/18/22/24/25/29 |
page_versions |
Snapshots annulables de l'éditeur de blocs (undo/restore, une ligne par save) | FK page_id → pages CASCADE ; title, blocks_json (liste complète des blocs), note, user_id → users |
mig. 7 (l.336–365) |
page_history |
Historique legacy des lignes de base (collection_pages) |
FK page_id → collection_pages CASCADE ; change_type, snapshot_json |
db.py l.249 |
page_global_templates |
Modèles de page globaux créés par les utilisateurs | blocks_json (format éditeur), FK created_by → users SET NULL |
mig. 13 (l.582–597) |
synced_blocks |
Bloc synchronisé : source de vérité du contenu, réutilisé sur plusieurs pages | content (JSON blocs), created_by → users (sans FK), workspace (TEXT), synced via page_synced_blocks |
mig. 15 (l.626–662) |
page_synced_blocks |
Référence page ↔ bloc synchronisé (désynchronisable par page) | FK page_id → pages CASCADE, synced_block_id → synced_blocks CASCADE ; block_index, UNIQUE(page_id, synced_block_id) |
mig. 15 (l.646–662) |
page_views |
Compteur de vues quotidien par page (même pattern que site_views) |
FK page_id → pages CASCADE ; PK composite (page_id, day), views |
mig. 29 (l.1454–1461) |
pages_fts |
Table virtuelle FTS5 (title, body) pour la recherche plein texte de la palette de commandes ; synchronisée par 3 triggers AFTER INSERT/UPDATE/DELETE sur pages ; backfill au boot ; repli LIKE si FTS5 absent |
rowid = pages.id |
mig. 3 (l.156–205) |
2.5 Bases de données (collections, style Notion)
| Table | But | Clés / colonnes clés | Emplacement |
|---|---|---|---|
collections |
Base de données (collection) | schema_json (schéma des propriétés), FK gitea_owner/gitea_repo (texte, verrouillage sync), workspace_id → workspaces, created_by → users, is_locked, is_inline + parent_page_id (base inline), is_task + task_assignee_prop/task_status_prop/task_due_prop → collection_properties SET NULL (mapping My Tasks, mig. 30 l.1558–1585), permission_type, teamspace_id (sans FK), form_config_json, sync_version |
db.py l.172 + ALTER db.py l.495–520, mig. 18/24/29/30 |
collection_pages |
Ligne d'une base (row) | FK collection_id → collections CASCADE, parent_id → collection_pages (hiérarchie interne), property_values_json (valeurs par propriété), gitea_issue_id/gitea_issue_number (sync Kanban), cover_url, external_event_id (calendrier, mig. 27), permission_type, sync_version |
db.py l.187 ; mig. 10/18/27 |
collection_views |
Vues sauvegardées par base (table/board/calendar/gallery/list…) | FK collection_id → collections CASCADE ; view_type, config_json (filtres/tri/groupement/columns visibles), position, created_by (NULL = vue partagée, sinon personnelle — mig. 10) |
db.py l.210 ; mig. 10 (l.472–478) |
collection_properties |
Schéma des propriétés d'une base (remplace project_properties) |
FK collection_id → collections CASCADE ; prop_type, options_json (select/multi-select), number_format, relationnels : related_collection_id → collections SET NULL, relation_property_id/target_property_id → collection_properties SET NULL, rollup_function, formula_expression, validation_json (mig. 4), group_name (mig. 10), button_automation_id → automations SET NULL (bouton natif, mig. 26) ; UNIQUE(collection_id, name) |
db.py l.222–241 ; mig. 4/10/26 |
collection_data_sources |
Sources de données liées (base liée type Notion « Linked database ») | FK collection_id → collections CASCADE, source_collection_id → collections ; is_linked, UNIQUE(collection_id, source_collection_id) |
db.py l.480–493 |
collection_dashboards |
Dashboard = combinaison de vues/widgets sur une page | FK collection_id → collections CASCADE ; layout_json ({"columns":1,"widgets":[]}) |
db.py l.531–542 |
database_templates |
Modèles de bases prêts à l'emploi (seed idempotent, v5.3.0 : Meeting notes, etc.) | name, icon, description, schema_json |
db.py l.272 + mig. 4 (l.390–411) ; seed via app/services/db_templates.py::SEED_TEMPLATES |
page_templates |
Modèles de ligne par base, éventuellement récurrents | FK collection_id → collections CASCADE ; property_values_json, content_json (blocs), is_recurring + recurrence_rule |
db.py l.294–303 + ALTER l.512–522 |
page_dependencies |
Dépendances entre lignes (bloque / bloqué par) | FK page_id, dependency_id → collection_pages CASCADE ; dependency_type, auto_shift (overlap/…) ; UNIQUE(page_id, dependency_id, dependency_type) |
db.py l.551–563 |
sprints |
Sprints Scrum par base de tâches | FK collection_id → collections CASCADE ; start_date/end_date (TEXT), status (planning/active/done), goal, auto_complete |
db.py l.570–582 |
sprint_pages |
Lignes affectées à un sprint + vélocité | FK sprint_id → sprints CASCADE, page_id → collection_pages CASCADE ; velocity_points, status_at_start, UNIQUE(sprint_id, page_id) |
db.py l.584–594 |
reminder_log |
Journal anti-doublon des rappels : 1 ligne par (ligne de base, date d'occurrence) — le rappel ne fire qu'une fois même après restart | FK page_id → collection_pages CASCADE ; UNIQUE(page_id, occurrence_date) ; alimenté par le scan reminders_enabled (app/config.py l.75–77) — il n'existe pas de table reminders ; les dates de rappel proviennent des propriétés due des lignes |
mig. 11 (l.485–509) |
2.6 Automatisation, workers & webhooks
| Table | But | Clés / colonnes clés | Emplacement |
|---|---|---|---|
automations |
Règles if-this-then-that (moteur v5.1.0) : trigger_type event/cron/button, event, cron_expression, collection_id (TEXT sans FK, scope optionnel), condition_json (clauses), actions_json (descripteurs), enabled, run_count, last_run_at, trigger_mode (any|all, fenêtre de 5 min, mig. 26) |
mig. 5 (l.208–257) + mig. 26 (l.1196–1201) | |
automation_steps |
Étapes ordonnées d'une automatisation v2 (trigger/condition/delay/action, v7.0.0) ; l'engine retombe sur condition_json/actions_json si aucune step |
FK automation_id → automations CASCADE ; kind, position, config_json |
mig. 26 (l.1156–1172) |
automation_runs |
Historique d'exécution (audit + UI Settings) | FK automation_id → automations CASCADE ; status (fired/skipped/error), trigger_source, collection_id/page_id (TEXT/INT sans FK déclarée), detail |
mig. 5 (l.242–257) |
workers |
Snippets Python sandboxés (cron ou manuel), partageables (v7.0.0) | slug UNIQUE, FK workspace_id → workspaces CASCADE, code_py, schedule_cron, shared, daily_budget_s (budget CPU quotidien), created_by → users (sans FK déclarée) |
mig. 26 (l.1174–1192) |
worker_runs |
Logs d'exécution des workers | FK worker_id → workers CASCADE ; status, logs, duration_ms |
mig. 26 (l.1194–1210) |
webhook_subscriptions |
Abonnement sortant URL + event + secret (créée au boot hors migrations, appelée par init_db l.834–837) |
url, event, secret, active |
app/services/webhook_outbound.py l.272–286 |
webhook_deliveries |
Journal de livraison (retry ledger) | FK webhook_id → webhook_subscriptions CASCADE ; status (pending/retrying/success/error/superseded), http_code, attempt, event, next_retry_at (epoch, scheduler v6.4), payload (JSON), duration_ms |
mig. 20 (l.913–928) + mig. 21 (l.940–957) |
idempotency_keys |
Support d'Idempotency-Key sur les POST de l'API v2 (réponse rejouée) |
PK key TEXT ; FK user_id → users CASCADE ; response_json, status_code |
mig. 20 (l.930–938) |
2.7 IA / Agent FlowDeck
| Table | But | Clés / colonnes clés | Emplacement |
|---|---|---|---|
agents |
Définition d'agent (personnel ou workspace) | FK workspace_id → workspaces, created_by → users ; agent_type, system_instructions, model, scope_json, trigger_json, approval_mode (auto/gated), is_active, UNIQUE(workspace_id, name) |
db.py l.660–680 |
agent_conversations |
Conversation d'un utilisateur avec un agent | FK agent_id → agents CASCADE, user_id → users ; status, context_json, override provider/model (ALTER db.py l.820–827), memory_enabled (mig. 31) |
db.py l.681–691 |
agent_messages |
Messages (rôles user/assistant/tool), coût tokens | FK conversation_id → agent_conversations CASCADE ; tool_calls_json, model, tokens_used |
db.py l.692–702 |
agent_actions |
Journal des tool calls exécutés (audit + undo) | FK conversation_id → … CASCADE ; tool_name, target_type/target_id, payload_json, result_json, status, undo_snapshot_json, executed_by → users |
db.py l.703–716 |
agent_skills |
Skills réutilisables (prompt + outils autorisés) | FK workspace_id → workspaces, created_by → users ; prompt_template, allowed_tools_json, UNIQUE(workspace_id, name) |
db.py l.717–729 |
agent_triggers |
Déclencheurs d'agent (manual/cron/événement) | FK agent_id → agents CASCADE ; trigger_type, config_json, is_active, last_fired_at |
db.py l.730–740 |
agent_feedback |
👍/👎 sur les réponses (rating CHECK up/down) | FK conversation_id/message_id/user_id en SET NULL ; snippet, comment |
db.py l.742–756 |
agent_policies |
Gouvernance par workspace (1 ligne/workspace) | FK workspace_id → workspaces CASCADE, UNIQUE(workspace_id) ; allowed_tools_json, max_steps, require_approval |
mig. 28 (l.1326–1338) |
agent_approvals |
File d'approbation des actions d'écriture gated | conversation_id (INT sans FK déclarée), tool, args_json, status (pending/…), requester_id/approver_id → users |
mig. 28 (l.1340–1354) |
agent_memory |
Résumé mémorisé par conversation (upsert 1:1, v7.54.0) | PK conversation_id → agent_conversations CASCADE ; kind (summary), content |
mig. 31 (l.1661–1681) |
agent_connectors |
Catalogue de connecteurs d'agent (custom/Discord/Telegram/MCP, v7.55–7.57) | name, url, secret_encrypted (Fernet), kind, auth (bearer/…), tools_json (cache d'outils MCP), enabled, status, created_by (sans FK) |
mig. 32 (l.1640–1659) + mig. 34 (l.1613–1624) |
connector_tokens |
Tokens OAuth par (kind, user) — Google / M365, chiffrés Fernet | PK composite (kind, user_id) ; tokens_enc |
mig. 33 (l.1626–1638) |
plugins |
Registre des modules activables/désactivables de l'instance (web-tools, web-clipper, automations) — seed idempotent | PK slug TEXT ; enabled |
mig. 35 (l.1589–1611) |
semantic_embeddings |
Recherche sémantique hybride + Ask AI (v6.9) : vecteurs « hashed-TF » 256 dims sans dépendance externe ; chunks par ressource polymorphe | PK composite (resource_type, resource_id, chunk_id) (page | collection…) ; chunk_text, embedding BLOB, model |
mig. 25 (l.1096–1133) |
semantic_index_state |
État d'indexation incrémentale (dernier index par ressource) | PK composite (resource_type, resource_id) ; indexed_at |
mig. 25 (l.1122–1133) |
2.8 Sites publics & formulaires
(mig. 24, migrations.py l.1021–1093)
| Table | But | Clés / colonnes clés |
|---|---|---|
sites |
Mini-site public multi-pages (façon Notion Sites) | slug UNIQUE, FK root_page_id → pages CASCADE, custom_domain UNIQUE, theme, password_hash, expires_at, noindex, analytics_id, created_by → users |
site_pages |
Arbre public ordonné des pages du site | FK site_id → sites CASCADE, page_id → pages CASCADE ; position, UNIQUE(site_id, page_id) |
site_views |
Compteur de vues jour/site (upsert, pas d'IP brute — RGPD) | FK site_id → sites CASCADE ; PK composite (site_id, day), views |
form_responses |
Journal anonymisé des soumissions de formulaire public | FK collection_id → collections CASCADE, row_id → collection_pages SET NULL (ligne créée) ; ip_hash (hash tournant quotidien, pas d'IP) ; config du form dans collections.form_config_json |
2.9 Calendrier & réunions
| Table | But | Clés / colonnes clés | Emplacement |
|---|---|---|---|
calendar_links |
Lien par utilisateur entre une collection et un calendrier externe (Google REST ou CalDAV générique) | FK user_id → users CASCADE, collection_id → collections CASCADE ; provider, tokens_enc (Fernet), calendar_id, date_property, sync_token, UNIQUE(user_id, provider, calendar_id) |
mig. 27 (l.1231–1245) |
meeting_transcripts |
Note de réunion (audio + transcription + résumé IA), machine à états du bloc Notion-like (idle/recording/paused/processing/done/failed) | FK page_id → pages CASCADE ; audio_path, transcript, summary, language ; v7.1.1 : status, mode, channels_json, instruction_snapshot, consent_json, segments_json, processing_steps_json, summary_json, generated_title, completeness_class, quality_score, duration_ms/paused_ms, event_occurrence_id (index unique partiel : une occurrence calendrier = au plus une note) |
mig. 27 (l.1247–1271) + mig. 38 (l.1730–1800) |
meeting_consent_log |
Journal append-only du consentement d'enregistrement | FK transcript_id → meeting_transcripts CASCADE ; method (start_attestation/…), attested_by → users, participants_json |
mig. 38 (l.1767–1777) |
meeting_distributions |
Journal append-only des diffusions de compte-rendu (copy_link/…) | FK transcript_id → meeting_transcripts CASCADE ; channel, actor_id → users, recipients_json, status |
mig. 38 (l.1779–1788) |
2.10 Import/export & offline (PWA, clipper)
| Table | But | Clés / colonnes clés | Emplacement |
|---|---|---|---|
import_jobs |
Job d'import asynchrone (polling UI) — Notion/CSV/markdown | PK id TEXT (uuid) ; source, filename, status (queued/running/done/error), error, report_json |
mig. 9 (l.434–451) |
import_items |
Dédup : 1 ligne par page importée, clé (workspace, source, external_id) → re-import idempotent | FK workspace_id/page_id sans REFERENCES déclaré ; UNIQUE(workspace_id, source, external_id) |
mig. 9 (l.418–432) |
offline_sync_queue |
File serveur des mutations reçues des clients hors-ligne (/api/v2/sync/batch), rejouables/auditées par device |
FK user_id → users CASCADE ; device_id, type (page_create/update/delete/move, collection_*), payload (JSON), client_timestamp, server_version, status (pending/syncing/synced/failed), retries, error ; détection de conflit édit/édit via sync_version (mig. 17, triggers sur pages, collection_pages, collections) |
mig. 16 (l.668–700) |
extension_devices |
Devices de l'extension Web Clipper (token hashé, scopes) | FK user_id → users CASCADE ; device_id, token_hash, scopes, revoked, UNIQUE(user_id, extension_name, device_id) |
mig. 19 (l.838–857) |
extension_clips |
Clippings capturés (page cible + workspace cible) | FK user_id → users CASCADE, target_page_id → pages SET NULL, target_workspace_id → workspaces SET NULL ; clip_type, source_url |
mig. 19 (l.861–877) |
2.11 API & audit
| Table | But | Clés / colonnes clés | Emplacement |
|---|---|---|---|
api_audit_log |
Traînée immuable des mutations de l'API publique v2 | FK user_id → users SET NULL, token_id → api_tokens SET NULL ; action, resource_type, resource_id, ip_address, detail |
mig. 20 (l.895–911) |
schema_version |
Registre des migrations appliquées (meta) | PK version INTEGER ; name, applied_at |
app/migrations.py l.41–51 |
2.12 Divers
favorites, recents, notes, ai_keywords, login_history déjà couverts ci-dessus. À noter également : les tables temporaires de reconstruction vues dans le code (comments_new, tags_new) ne sont pas des tables permanentes (pattern recreate → copy → drop → rename).
3. Relations clés (graphe FK simplifié)
graph TD
USERS[users]
WS[workspaces]
PAGES[pages]
COLS[collections]
CP[collection_pages]
CPV[collection_properties]
CV[collection_views]
WS -->|owner_id| USERS
WS -->|workspace_members| USERS
WS -->|workspace_id| PAGES
WS -->|workspace_id| COLS
PAGES -->|parent_id self| PAGES
PAGES -->|collection_id| COLS
PAGES -->|collection_row_id CASCADE| CP
CP -->|collection_id CASCADE| COLS
CP -->|parent_id self| CP
CPV -->|collection_id CASCADE| COLS
CPV -->|related_collection_id| COLS
CPV -->|relation/target_property_id self| CPV
CPV -->|button_automation_id| AUTO[automations]
CV -->|collection_id CASCADE| COLS
AUTO -->|collection_id texte sans FK| COLS
AGENTS[agents] -->|workspace_id| WS
CONV[agent_conversations] -->|agent_id CASCADE| AGENTS
CONV -->|user_id| USERS
MSG[agent_messages + actions] -->|conversation_id CASCADE| CONV
GRP[user_groups] -->|workspace_id CASCADE| WS
PPERM[page/collection/property_permissions] -->|user_id XOR group_id| USERS
PPERM -->|group_id| GRP
SITES[sites/site_pages] -->|root/page CASCADE| PAGES
TRANS[meeting_transcripts] -->|page_id CASCADE| PAGES
CAL[calendar_links] -->|user_id| USERS
CAL -->|collection_id CASCADE| COLS
CLIPS[extension_clips] -->|target_page_id| PAGES
Points de relation à retenir :
usersest le hub d'identité : plus de 30 FK entrantes (sessions, tokens, permissions, audit, contenu).workspacesest le hub d'isolation multi-tenant (owner_id,workspace_membersdéfinit le rôle).- Hiérarchie
pages ↔ collection_pages: deux univers de « pages ».pages= éditeur de blocs wiki (FK selfparent_id, soft-deletedeleted_at, corbeille) ;collection_pages= lignes de base. Le pont v6.5 : chaque ligne peut avoir une page-ombrepages.collection_row_id(CASCADE) qui porte son contenu blocs — versions, synced blocks et temps réel opèrent sur elle. - Chaîne de cascades principales :
collections → collection_pages/collection_views/collection_properties → …etpages → page_versions/page_shares/page_synced_blocks/page_permissions/guest_shares/text_reactions. Les suppressions de ressource nettoyent l'éditorial ; les grants d'audit (*_audit_log,api_audit_log,permission_audit_log) privilégientSET NULLsur l'acteur pour survivre à la suppression du compte. - Permissions en 3 niveaux (mig. 18) :
pages/collections/collection_pages.permission_type(inherit|restricted|private) pilote si les tables*_permissions(user XOR group viaCHECK) font foi ; les groupes (user_groups/group_members) sont workspace-scopés ;page_sharesreste le chemin de partage simple,guest_sharesle chemin sans compte. - Sync sémantique Gitea :
boards/cards/col_mapping/checklists/property_valuesetcollection_pages.gitea_issue_id+collections.gitea_owner/reporelient les entités FlowDeck aux issues Gitea (jointures par ID numérique, pas de FK — système externe). - Polymorphisme non-FK :
notifications.resource_type/resource_id,semantic_embeddings.resource_type/resource_id,permission_audit_log.resource_type/resource_id,import_items.page_id,agent_approvals.conversation_id,automations.collection_id,custom_emojis.workspace_id,teamspaces-id surpages/collections(ALTER sans FK) — cohérence maintenue applicativement, pas par le moteur.
4. Colonnes JSON structurantes
Le modèle est fortement « document-oriented » : le JSON TEXT est le format de stockage des structures riches (choix assumé du SQLite sans JSON natif, requêtage via json_extract côté applicatif).
| Domaine | Table.colonne | Contenu |
|---|---|---|
| Kanban | boards.columns_json / wip_limits_json |
liste ordonnée des colonnes ; limites WIP par colonne |
| Kanban/legacy | project_properties.options_json |
options de select |
| Collections | collections.schema_json |
schéma complet des propriétés d'une base (déclaratif, dupliqué avec collection_properties) |
| Collections | collections.form_config_json |
formulaire public (champs, validations) |
| Lignes | collection_pages.property_values_json |
map {property_name → valeur} de la ligne |
| Vues | collection_views.config_json |
config de vue : filtres, sorts, groupements, colonnes visibles/cache, sizing — cœur du système de vues multiples |
| Propriétés | collection_properties.options_json / validation_json |
options select, règles de validation |
| Dashboard | collection_dashboards.layout_json |
colonnes + widgets |
| Modèles | database_templates.schema_json, page_templates.property_values_json / content_json |
blueprint de base / ligne |
| Éditeur | pages.content (avec content_format='blocks') |
tableau JSON des blocs (heading, todo, callout, synced_block, meeting, etc.) — l'unité de contenu fondamentale |
| Historique | page_versions.blocks_json, page_history.snapshot_json, page_global_templates.blocks_json, synced_blocks.content |
snapshots de liste de blocs |
| Workspace | workspaces.settings_json, users.sidebar_config, users.notification_prefs, users.totp_backup_hashes |
préférences |
| Agent | agents.scope_json/trigger_json, agent_conversations.context_json, agent_messages.tool_calls_json, agent_actions.payload_json/result_json/undo_snapshot_json, agent_skills.allowed_tools_json, agent_triggers.config_json, agent_policies.allowed_tools_json, agent_approvals.args_json |
orchestration, audit et undo |
| LLM | user_llm_keys.models_json |
cache des modèles distants |
| Automatisation | automations.condition_json/actions_json, automation_steps.config_json |
DSL des règles (fallback steps ↔ legacy colonnes) |
| Sync offline | offline_sync_queue.payload, idempotency_keys.response_json |
mutation client + réponse rejouée |
| Webhooks | webhook_deliveries.payload |
corps livré |
| Réunions | meeting_transcripts.channels_json/consent_json/segments_json/processing_steps_json/summary_json, meeting_consent_log.participants_json, meeting_distributions.recipients_json |
pipeline transcription |
| Imports | import_jobs.report_json |
rapport par fichier |
| SSO | sso_config.attribute_mapping, groups_mapping, sso_login_history (mapping) |
attributs d'assertion |
5. Couche Pydantic (app/models/)
Les modèles Pydantic ne couvrent qu'une petite partie du contrat API — la majorité des routes FastAPI valide en dur dans les handlers :
app/models/requests.py(75 l.) :FileSaveRequest— sauvegarde de fichier via Gitea (path,content,message,shaoptionnel pour update) ;@model_validator(mode="after")restreint l'extension viaALLOWED_EXTENSIONS/_ext(app/middleware/security) ;IssueCreateRequest/IssueUpdateRequest— création/mutation d'issues Gitea (title ≤ 500,statepattern^(open|closed)$, labels/milestone/assignee en ids virgule) ;CardMoveRequest— déplacement de carte Kanban (owner/repo/issue_id/column) ;ColMappingRequest— mapping colonne ↔ label ;UploadValidationResult— DTO de validation d'upload (filename/size/extension/valid/error).
app/models/responses.py(38 l.) : deux enveloppes standardisées, utilisées enresponse_modelOpenAPI des routes publiques :ErrorResponse {error, detail?};SuccessResponse {status="ok", data?: dict}.
Aucun modèle ORM (pas de SQLAlchemy) : l'accès aux données est du SQL brut sqlite3 via le context-manager get_conn() (app/db.py l.843–858, row_factory = sqlite3.Row), et les conversions dict↔JSON des colonnes *_json sont faites manuellement dans les services.
6. Fiches de synthèse technique
| Aspect | Valeur réelle (vérifiée dans le code) |
|---|---|
| Moteur | SQLite unique, fichier settings.db_path (database_url = sqlite:///…), WAL + busy_timeout=5000, foreign_keys=ON |
| ORM | aucun — SQL brut |
| Pilote | sqlite3 stdlib |
| Migrations | maison, versionnées : baseline v1 (init_db idempotent) + registry @register (app/migrations.py), table schema_version, transaction par migration |
| Version de schéma | 38 |
| Triggers | FTS5 (pages_fts_ai/ad/au), bumps sync_version (*_sync_version_bu) sur pages/collection_pages/collections |
| Full-text | FTS5 optionnel, repli LIKE |
| Recherche vectorielle | semantic_embeddings (hash TF 256-dim en BLOB, PK composite polymorphe, pas d'extension vec0) |
| Chiffrement au repos | Fernet sur calendar_links.tokens_enc, connector_tokens.tokens_enc, secrets SSO ; hash sha256 pour api_tokens.token_hash, scim_tokens.token_hash, extension_devices.token_hash ; password_hash (users) |
| Sauvegardes | copie planifiée du fichier SQLite (backup_*, app/config.py l.66–70) |
| Concurrence | verrou applicatif d'écriture indirect via WAL + busy_timeout ; migration async du 510 call sites synchrones encore en cours (note A21, app/db.py l.848–852) |