Files
flowdeck/ARCHITECTURE.md
bruno 95bc861cdb
FlowDeck CI / lint (push) Successful in 1m13s
FlowDeck CI / test (push) Successful in 9m20s
FlowDeck CI / docker (push) Successful in 1m10s
feat: v6.3.0 API publique complete v2 (REST /api/v2, scopes, OpenAPI)
- Router api_v2.py (~100 endpoints) : tokens, users, workspaces/members,
  collections, pages, proprietes, vues/dashboards, commentaires/mentions,
  notifications, favoris/tags/recents, partage/publish, historique, sprints,
  templates, export/import, forges, recherche FTS, admin, webhooks CRUD
- Helpers api_v2_helpers.py : Bearer unifie (sha256/expires_at/extension_devices),
  scopes hierarchiques read<write<admin, pagination + X-Total-Count, ISO-8601,
  RFC 7807, idempotence, audit, rate-limit par token
- Migration 20 : api_tokens.scopes/expires_at, webhook_deliveries,
  api_audit_log, idempotency_keys
- main.py : handler d'erreurs unifie StarletteHTTPException, /docs + /redoc
- config : PUBLIC_API_INSECURE_OK (dev only), API_V2_RATE_LIMIT_PER_TOKEN
- OpenAPI docs/openapi-v2.json (402 chemins), tests/test_public_api_v2.py (24)
- Docs : CHANGELOG (v6.2.0/6.2.1 clipper + v6.3.0), ROADMAP, API_GUIDE_V6,
  V6_Web_Clipper, README, ARCHITECTURE, /help
- Suite complete 668 verte, ruff OK
2026-09-20 13:19:29 -04:00

81 KiB

Architecture FlowDeck — Document Complet v4.0

Version: 4.0 · Date: 2026-07-20 · Auteur: Hermes-Deepin Cible: Clone Notion intégré à Gitea/GitHub — multi-comptes, multi-intégrations Version déployée: v4.0.x (production : https://flowdeck.dracodev.net)


Table des matières

  1. Vue d'ensemble
  2. Architecture système
  3. Modèle de données
  4. Routage & API
  5. Frontend
  6. Système d'authentification
  7. Intégration Gitea
  8. Système de propriétés
  9. Système de vues
  10. Sub-items & Dépendances
  11. My Tasks & Dashboard unifié
  12. Éditeur de blocs
  13. Multi-utilisateurs & Collaboratif
  14. Déploiement
  15. Références

1. Vue d'ensemble

FlowDeck est un clone de Notion intégré à Gitea. Il recrée l'expérience Notion — databases, propriétés typées, vues multiples, sub-items, dépendances, éditeur de blocs — tout en restant connecté aux repositories et issues Gitea.

Principes architecturaux

  • Serveur-side rendering — Jinja2 + HTMX, pas de SPA lourde
  • SQLite-first — un fichier, zéro maintenance, migration PostgreSQL triviale si scaling futur
  • Gitea-native — chaque database peut être liée à un repo Gitea, les issues deviennent des pages
  • Progressive enhancement — fonctionnel sans JS, enrichi avec Alpine.js + SortableJS
  • Propriétés en JSON — flexibilité maximale du schéma sans migration de table

2. Architecture système

┌──────────────────────────────────────────────────────────────────┐
│                         CLIENT (Browser)                          │
│                                                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  Templates Jinja2 (SSR)                                   │    │
│  │  ├─ base.html            — Layout Notion commun (sidebar)  │    │
│  │  ├─ _header.html         — Topbar + breadcrumbs            │    │
│  │  ├─ _workspace_tree_macro.html — Macro arbre sidebar       │    │
│  │  ├─ landing.html         — Landing page non-auth           │    │
│  │  ├─ workspaces.html      — Page Workspaces (locale/Gitea)  │    │
│  │  ├─ library.html         — Bibliothèque multi-onglets      │    │
│  │  ├─ local_workspace.html — Workspace local (explorer)      │    │
│  │  ├─ page_editor.html     — Éditeur de blocs                │    │
│  │  ├─ trash.html           — Corbeille (local only)          │    │
│  │  ├─ settings.html        — Settings / My Account / Admin    │    │
│  │  ├─ accounts.html        — Gestion comptes admin           │    │
│  │  ├─ board.html           — Kanban (wrapper)                │    │
│  │  ├─ board_fragment.html  — Cartes Kanban                   │    │
│  │  ├─ table_view.html      — Vue table                       │    │
│  │  ├─ login.html           — Login/Register (local + OAuth)   │    │
│  │  ├─ public_page.html     — Page publiée (/p/slug)          │    │
│  │  └─ (legacy: dashboard, detailed_board, card_detail, etc.) │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  JavaScript (servi localement, pas de CDN externe)        │    │
│  │  ├─ Alpine.js 3.14    — Interactivité légère              │    │
│  │  ├─ HTMX 1.9          — AJAX sans JS (legacy)             │    │
│  │  ├─ SortableJS 1.15   — Drag & drop                       │    │
│  │  └─ Prism.js          — Syntax highlighting               │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  CSS custom — Design system Notion, dark theme, 31+ KB     │    │
│  └─────────────────────────────────────────────────────────┘    │
└──────────────────────────┬───────────────────────────────────────┘
                           │ HTTP (HTML fragments / JSON)
┌──────────────────────────▼───────────────────────────────────────┐
│                    FASTAPI (Python 3.11+)                         │
│                                                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  MIDDLEWARE                                               │    │
│  │  ├─ SessionMiddleware   — Sessions utilisateur            │    │
│  │  ├─ CSRFMiddleware      — Protection CSRF                 │    │
│  │  ├─ CORSMiddleware      — CORS (* pour dev local)         │    │
│  │  └─ RateLimiter         — Rate limiting par IP            │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  ROUTERS                                                  │    │
│  │  ├─ main.py           — FastAPI app, lifespan, CORS, 404   │    │
│  │  ├─ dashboard.py      — /, /workspaces, /library, /help    │    │
│  │  │                    — /trash, /settings, /accounts        │    │
│  │  │                    — /p/{slug} (pages publiques)         │    │
│  │  │                    — _sidebar_data() — données sidebar   │    │
│  │  ├─ board.py          — /board/{o}/{r}       Board + vues   │    │
│  │  │                    — /board/api/pages     CRUD pages     │    │
│  │  │                    — /board/api/trash     Trash          │    │
│  │  ├─ api.py            — /api/...             CRUD + sync    │    │
│  │  ├─ auth.py           — /auth/...            OAuth2 + local │    │
│  │  ├─ local_workspace.py— /api/local-workspace CRUD + upload  │    │
│  │  ├─ my_tasks.py       — /my-tasks            Dashboard      │    │
│  │  ├─ library.py        — /api/library/*       API bibliothèque│    │
│  │  ├─ pages.py          — /pages/...           Pages CRUD     │    │
│  │  ├─ collections.py    — /db/...              Collections    │    │
│  │  ├─ editor.py         — /api/editor/...      Block editor   │    │
│  │  ├─ public_api.py     — /api/v1              Public API v1  │    │
│  │  ├─ api_v2.py         — /api/v2              Public API v2  │    │
│  │  │                    — Bearer + scopes, CRUD complet        │    │
│  │  ├─ web_clipper.py    — /api/v2/web-clipper  Web Clipper     │    │
│  │  ├─ permissions.py    — /api/v2 (ACL)        Permissions     │    │
│  │  ├─ workspace.py      — Workspaces API + Gitea projets     │    │
│  │  ├─ webhooks.py       — /webhooks/...        Gitea hooks    │    │
│  │  └─ admin.py          — /api/admin/*         Admin users    │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  SERVICES                                                 │    │
│  │  ├─ GiteaClient (httpx async)  — API Gitea                │    │
│  │  ├─ FormulaEngine             — Moteur de formules        │    │
│  │  ├─ RollupEngine              — Agrégations cross-DB      │    │
│  │  ├─ CollectionSync            — Sync Gitea ↔ collections  │    │
│  │  ├─ DependencyChecker         — Contraintes dépendances   │    │
│  │  ├─ PermissionManager         — ACL multi-user            │    │
│  │  └─ SearchEngine              — FTS5 recherche full-text  │    │
│  └─────────────────────────────────────────────────────────┘    │
│                                                                   │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  DATA LAYER                                               │    │
│  │  ┌─────────────────────────────────────────────────┐    │    │
│  │  │           SQLite (WAL mode, foreign keys ON)      │    │    │
│  │  │                                                  │    │    │
│  │  │  ┌─ collections (v1.3)                           │    │    │
│  │  │  ├─ collection_pages (v1.3)                      │    │    │
│  │  │  ├─ collection_views (v1.3)                      │    │    │
│  │  │  ├─ collection_properties (v1.4)                 │    │    │
│  │  │  ├─ boards             (legacy)                  │    │    │
│  │  │  ├─ cards              (legacy)                  │    │    │
│  │  │  ├─ col_mapping        (legacy)                  │    │    │
│  │  │  ├─ notes                                       │    │    │
│  │  │  ├─ checklists                                  │    │    │
│  │  │  ├─ checklist_items                             │    │    │
│  │  │  ├─ project_properties (legacy → collection_properties) │
│  │  │  ├─ property_values    (legacy → collection_pages)      │
│  │  │  ├─ ai_keywords                                 │    │    │
│  │  │  ├─ pages                                       │    │    │
│  │  │  ├─ page_shares          (v4.0)                  │    │    │
│  │  │  ├─ recents              (v4.0)                  │    │    │
│  │  │  ├─ tags                 (v4.0)                  │    │    │
│  │  │  ├─ page_tags            (v4.0)                  │    │    │
│  │  │  ├─ gitea_private_pages  (v4.0)                  │    │    │
│  │  │  ├─ users                                       │    │    │
│  │  │  ├─ user_tokens                                 │    │    │
│  │  │  ├─ workspaces            (v2.0)                │    │    │
│  │  │  ├─ workspace_members     (v2.0)                │    │    │
│  │  │  ├─ page_history          (v2.0)                │    │    │
│  │  │  ├─ comments              (v2.0)                │    │    │
│  │  │  ├─ favorites             (v2.0)                │    │    │
│  │  │  └─ database_templates    (v2.0)                │    │    │
│  │  └─────────────────────────────────────────────────┘    │    │
│  └─────────────────────────────────────────────────────────┘    │
└──────────────────────────┬───────────────────────────────────────┘
                           │ HTTPS
┌──────────────────────────▼───────────────────────────────────────┐
│                  GITEA API REST v1                                │
│  git.dracodev.net                                                │
│  ├─ /api/v1/user/repos        — Liste projets                    │
│  ├─ /api/v1/repos/{o}/{r}/issues — Issues                        │
│  ├─ /api/v1/repos/{o}/{r}/labels — Labels                        │
│  ├─ /api/v1/repos/{o}/{r}/milestones — Milestones                │
│  ├─ /api/v1/user              — Infos utilisateur                │
│  └─ /api/v1/orgs/{org}/repos — Repos d'organisation              │
└──────────────────────────────────────────────────────────────────┘

3. Modèle de données

3.1 Tables Core (existantes v1.0)

-- Authentification
CREATE TABLE users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    login TEXT NOT NULL UNIQUE,
    full_name TEXT NOT NULL DEFAULT '',
    email TEXT NOT NULL DEFAULT '',
    avatar_url TEXT NOT NULL DEFAULT '',
    is_admin BOOLEAN NOT NULL DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE user_tokens (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    gitea_user_id INTEGER NOT NULL UNIQUE,
    gitea_token TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Boards (legacy — sera absorbé par collections en v1.3)
CREATE TABLE boards (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    project_owner TEXT NOT NULL,
    project_name TEXT NOT NULL,
    columns_json TEXT NOT NULL DEFAULT '["Backlog","À faire","En cours","Révision","Terminé"]',
    wip_limits_json TEXT DEFAULT '{}',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(project_owner, project_name)
);

CREATE TABLE cards (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    board_id INTEGER NOT NULL REFERENCES boards(id),
    gitea_issue_id INTEGER,
    column_name TEXT NOT NULL DEFAULT 'Backlog',
    position INTEGER NOT NULL DEFAULT 0,
    priority TEXT DEFAULT 'Medium',
    due_date TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE col_mapping (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    board_id INTEGER NOT NULL REFERENCES boards(id),
    column_name TEXT NOT NULL,
    gitea_label TEXT NOT NULL,
    close_issue BOOLEAN NOT NULL DEFAULT 0,
    UNIQUE(board_id, column_name)
);

-- Notes & Checklists
CREATE TABLE notes (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    project_owner TEXT NOT NULL,
    project_name TEXT NOT NULL,
    title TEXT NOT NULL DEFAULT 'Notes',
    content TEXT NOT NULL DEFAULT '',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(project_owner, project_name, title)
);

CREATE TABLE checklists (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    board_id INTEGER NOT NULL,
    gitea_issue_id INTEGER NOT NULL,
    title TEXT NOT NULL DEFAULT 'Checklist',
    position INTEGER NOT NULL DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(board_id, gitea_issue_id, title)
);

CREATE TABLE checklist_items (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    checklist_id INTEGER NOT NULL REFERENCES checklists(id) ON DELETE CASCADE,
    content TEXT NOT NULL DEFAULT '',
    checked BOOLEAN NOT NULL DEFAULT 0,
    position INTEGER NOT NULL DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Propriétés custom (legacy)
CREATE TABLE project_properties (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    project_owner TEXT NOT NULL,
    project_name TEXT NOT NULL,
    name TEXT NOT NULL,
    prop_type TEXT NOT NULL DEFAULT 'select',
    options_json TEXT DEFAULT '[]',
    position INTEGER NOT NULL DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(project_owner, project_name, name)
);

CREATE TABLE property_values (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    property_id INTEGER NOT NULL REFERENCES project_properties(id) ON DELETE CASCADE,
    gitea_issue_id INTEGER NOT NULL,
    value TEXT NOT NULL DEFAULT '',
    UNIQUE(property_id, gitea_issue_id)
);

CREATE TABLE ai_keywords (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    project_owner TEXT NOT NULL,
    project_name TEXT NOT NULL,
    keyword TEXT NOT NULL,
    color TEXT NOT NULL DEFAULT '#787774',
    usage_count INTEGER NOT NULL DEFAULT 1,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(project_owner, project_name, keyword)
);

-- Pages
CREATE TABLE pages (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    workspace TEXT NOT NULL,
    title TEXT NOT NULL DEFAULT 'New page',
    content TEXT NOT NULL DEFAULT '',
    content_format TEXT NOT NULL DEFAULT 'markdown',
    parent_section TEXT DEFAULT 'Private',
    parent_id INTEGER REFERENCES pages(id),
    sort_order INTEGER NOT NULL DEFAULT 0,
    deleted_at TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

3.2 Tables Database Concept (v1.3)

-- Collections = Databases Notion
CREATE TABLE collections (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    workspace_id INTEGER REFERENCES workspaces(id),
    name TEXT NOT NULL,
    description TEXT DEFAULT '',
    icon TEXT DEFAULT '📋',
    schema_json TEXT NOT NULL DEFAULT '[]',
    -- [{name,type,options,position}, ...]
    
    -- Lien Gitea optionnel
    gitea_owner TEXT,
    gitea_repo TEXT,
    
    is_locked BOOLEAN NOT NULL DEFAULT 0,
    is_inline BOOLEAN NOT NULL DEFAULT 0,
    parent_page_id INTEGER REFERENCES collection_pages(id),
    
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    created_by INTEGER REFERENCES users(id),
    UNIQUE(workspace_id, name)
);

-- Pages dans une collection
CREATE TABLE collection_pages (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    collection_id INTEGER NOT NULL REFERENCES collections(id) ON DELETE CASCADE,
    
    title TEXT NOT NULL DEFAULT '',
    icon TEXT DEFAULT '📄',
    position INTEGER NOT NULL DEFAULT 0,
    
    -- Sub-items
    parent_id INTEGER REFERENCES collection_pages(id),
    
    -- Gitea link
    gitea_issue_id INTEGER,
    gitea_issue_number INTEGER,
    
    -- Toutes les valeurs de propriétés en JSON
    property_values_json TEXT NOT NULL DEFAULT '{}',
    
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    created_by INTEGER REFERENCES users(id),
    updated_by INTEGER REFERENCES users(id)
);

CREATE INDEX idx_cp_collection ON collection_pages(collection_id, position);
CREATE INDEX idx_cp_parent ON collection_pages(parent_id);
CREATE INDEX idx_cp_gitea ON collection_pages(gitea_issue_id);

-- Vues d'une collection
CREATE TABLE collection_views (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    collection_id INTEGER NOT NULL REFERENCES collections(id) ON DELETE CASCADE,
    
    name TEXT NOT NULL DEFAULT 'Default View',
    view_type TEXT NOT NULL DEFAULT 'table',
    -- table | board | calendar | timeline | gallery | list
    
    config_json TEXT NOT NULL DEFAULT '{}',
    -- {
    --   group_by: "Status",
    --   filters: [{property,operator,value}, ...],
    --   filter_conjunction: "and" | "or",
    --   sorts: [{property,direction}, ...],
    --   visible_properties: ["Title","Status"],
    --   card_size: "medium",
    --   cover_property: "Files",
    --   date_property: "DueDate",
    --   date_range_property: "EndDate"
    -- }
    
    position INTEGER NOT NULL DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

3.3 Tables Propriétés Avancées (v1.4)

CREATE TABLE collection_properties (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    collection_id INTEGER NOT NULL REFERENCES collections(id) ON DELETE CASCADE,
    
    name TEXT NOT NULL,
    prop_type TEXT NOT NULL DEFAULT 'text',
    -- title, text, number, select, multi_select, status,
    -- date, person, checkbox, url, email, phone,
    -- relation, rollup, formula,
    -- files, unique_id,
    -- created_time, created_by, last_edited_time, last_edited_by
    
    -- Options pour select/multi_select/status
    options_json TEXT DEFAULT '[]',
    -- [{name:"Todo",color:"gray"}, {name:"Done",color:"green"}]
    
    -- Format pour number
    number_format TEXT DEFAULT 'number',
    -- number, percent, dollar, euro, pound, yen
    
    -- Pour relation
    related_collection_id INTEGER REFERENCES collections(id),
    reverse_name TEXT,
    
    -- Pour rollup
    relation_property_id INTEGER REFERENCES collection_properties(id),
    target_property_id INTEGER REFERENCES collection_properties(id),
    rollup_function TEXT,
    -- count, sum, avg, min, max, range, unique
    
    -- Pour formula
    formula_expression TEXT,
    
    position INTEGER NOT NULL DEFAULT 0,
    required BOOLEAN NOT NULL DEFAULT 0,
    visible_in_views BOOLEAN NOT NULL DEFAULT 1,
    
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(collection_id, name)
);

3.4 Tables Multi-Utilisateurs (v2.0)

CREATE TABLE workspaces (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    owner_id INTEGER NOT NULL REFERENCES users(id),
    settings_json TEXT NOT NULL DEFAULT '{}',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE workspace_members (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    workspace_id INTEGER NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
    user_id INTEGER NOT NULL REFERENCES users(id),
    role TEXT NOT NULL DEFAULT 'editor',
    -- admin | editor | commenter | viewer
    joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(workspace_id, user_id)
);

CREATE TABLE page_history (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    page_id INTEGER NOT NULL REFERENCES collection_pages(id) ON DELETE CASCADE,
    user_id INTEGER REFERENCES users(id),
    change_type TEXT NOT NULL,
    -- created | updated | moved | deleted | restored
    snapshot_json TEXT NOT NULL DEFAULT '{}',
    -- Snapshot des valeurs avant modification
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE comments (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    page_id INTEGER NOT NULL REFERENCES collection_pages(id) ON DELETE CASCADE,
    user_id INTEGER NOT NULL REFERENCES users(id),
    body TEXT NOT NULL DEFAULT '',
    parent_id INTEGER REFERENCES comments(id),
    resolved BOOLEAN NOT NULL DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE favorites (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL REFERENCES users(id),
    page_id INTEGER REFERENCES collection_pages(id),
    collection_id INTEGER REFERENCES collections(id),
    -- L'un des deux est NOT NULL
    position INTEGER NOT NULL DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(user_id, page_id, collection_id)
);

CREATE TABLE database_templates (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    workspace_id INTEGER REFERENCES workspaces(id),
    name TEXT NOT NULL,
    description TEXT DEFAULT '',
    schema_json TEXT NOT NULL DEFAULT '[]',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    created_by INTEGER REFERENCES users(id)
);

CREATE TABLE page_templates (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    collection_id INTEGER NOT NULL REFERENCES collections(id) ON DELETE CASCADE,
    name TEXT NOT NULL DEFAULT 'Default',
    property_values_json TEXT NOT NULL DEFAULT '{}',
    content_json TEXT DEFAULT '[]',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

3.5 Diagramme des relations

┌──────────────┐       ┌──────────────────┐       ┌───────────────────┐
│  workspaces  │──1:N──│  workspace_members│──N:1──│      users         │
│              │       │  - role           │       │  - login           │
│  - name      │       │  - joined_at      │       │  - email           │
│  - owner_id  │       └──────────────────┘       │  - avatar_url      │
└──────┬───────┘                                  │  - is_admin        │
       │ 1:N                                      └──────┬────┬────────┘
       ▼                                                 │    │
┌──────────────┐       ┌──────────────────┐             │    │ (author)
│ collections  │──1:N──│ collection_pages  │◄─N:1───────┘    │
│              │       │                  │                   │
│  - name      │       │  - title         │                   │
│  - schema_json│      │  - parent_id ────┼─── self-ref      │
│  - icon      │       │  - gitea_issue_id│                   │
│  - gitea_*   │       │  - property_values│                  │
│  - is_locked │       │    _json         │                   │
└──────┬───────┘       └────────┬─────────┘                   │
       │ 1:N                    │ 1:N                         │
       ▼                        ▼                             │
┌──────────────────┐  ┌──────────────────┐                   │
│collection_views  │  │   comments       │                   │
│  - view_type     │  │  - body          │                   │
│  - config_json   │  │  - resolved      │                   │
└──────────────────┘  │  - parent_id     │                   │
                      └────────┬─────────┘                   │
                               │                              │
┌──────────────────┐          │                              │
│collection_properties│       │                              │
│  - name          │          │                              │
│  - prop_type     │◄─────────┘                              │
│  - options_json  │                                         │
│  - related_collection_id ──┐                               │
│  - relation_property_id    │ self-ref                      │
│  - target_property_id      │                               │
│  - formula_expression      │                               │
└────────────────────────────┘                               │
                                                             │
┌──────────────────┐  ┌──────────────────┐                  │
│  page_history    │  │   favorites      │                  │
│  - change_type   │  │  - position      │                  │
│  - snapshot_json │  └──────────────────┘                  │
│  - user_id ──────┼────────────────────────────────────────┘
└──────────────────┘

4. Routage & API

4.1 Routes existantes (v1.2)

GET  /                                          → Dashboard
GET  /board/{owner}/{repo}                      → Board Kanban
GET  /board/{owner}/{repo}/view/{view}          → Fragment vue (kanban|table|status|teamload|detailed)
GET  /board/{owner}/{repo}?view=&status=&filter=&sort=  → Avec paramètres

GET  /board/api/properties/{o}/{r}              → Liste propriétés
POST /board/api/properties/{o}/{r}              → Créer propriété
DEL  /board/api/properties/{o}/{r}              → Supprimer propriété
POST /board/api/properties/{o}/{r}/values       → Set valeur propriété
GET  /board/api/ai-keywords/{o}/{r}             → Liste keywords
POST /board/api/ai-keywords/{o}/{r}/extract     → Extraire keywords
POST /board/api/sync/{o}/{r}                    → Sync complète

GET  /api/health                                → Health check
GET  /api/stats                                 → Statistiques
GET  /api/projects                              → Projets JSON
POST /api/move                                  → Déplacer carte
GET  /api/issues/{o}/{r}/{id}                   → Issue detail (JSON)
GET  /api/issues/{o}/{r}/{id}?format=html       → Issue detail (HTML)
POST /api/issues/{o}/{r}                        → Créer issue
PATCH /api/issues/{o}/{r}/{id}                  → Update issue

POST /api/checklists/{o}/{r}/{id}               → Créer checklist
POST /api/checklist-items/{o}/{r}/{id}/{cl_id}  → Ajouter item
PATCH /api/checklist-items/{id}                 → Toggle item

GET  /notes/{o}/{r}                             → Lire notes
POST /notes/{o}/{r}                             → Sauvegarder notes

GET  /auth/login                                → Login OAuth2 Gitea
GET  /auth/callback                             → Callback OAuth2
GET  /auth/logout                               → Déconnexion

POST /webhooks/gitea                           → Webhook Gitea

GET  /pages/{page_id}                           → Voir page
POST /board/api/pages                          → Créer page
GET  /board/api/pages/{id}                     → Get page JSON
PUT  /board/api/pages/{id}                     → Update page
PUT  /board/api/pages/{id}/move                → Déplacer page (drag & drop)
POST /board/api/pages/{id}/blocks              → Save block editor
DEL  /board/api/pages/{id}                     → Soft delete

GET  /board/trash                              → Page corbeille
GET  /board/api/trash                          → Liste trash
POST /board/api/trash/{id}/restore             → Restaurer
DEL  /board/api/trash/{id}                     → Suppression définitive

GET  /library                                   → Bibliothèque
GET  /accounts                                  → Gestion comptes

4.2 Routes Database (v1.3-v1.5)

GET    /db                                          → Liste collections du workspace
POST   /db                                          → Créer collection
GET    /db/{collection_id}                          → Vue par défaut de la collection
GET    /db/{collection_id}/view/{view_id}           → Vue spécifique
GET    /db/{collection_id}/fragment/{view_type}     → Fragment HTMX

POST   /db/{collection_id}/pages                   → Créer page
GET    /db/{collection_id}/pages/{page_id}          → Détail page
PUT    /db/{collection_id}/pages/{page_id}          → Update page
DELETE /db/{collection_id}/pages/{page_id}          → Soft delete page

GET    /db/{collection_id}/properties               → Liste propriétés
POST   /db/{collection_id}/properties               → Créer propriété
PUT    /db/{collection_id}/properties/{prop_id}     → Update propriété
DELETE /db/{collection_id}/properties/{prop_id}     → Supprimer propriété

GET    /db/{collection_id}/views                    → Liste vues
POST   /db/{collection_id}/views                    → Créer vue
PUT    /db/{collection_id}/views/{view_id}          → Update config vue
DELETE /db/{collection_id}/views/{view_id}          → Supprimer vue

POST   /db/{collection_id}/views/duplicate/{id}     → Dupliquer vue
POST   /db/{collection_id}/views/save-as            → Sauvegarder état courant

GET    /db/{collection_id}/templates                → Page templates
POST   /db/{collection_id}/templates                → Créer template
POST   /db/{collection_id}/templates/{tid}/apply    → Appliquer template

POST   /db/{collection_id}/import/csv               → Import CSV
GET    /db/{collection_id}/export/csv               → Export CSV

4.3 Routes Sub-items & Dépendances (v1.8)

GET    /db/{collection_id}/pages/{page_id}/sub-items     → Liste sub-items
POST   /db/{collection_id}/pages/{page_id}/sub-items     → Créer sub-item

GET    /db/{collection_id}/pages/{page_id}/dependencies  → Bloque/Bloqué par
POST   /db/{collection_id}/pages/{page_id}/dependencies  → Ajouter dépendance
DELETE /db/{collection_id}/pages/{page_id}/dependencies/{dep_id} → Supprimer

POST   /db/{collection_id}/pages/{page_id}/check-deps   → Vérifier contraintes

4.4 Routes My Tasks (v1.9)

GET    /my-tasks                                  → Dashboard unifié
GET    /my-tasks?view=calendar                    → Vue calendrier
GET    /my-tasks?view=overdue                     → Tâches en retard
GET    /my-tasks?view=upcoming&days=7             → 7 prochains jours
GET    /my-tasks?filter=collection:3              → Filtrer par collection

4.5 Routes Multi-Utilisateurs (v2.0)

GET    /workspace/{ws_id}                         → Workspace
POST   /workspace                                  → Créer workspace
PUT    /workspace/{ws_id}/settings                 → Settings
POST   /workspace/{ws_id}/members                  → Inviter membre
PUT    /workspace/{ws_id}/members/{user_id}/role   → Changer rôle
DELETE /workspace/{ws_id}/members/{user_id}        → Retirer membre

GET    /db/{collection_id}/pages/{page_id}/history → Historique
GET    /db/{collection_id}/pages/{page_id}/comments→ Commentaires
POST   /db/{collection_id}/pages/{page_id}/comments→ Ajouter commentaire

GET    /favorites                                   → Liste favoris
POST   /favorites                                   → Ajouter favori
DELETE /favorites/{id}                              → Retirer favori

4.6 Routes Sidebar & Library (v4.0)

GET    /library                                     → Bibliothèque dynamique
GET    /api/library/recents                         → Pages récentes
GET    /api/library/favorites                       → Favoris
GET    /api/library/shared                          → Pages partagées
GET    /api/library/published                       → Pages publiées
GET    /api/library/private                         → Pages privées
GET    /api/library/workspace                       → Pages du workspace

GET    /api/local-workspace/items                   → Arborescence workspace
POST   /api/local-workspace/items                   → Créer page/dossier
PUT    /api/local-workspace/items/{id}              → Renommer
DELETE /api/local-workspace/items/{id}              → Soft delete
POST   /api/local-workspace/upload                  → Upload fichier
GET    /api/files/{ws_id}/{filename}                → Servir fichier uploadé

GET    /help                                        → Page d'aide complète
GET    /api/public/pages/{slug}                     → Page publique

4.7 Routes Share & Publish (v4.0)

POST   /api/pages/{id}/share                        → Partager une page
DELETE /api/pages/{id}/share/{share_id}             → Retirer partage
GET    /api/pages/{id}/shares                       → Liste partages
POST   /api/pages/{id}/publish                      → Publier page
DELETE /api/pages/{id}/publish                      → Dépublier

5. Système d'authentification (v4.0)

FlowDeck supporte 3 méthodes d'authentification et la connexion d'intégrations multiples à un même compte local.

5.1 Types de comptes

Type Auth Workspaces Intégrations
Local pur email + password Locaux uniquement Aucune
Local + intégrations email + password Locaux + distants Gitea et/ou GitHub
OAuth pur Gitea ou GitHub Distants uniquement Non-déconnectable

5.2 Comportement du sidebar selon le type de compte

LOCAL PUR:
  ├─ 📁 [nom workspace local]
  ├─ 📅 Meetings
  ├─ 🕒 Recents
  ├─ ⭐ Favorites
  ├─ 🤖 Agents
  ├─ 👥 Shared        ← visible si pages partagées
  ├─ 🌐 Published     ← visible si pages publiées
  ├─ (Private caché)
  └─ 📚 Library / ✅ My Tasks / 🗑️ Trash / ❓ Help

LOCAL + GITEA/GITHUB:
  ├─ 🔗 [owner/repo]  ← workspace distant actif
  ├─ 📅 Meetings
  ├─ 🕒 Recents
  ├─ ⭐ Favorites
  ├─ 🤖 Agents
  ├─ 👥 Shared
  ├─ 🌐 Published
  ├─ 🔒 Private        ← visible (fichiers locaux liés au projet distant)
  └─ 📚 / ✅ / 🗑️ / ❓

OAUTH PUR (GITEA/GITHUB):
  ├─ 🔗 [owner/repo]  ← identité = compte Gitea/GitHub
  ├─ 🕒 Recents
  ├─ ⭐ Favorites
  ├─ 👥 Shared
  ├─ 🌐 Published
  ├─ (Private caché)
  └─ 📚 / ✅ / ❓ (Trash caché — pas de workspace local)

5.3 Règles d'affichage

  • Section Private : visible UNIQUEMENT si compte local + workspace distant actif
  • Trash : visible si auth_method == 'local' ou si des pages locales existent
  • Boutons New File/New Folder : cachés si has_active_workspace == False
  • Message "No workspace open" : affiché dans la section Workspace si aucun workspace actif
  • Header sidebar : toujours le compte local si auth_method=local, même avec intégrations
  • "" → aucun workspace actif
  • "42" → workspace local ID 42
  • "gitea:owner:repo" → workspace distant Gitea
  • Nettoyage automatique si cookie invalide (workspace supprimé ou autre user)

6. Frontend

5.1 Layout

┌──────────────────────────────────────────────────────────────┐
│ TOPBAR (unified-header)                                      │
│ ┌──────┬───────────────────────────────────────┬───────────┐ │
│ │ «»   │ Breadcrumbs: 🏠 Workspaces           │ Share ⚙    │ │
│ └──────┴───────────────────────────────────────┴───────────┘ │
├────────────┬─────────────────────────────────────────────────┤
│ SIDEBAR    │ CONTENU PRINCIPAL                               │
│ (240px)    │                                                 │
│            │  ┌─ Help Page ────────────────────────────────┐│
│ ┌────────┐ │  │ ┌──────────────┐ ┌──────────────┐          ││
│ │ Header │ │  │ │ 🚀 Getting   │ │ 📝 Pages     │  (...)   ││
│ │ [D]    │ │  │ │   Started    │ │   & Editor   │          ││
│ │ Draco  │ │  │ └──────────────┘ └──────────────┘          ││
│ └────────┘ │  └─────────────────────────────────────────────┘│
│            │                                                 │
│ 🏠 💬 📨 🔍│  ┌─ Library ─────────────────────────────────┐│
│            │  │ [Recents|Favorites|Shared|Published|...]    ││
│ 📁 Worksp. │  │ ┌──────┬──────────┬──────────┬───────────┐ ││
│  📄 New Pg │  │ │Name  │Created by│Source    │Last edited│ ││
│  📁 New Fl │  │ ├──────┼──────────┼──────────┼───────────┤ ││
│  📄 page 1 │  │ │doc A │Draco     │Local     │10 min ago │ ││
│  📄 page 2 │  │ │doc B │bruno     │Gitea     │2 hours ago│ ││
│            │  │ └──────┴──────────┴──────────┴───────────┘ ││
│ 📅 Meeting │  └─────────────────────────────────────────────┘│
│ 🕒 Recents │                                                 │
│ ⭐ Favoris │  ┌─ Workspaces ───────────────────────────────┐│
│ 🤖 Agents  │  │ ┌──────────┐ ┌──────────┐ ┌───┐ ┌───┐     ││
│ 👥 Shared  │  │ │Project A │ │Project B │ │ + │ │ + │     ││
│ 🌐 Publ.   │  │ │3 pages   │ │12 pages  │ │   │ │   │     ││
│            │  │ └──────────┘ └──────────┘ └───┘ └───┘     ││
│ ══════════ │  └─────────────────────────────────────────────┘│
│ 📚 Library │                                                 │
│ ✅ My Task │  ┌─ Settings ─────────────────────────────────┐│
│ 🗑️ Trash  │  │ My Account | Admin | Integrations            ││
│ ❓ Help    │  │ [Upload photo] [Full Name] [Email] [...]    ││
│            │  └─────────────────────────────────────────────┘│
│ 💬 [+📄]   │                                                 │
└────────────┴─────────────────────────────────────────────────┘

5.2 Vues supportées

Vue Icône Template Description
Table 📊 table_view.html Colonnes = propriétés, lignes = pages
Board (Kanban) 📋 board_fragment.html Groupé par propriété Select
Calendar 📅 calendar_view.html Groupé par propriété Date
Timeline (Gantt) 📈 timeline_view.html Barres horizontales sur axe Date
Gallery 🖼️ gallery_view.html Cartes visuelles avec cover
List 📝 list_view.html Compact, 1 colonne + preview

5.3 Composants réutilisables

  • Property editor — éditeur inline par type (select dropdown, date picker, text input...)
  • Filter builder — UI de construction de filtres (propriété + opérateur + valeur)
  • Sort builder — UI de tri multi-critères
  • Group selector — dropdown pour choisir la propriété de groupement
  • View tabs — onglets pour switcher entre vues
  • Page modal — détail d'une page avec propriétés, checklists, commentaires
  • Slash menu — menu contextuel pour l'éditeur de blocs

6. Système d'authentification

6.1 Flux OAuth2 Gitea

User ──→ GET /auth/login ──→ Redirect Gitea OAuth
                                    │
                              Gitea authorize page
                                    │
User authorize ──→ GET /auth/callback?code=xxx
                                    │
                              POST /api/v1/login/oauth/access_token
                                    │
                              ┌─────▼──────┐
                              │ access_token│
                              └─────┬──────┘
                                    │
                              GET /api/v1/user
                                    │
                              ┌─────▼──────────────────┐
                              │ Create/Update:          │
                              │  - users (login, email) │
                              │  - user_tokens (token)  │
                              │  - session cookie       │
                              └─────────────────────────┘

6.2 Multi-utilisateurs (v2.0)

┌──────────────────────────────────────────────────────────┐
│                    PERMISSION MODEL                        │
│                                                           │
│  Workspace                                                │
│  ├─ Owner (créateur)      — tout peut faire              │
│  ├─ Admin                 — gérer membres, config        │
│  ├─ Editor                — CRUD pages, propriétés       │
│  ├─ Commenter             — commentaires seulement       │
│  └─ Viewer                — lecture seule                │
│                                                           │
│  Collection (hérite du workspace par défaut)              │
│  ├─ Peut être overridé par collection                    │
│  └─ Database lock → lecture seule pour tout le monde     │
│                                                           │
│  Page                                                     │
│  ├─ Propriétaire = created_by                            │
│  ├─ Mention @user → notifie l'utilisateur                │
│  └─ Assignee (property type Person)                      │
└──────────────────────────────────────────────────────────┘

6.5 Modèle de comptes & intégrations (v4.0)

6.5.1 Types de comptes

FlowDeck supporte 3 types de comptes, déterminés par auth_method dans la table users :

┌──────────────┬──────────────────┬─────────────────────────────────────┐
│ auth_method  │ Login via        │ Identité dans le sidebar            │
├──────────────┼──────────────────┼─────────────────────────────────────┤
│ local        │ email + password │ Nom local (ex: "Draco")            │
│ gitea        │ OAuth2 Gitea     │ Nom Gitea + badge 🦎 Gitea         │
│ github       │ OAuth2 GitHub    │ Nom GitHub + badge 🐙 GitHub       │
└──────────────┴──────────────────┴─────────────────────────────────────┘

Règle fondamentale : Un compte local avec intégration(s) reste un compte LOCAL. Le badge OAuth (🦎/🐙) n'apparaît QUE pour les comptes dont auth_method est le provider.

Compte LOCAL avec intégrations :
┌──────────────────────────────────┐
│ [D] Draco                        │  ← Nom local, PAS de badge
│     [email protected]      │
└──────────────────────────────────┘

Compte GITEA (auth_method=gitea) :
┌──────────────────────────────────┐
│ [B] bruno            [🦎 Gitea]  │  ← Badge visible
│     [email protected]           │
└──────────────────────────────────┘

6.5.2 Matrice des intégrations

Compte principal    Peut ajouter      Déconnexion possible
─────────────────────────────────────────────────────────
local               Gitea             ✅ Oui
local               GitHub            ✅ Oui
local               Gitea + GitHub    ✅ Les deux
─────────────────────────────────────────────────────────
gitea               GitHub            ✅ GitHub      ❌ Gitea
github              Gitea             ✅ Gitea       ❌ GitHub

Settings → Integrations : Affiche toutes les intégrations avec bouton Disconnect. Le provider principal (auth_method) n'a PAS de bouton Disconnect.

6.5.3 Scénarios de workspace

SCÉNARIO A : Compte local pur
─────────────────────────────
· Login email+password
· Workspaces locaux uniquement
· Section Private : CACHÉE (tout est déjà local)
· Trash : ✅ (restauration locale)

SCÉNARIO B : Compte local + intégration(s)
──────────────────────────────────────────
· Login email+password
· Workspaces : locaux + Gitea + GitHub
· Section Private : VISIBLE si workspace distant actif
· Private : stockage DB locale, associé user+repo
· Trash : ✅ (local uniquement)

SCÉNARIO C : Compte OAuth pur (gitea ou github)
────────────────────────────────────────────────
· Login via OAuth
· Workspaces : distants du provider principal
· Section Private : VISIBLE (toujours)
· Trash : ❌ (pas de contenu local)
· Intégration secondaire : possible
· Déconnexion provider principal : impossible

6.5.4 Sidebar — règles d'affichage par section

Section        Scénario A      Scénario B              Scénario C
─────────────  ──────────────  ──────────────────────  ──────────────
Recents        ✅ (local)      ✅ (tout)                ✅ (tout)
Favorites      ✅ (local)      ✅ (tout)                ✅ (tout)
Shared         ✅ (local)      ✅ (tout)                ✅ (tout)
Published      ✅ (local)      ✅ (tout)                ✅ (tout)
Agents         ✅              ✅                       ✅
Private        ❌ CACHÉ        ✅ si ws distant actif   ✅
Workspace      ✅              ✅                       ✅
Trash          ✅ (local)      ✅ (local)               ❌

Chaque section a un bouton → Library qui ouvre :

/library?tab=recents|favorites|shared|published|private|workspace

6.5.5 Système Share & Publish

Le bouton [Share] dans la topbar d'une page ouvre une modale à 2 tabs :

┌──────────────────────────────────────────────────────┐
│  [ Share ]  [ Publish ]                              │
├──────────────────────────────────────────────────────┤
│ Share tab :                                          │
│  · Invite people : [email] [Invite]                  │
│  · General access : [🔒 Restricted] Copy link        │
│  · Liste des personnes/groupes avec accès            │
│  → Document listé dans sidebar "Shared"             │
├──────────────────────────────────────────────────────┤
│ Publish tab :                                        │
│  · [Publish to web] → URL publique générée           │
│  · URL : https://flowdeck.draco.dev/p/<slug>        │
│  · [Unpublish]                                       │
│  → Document listé dans sidebar "Published"          │
└──────────────────────────────────────────────────────┘

6.5.6 Stockage des fichiers Private

Quand un workspace distant est actif, les fichiers créés dans la section Private sont stockés dans la base de données locale (table pages avec parent_section='Private' et workspace=gitea:<owner>/<repo>).

Cela permet de :

  • Prendre des notes personnelles liées à un projet sans les committer
  • Garder des brouillons avant de les pousser
  • Avoir un espace de travail privé même sur un repo partagé

6.5.7 Trash — règles

Contenu                  Trash ?   Comportement
───────────────────────  ────────  ──────────────────────────
Workspace local          ✅        Supprimé → restaurable
Dossier/fichier local    ✅        Supprimé → restaurable
Fichier Gitea/GitHub     ❌        Commit "delete" → pas trash
Page distante            ❌        Commit "delete" → pas trash

6.5.8 Édition de fichiers — UI unifiée

Fichier LOCAL                    Fichier DISTANT (Gitea/GitHub)
┌─────────────────────────┐      ┌─────────────────────────────┐
│ Même UI d'édition       │      │ Même UI d'édition           │
│ Même rendu visuel       │      │ Même rendu visuel           │
│ Sauvegarde auto DB      │      │ + Bouton [Commit]           │
│ Pas de commit           │      │ + Champ message commit      │
└─────────────────────────┘      └─────────────────────────────┘

6.5.9 Page Library — /library?tab=<tab>

┌──────────────────────────────────────────────────────────────┐
│  Library                                                     │
│  [Recents] [Favorites] [Shared] [Published]                   │
│  [Private] [Workspace]                                        │
├──────────────────────────────────────────────────────────────┤
│  Filtres : [All] [Local] [Gitea] [GitHub]                    │
│                                                              │
│  ┌─────────────────────────────────────────────────────────┐ │
│  │ 📄 Document X    Local  · modifié il y a 2h             │ │
│  │ 📄 README.md     Gitea  · bruno/flowdeck                │ │
│  │ 📄 Notes         Local  · Workspace Projet A            │ │
│  └─────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘

6.5.10 Schéma DB — mises à jour pour v4.0

-- users : ajout auth_method
ALTER TABLE users ADD COLUMN auth_method TEXT NOT NULL DEFAULT 'local';
-- 'local' | 'gitea' | 'github'

-- users : colonnes OAuth existantes
-- gitea_id INTEGER (NULL si pas Gitea)
-- github_id INTEGER (NULL si pas GitHub)
-- gitea_token TEXT (token intégration, NULL si pas connecté)
-- github_token TEXT (token intégration, NULL si pas connecté)

-- pages : ajout published
ALTER TABLE pages ADD COLUMN is_published BOOLEAN NOT NULL DEFAULT 0;
ALTER TABLE pages ADD COLUMN publish_slug TEXT;
ALTER TABLE pages ADD COLUMN is_shared BOOLEAN NOT NULL DEFAULT 0;
ALTER TABLE pages ADD COLUMN share_mode TEXT DEFAULT 'restricted';
-- 'restricted' | 'link' | 'public'

-- page_shares : qui a accès à quel document
CREATE TABLE page_shares (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    page_id INTEGER NOT NULL REFERENCES pages(id) ON DELETE CASCADE,
    shared_with_user_id INTEGER REFERENCES users(id),
    shared_with_email TEXT,
    permission TEXT NOT NULL DEFAULT 'view',
    -- 'view' | 'comment' | 'edit'
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    created_by INTEGER REFERENCES users(id)
);

-- recents : historique de consultation
CREATE TABLE recents (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL REFERENCES users(id),
    page_id INTEGER NOT NULL REFERENCES pages(id),
    workspace TEXT NOT NULL,
    source_type TEXT NOT NULL DEFAULT 'local',
    -- 'local' | 'gitea' | 'github'
    accessed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(user_id, page_id)
);

6.5.11 Plan de migration

Phase 1 — DB & Modèles
  1. Ajouter auth_method aux users existants (auto-détecter)
  2. Ajouter is_published, is_shared, share_mode aux pages
  3. Créer table page_shares
  4. Créer table recents

Phase 2 — Sidebar
  1. Badge OAuth (🦎/🐙) basé sur auth_method
  2. Afficher/cacher Private selon type de compte + workspace actif
  3. Boutons Library par section
  4. Trash filtré local-only

Phase 3 — Share & Publish
  1. Modale Share à 2 tabs fonctionnelle
  2. Share via email/user → table page_shares
  3. Share via lien → share_mode='link'
  4. Publish → is_published + publish_slug
  5. Page publique servable à /p/<slug>

Phase 4 — Library
  1. Page /library avec tabs
  2. Filtrage par source (local/gitea/github)
  3. Recents alimentés automatiquement

Phase 5 — Intégrations
  1. GitHub OAuth déjà fait (providers.py)
  2. Matrice de déconnexion respectée
  3. Compte local peut connecter/déconnecter Gitea+GitHub

7. Intégration Gitea

7.1 Flux de données

Création Collection         Sync bidirectionnelle
──────────────────         ──────────────────────

1. Collection vide         5. POST /board/api/sync/{o}/{r}
   (pas de lien Gitea)        │
                              ├─ GET issues Gitea
2. Collection liée            ├─ Map issues → collection_pages
   (gitea_owner + gitea_repo) │  (via _issue_column)
                              ├─ Extract AI keywords
3. Pull initial              └─ Upsert cards in DB
   GET /api/v1/repos/{o}/{r}/issues
                             6. Webhook Gitea
4. Mapping colonnes           POST /webhooks/gitea
   col_mapping:                │
   column_name → gitea_label  ├─ issue.opened → create page
                              ├─ issue.closed → archive page
                              └─ issue.labeled → move column

7.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),
            ...
        }

8. Système de propriétés

8.1 Architecture

┌─────────────────────────────────────────────────────────────┐
│                  PROPERTY SYSTEM                             │
│                                                              │
│  collection_properties (schéma)     property_values_json    │
│  ┌──────────────────────────┐      ┌──────────────────────┐ │
│  │ id: 1                    │      │ Dans collection_pages │ │
│  │ name: "Status"           │      │ {                    │ │
│  │ prop_type: "status"      │      │   "1": "Done",       │ │
│  │ options_json: [          │      │   "2": "P1",         │ │
│  │   {name:"Todo",          │      │   "3": "2026-07-15", │ │
│  │    color:"gray"},        │      │   "4": ["bruno"],    │ │
│  │   {name:"Done",          │      │   "5": ["page_42"],  │ │
│  │    color:"green"}        │      │ }                    │ │
│  │ ]                        │      └──────────────────────┘ │
│  └──────────────────────────┘                               │
│                                                              │
│  Avantages du JSON:                                          │
│  - Pas de migration de table pour ajouter une propriété     │
│  - Requêtes via json_extract() en SQLite                     │
│  - Facilement requêtable avec index si nécessaire            │
└─────────────────────────────────────────────────────────────┘

8.2 Types de propriétés

Type Stockage Exemple
title String "Ma tâche"
text String "Description longue..."
number Float 42 / 12.5
select String "En cours"
multi_select JSON array ["Frontend","Backend"]
status String + color "Done" (green)
date ISO 8601 string "2026-07-15"
person JSON array [{"id":1,"login":"bruno"}]
checkbox Boolean true
url String "https://..."
email String "[email protected]"
phone String "+1..."
relation JSON array IDs [42, 57]
rollup Computé 3 (COUNT)
formula Computé "Résultat"
files JSON array URLs [{"url":"...","name":"img.png"}]
unique_id Integer 42
created_time Auto "2026-07-10T..."
created_by Auto {"id":1,"login":"bruno"}
last_edited_time Auto "2026-07-10T..."
last_edited_by Auto {"id":1,"login":"bruno"}

8.3 Formula Engine

# Moteur d'expressions JavaScript-like
FUNCTIONS = {
    "prop":     lambda ctx, name: ctx["values"].get(name),
    "now":      lambda ctx: datetime.now().isoformat(),
    "today":    lambda ctx: date.today().isoformat(),
    "if":       lambda ctx, cond, a, b: a if cond else b,
    "concat":   lambda ctx, *args: "".join(str(a) for a in args),
    "round":    lambda ctx, n, d=0: round(n, d),
    "contains": lambda ctx, s, sub: sub in str(s),
    "length":   lambda ctx, s: len(str(s)),
    "toNumber": lambda ctx, s: float(s) if s else 0,
    "formatDate": lambda ctx, d, fmt: format_date(d, fmt),
    "dateAdd":  lambda ctx, d, n, unit: add_to_date(d, n, unit),
    "dateSubtract": lambda ctx, d, n, unit: subtract_from_date(d, n, unit),
    "replace":  lambda ctx, s, old, new: str(s).replace(old, new),
    "replaceAll": lambda ctx, s, old, new: str(s).replace(old, new),  # all by default
    "join":     lambda ctx, sep, *args: sep.join(str(a) for a in args),
    "empty":    lambda ctx, v: v is None or v == "" or v == [],
    "and":      lambda ctx, *args: all(args),
    "or":       lambda ctx, *args: any(args),
    "not":      lambda ctx, v: not v,
}

8.4 Rollup Engine

ROLLUP_FUNCTIONS = {
    "count":         lambda values: len(values),
    "count_values":  lambda values: len([v for v in values if v]),
    "empty":         lambda values: len([v for v in values if not v]),
    "not_empty":     lambda values: len([v for v in values if v]),
    "sum":           lambda values: sum(float(v) for v in values if v),
    "average":       lambda values: sum(vals:=...) / len(vals) if vals else 0,
    "median":        lambda values: statistics.median(vals),
    "min":           lambda values: min(vals),
    "max":           lambda values: max(vals),
    "range":         lambda values: max(vals) - min(vals),
    "unique":        lambda values: list(set(values)),
}

9. Système de vues

9.1 Config JSON d'une vue

{
  "group_by": "Status",
  "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"
}

9.2 Chaîne de rendu

Requête: GET /db/3/view/7
         │
         ▼
    1. Charger collection_views.id=7
       → view_type="board", config_json={...}
         │
         ▼
    2. Charger collection_pages WHERE collection_id=3
       → [page1, page2, page3, ...]
         │
         ▼
    3. Appliquer config_json.filters
       → pages filtrées
         │
         ▼
    4. Appliquer config_json.sorts
       → pages triées
         │
         ▼
    5. Grouper par config_json.group_by
       → { "Todo": [p1, p2], "Doing": [p3], "Done": [p4] }
         │
         ▼
    6. Ne garder que config_json.visible_properties
       → Alléger le payload
         │
         ▼
    7. Render template correspondant à view_type
       → board_fragment.html
         │
         ▼
    8. Retourner HTML fragment (HTMX)

10. Sub-items & Dépendances

10.1 Sub-items

┌──────────────────────────────────────┐
│  collection_pages (même collection)  │
│                                      │
│  id │ title          │ parent_id     │
│  ───┼────────────────┼───────────────│
│  1  │ Refonte UI     │ NULL          │  ← Parent
│  2  │ Header         │ 1             │  ← Sub-item de 1
│  3  │ Dark mode      │ 1             │  ← Sub-item de 1
│  4  │ Tests header   │ 2             │  ← Sub-sub-item
│                                      │
│  Activation:                         │
│  1. enable_sub_items(collection_id)  │
│  2. Crée 2 propriétés relation auto: │
│     - "Parent item" → même collection│
│     - "Sub-item" (inverse)           │
│  3. Board affiche sub-items indentés │
└──────────────────────────────────────┘

10.2 Dépendances

┌───────────────────────────────────────────┐
│  Tâche A ──blocks──► Tâche B              │
│  Tâche B ──blocked_by──► Tâche A          │
│                                            │
│  Contraintes:                              │
│  - B ne peut pas être "Done" si A ≠ "Done"│
│  - Timeline affiche les flèches            │
│                                            │
│  check_dependency_constraint():            │
│    if new_status == "Done":                │
│      blocked = get_blocked_pages(page_id)  │
│      if any(not done for blocked):         │
│        raise DependencyError(...)          │
└───────────────────────────────────────────┘

11. My Tasks & Dashboard unifié

┌──────────────────────────────────────────────────────┐
│                    MY TASKS                           │
│                                                       │
│  ┌──────────────────────────────────────────────┐    │
│  │ 📋 Projet Alpha                     [3 tâches]│    │
│  │ ├─ Design maquette    🟡 En cours  15 juil   │    │
│  │ ├─ API endpoint       🔴 Bloqué    20 juil   │    │
│  │ └─ Tests UI           ⚪ Backlog    —         │    │
│  │                                               │    │
│  │ 📋 Daily Tasks                       [2]      │    │
│  │ ├─ Réunion standup    🟢 Done      10 juil   │    │
│  │ └─ Revue de code      🟡 En cours  12 juil   │    │
│  └──────────────────────────────────────────────┘    │
│                                                       │
│  Algorithme:                                          │
│  1. Scanner toutes les collections du workspace      │
│  2. Filtrer pages où:                                 │
│     - Une propriété "Person" = current_user          │
│     - Status ≠ "Done" / "Archived"                   │
│  3. Grouper par collection                           │
│  4. Trier par Due Date ASC                           │
└──────────────────────────────────────────────────────┘

12. Éditeur de blocs

12.1 Types de blocs

┌──────────────────────────────────────────────────┐
│  BLOCK TYPES                                      │
│                                                   │
│  Texte:                                           │
│  ├─ text (paragraph)                              │
│  ├─ heading_1, heading_2, heading_3, heading_4   │
│  ├─ bulleted_list, numbered_list                 │
│  ├─ to_do (checkbox)                              │
│  ├─ toggle (repliable)                            │
│  └─ quote                                         │
│                                                   │
│  Média:                                           │
│  ├─ image (resizable, alignable)                  │
│  ├─ video (embed)                                 │
│  ├─ file (attachment)                             │
│  ├─ bookmark (link preview)                       │
│  └─ code (syntax highlighting)                    │
│                                                   │
│  Layout:                                          │
│  ├─ divider                                       │
│  ├─ callout (icône + fond coloré)                │
│  └─ column (2, 3, 4, 5 colonnes)                 │
│                                                   │
│  Data:                                            │
│  ├─ inline_database (collection embarquée)       │
│  └─ linked_database (vue d'une collection)        │
└──────────────────────────────────────────────────┘

12.2 Format de stockage

{
  "blocks": [
    {
      "id": "b1",
      "type": "heading_2",
      "content": "Architecture",
      "children": []
    },
    {
      "id": "b2",
      "type": "text",
      "content": "FlowDeck utilise FastAPI...",
      "children": []
    },
    {
      "id": "b3",
      "type": "toggle",
      "content": "Détails techniques",
      "children": [
        {
          "id": "b3a",
          "type": "bulleted_list",
          "content": "SQLite WAL mode",
          "children": []
        },
        {
          "id": "b3b",
          "type": "bulleted_list",
          "content": "HTMX + Alpine.js",
          "children": []
        }
      ]
    },
    {
      "id": "b4",
      "type": "code",
      "content": "print('Hello')",
      "language": "python",
      "children": []
    },
    {
      "id": "b5",
      "type": "callout",
      "content": "Note importante",
      "icon": "💡",
      "color": "blue",
      "children": []
    },
    {
      "id": "b6",
      "type": "divider",
      "content": "",
      "children": []
    }
  ]
}

13. Multi-utilisateurs & Collaboratif

13.1 Modèle de workspace

┌─────────────────────────────────────────────────────────┐
│  WORKSPACE "Foxy Dev Team"                              │
│                                                          │
│  Members:                                                │
│  ├─ bruno (owner)     — tout                            │
│  ├─ marie (admin)     — gérer membres, config           │
│  ├─ paul (editor)     — CRUD pages                      │
│  └─ julie (viewer)    — lecture seule                   │
│                                                          │
│  Collections:                                            │
│  ├─ Tasks ─┬─ permissions: workspace                    │
│  │         └─ visibilité: tous les membres              │
│  ├─ Roadmap─ permissions: admin+editor                  │
│  │         └─ visibilité: bruno, marie, paul            │
│  └─ HR ──── permissions: owner only                     │
│            └─ visibilité: bruno uniquement              │
│                                                          │
│  Collaboration features:                                 │
│  ├─ Présence en temps réel (qui est en ligne)           │
│  ├─ Verrouillage optimiste (dernier qui sauve gagne)    │
│  ├─ Historique des modifications par page               │
│  ├─ Commentaires avec @mentions                         │
│  └─ Notifications (changement de statut, assignation)   │
└─────────────────────────────────────────────────────────┘

13.2 Gestion des conflits

Stratégie: "Last write wins" avec historique

1. Alice ouvre la page → charge version v=42
2. Bob ouvre la page  → charge version v=42
3. Alice sauvegarde   → v=43 (succès)
4. Bob sauvegarde     → v=43 (succès, écrase Alice)
   → L'historique conserve le snapshot v=42→v=43 d'Alice

Alternative future: Operational Transformation (OT)
ou CRDT pour du vrai temps réel (complexité ++)

14. Déploiement

14.1 Docker Compose (production)

services:
  flowdeck:
    build: .
    container_name: flowdeck
    ports:
      - "8080:8080"
    environment:
      - GITEA_URL=https://git.dracodev.net
      - GITEA_TOKEN=${GITEA_TOKEN}
      - GITEA_OAUTH_CLIENT_ID=${GITEA_OAUTH_CLIENT_ID}
      - GITEA_OAUTH_CLIENT_SECRET=${GITEA_OAUTH_CLIENT_SECRET}
      - APP_SECRET_KEY=${APP_SECRET_KEY}
      - DATABASE_URL=sqlite:////data/flowdeck.db
      - WEBHOOK_BASE_URL=${WEBHOOK_BASE_URL:-http://localhost:8080}
    volumes:
      - flowdeck_data:/data
    restart: unless-stopped

  # Optionnel: PostgreSQL pour le futur
  # postgres:
  #   image: postgres:16-alpine
  #   environment:
  #     POSTGRES_USER: flowdeck
  #     POSTGRES_PASSWORD: ${DB_PASSWORD}
  #     POSTGRES_DB: flowdeck
  #   volumes:
  #     - pg_data:/var/lib/postgresql/data

volumes:
  flowdeck_data:
  # pg_data:

14.2 Migration SQLite → PostgreSQL

# Étape 1: Exporter le schéma SQLite vers PostgreSQL
pgloader /data/flowdeck.db postgresql://flowdeck:***@localhost/flowdeck

# Étape 2: Changer la config
# DATABASE_URL=postgresql://flowdeck:***@localhost/flowdeck

# Étape 3: Redémarrer
docker compose restart flowdeck

15. Références


16. Versions à venir

v4.1.0 — Content Blocks enrichis (priorité standard)

  • Callout boxes — Blocs d'alerte (info, warning, tip, danger)
  • Table of Contents — Auto-généré depuis les headings
  • LaTeX / KaTeX — Formules mathématiques
  • Toggle lists — Listes pliables/dépliables
  • Multi-columns — Mise en page 2-3 colonnes

v4.2.0 — Export (priorité standard)

  • Export Markdown avec images
  • Export PDF (via WeasyPrint ou headless Chrome)
  • Export HTML standalone (page auto-suffisante)

v4.3.0 — Collaboration (priorité basse)

  • Commentaires inline sur les pages
  • @mentions pour notifier des utilisateurs
  • Notifications email (changements, mentions)
  • Permissions par workspace (viewer, editor, admin)

v5.0.0 — Plateforme avancée (futur)

  • AI Assistants — Génération de contenu, résumés, suggestions
  • Embeds — Vidéos, PDF, Figma, Google Docs
  • Automatisations — Règles déclenchées sur événements (Notion-style)
  • Base de données avancée — Relations inter-collections, rollups
  • Kanban flexible — Colonnes custom, WIP limits
  • API publique REST v2 — /api/v2 (v6.3.0) : Bearer + scopes read/write/admin, CRUD complet, pagination, RFC 7807, idempotence, audit, OpenAPI (/docs, docs/openapi-v2.json) ; /api/v1 lecture seule (compat)
  • Volume Docker persistant — /data monté pour survie des données
  • PostgreSQL — Migration optionnelle pour scaling