Files
flowdeck/docs/NOTION_DATABASE_TASKS_GUIDE.md
bruno 1dea15ad90
FlowDeck CI / test (push) Failing after 4s
FlowDeck CI / docker (push) Has been skipped
docs: Guide Notion Database & Tasks — analyse complète pour FlowDeck
- Anatomie des Databases Notion (Collection/CollectionView/Pages)
- 21 types de propriétés, 6 types de vues, Relations & Rollups
- Architecture des Tasks (sub-items auto-référencés, dépendances)
- Plan d'implémentation en 6 phases avec SQL, Python et HTML
- Rétrocompatibilité avec les boards Gitea existants
2026-07-09 17:41:21 -04:00

36 KiB

Guide Technique — Recréer Database & Tasks de Notion dans FlowDeck

Version: 1.0 · Date: 2026-07-10 Auteur: Hermes-Deepin · Cible: FlowDeck v1.2+


Table des matières

  1. Introduction
  2. Comprendre Notion : le modèle « Blocs & Collections »
  3. La Database Notion : anatomie complète
  4. Les Tasks Notion : un cas spécial de database
  5. Comment Notion connecte Database et Tasks
  6. Implémentation dans FlowDeck : plan pas à pas
  7. Références

Introduction

L'objectif est de recréer dans FlowDeck les deux fonctionnalités les plus puissantes de Notion :

  • Les Databases — tables structurées avec propriétés typées, vues multiples, filtres et tris
  • Les Tasks — sous-tâches hiérarchiques avec dépendances, liées aux databases

Ces deux systèmes sont profondément interconnectés dans Notion : les tâches ne sont qu'une forme spécialisée de database, avec un parent auto-référencé et des propriétés de dépendance.

État actuel de FlowDeck (v1.2)

FlowDeck a déjà une base solide :

✅ Boards (Kanban lié à Gitea)     → table `boards`
✅ Cards (issues → colonnes)       → table `cards`
✅ Colonnes custom                 → `project_properties` + `property_values`
✅ Checklists par issue            → `checklists` + `checklist_items`
✅ Vues multiples                  → Kanban, Table, Status, TeamLoad, Detailed
✅ Notes Markdown                  → `notes`
✅ Pages (ébauche Notion editor)   → `pages` (content, parent_id, sort_order, content_format)
⚠️ Database concept                → ABSENT (pas de notion de collection/row)
❌ Relations entre projets         → ABSENT
❌ Sub-tasks / Parent-item         → ABSENT
❌ Rollups / Formules              → ABSENT
❌ Dependencies (bloque/bloqué par) → ABSENT

1. Comprendre Notion : le modèle « Blocs & Collections »

1.1 Le principe fondateur : « Everything is a Block »

Dans Notion, chaque élément de contenu est un bloc. Un bloc peut être :

┌──────────────────────────────────────┐
│  BLOC                                │
│  ├─ id: UUID                         │
│  ├─ type: "paragraph"                │
│  │         "heading_1"               │
│  │         "to_do"                   │
│  │         "bulleted_list_item"      │
│  │         "child_page"              │
│  │         "child_database"          │
│  │         "collection_view_page"    │  ← Database
│  │         ...                       │
│  ├─ content: [...]   (blocs enfants) │
│  ├─ properties: {}  (metadata)       │
│  └─ parent: {type, id}               │
└──────────────────────────────────────┘

1.2 Les 3 couches d'une Database Notion

Une database Notion est un assemblage de 3 concepts distincts :

┌────────────────────────────────────────────────────────────┐
│                     DATABASE NOTION                         │
│                                                             │
│  ┌─────────────┐    ┌──────────────────┐    ┌────────────┐│
│  │  COLLECTION │    │ COLLECTION_VIEW  │    │   PAGES    ││
│  │  (schema)   │◄───│ (présentation)   │◄───│  (data)   ││
│  │             │    │                  │    │           ││
│  │ properties  │    │ type: table      │    │ row 1     ││
│  │ - Name      │    │       board      │    │ row 2     ││
│  │ - Status    │    │       calendar   │    │ row 3     ││
│  │ - Priority  │    │       gallery    │    │ ...       ││
│  │ - Due Date  │    │       list       │    │           ││
│  │ - Assignee  │    │       timeline   │    │           ││
│  │             │    │                  │    │           ││
│  │             │    │ filters          │    │           ││
│  │             │    │ sorts            │    │           ││
│  │             │    │ grouping         │    │           ││
│  │             │    │ format options   │    │           ││
│  └─────────────┘    └──────────────────┘    └────────────┘│
│                                                             │
│  1 SCHÉMA           N VUES (filtres       N LIGNES           │
│  partagé            + tris différents)    (pages filles)     │
└────────────────────────────────────────────────────────────┘

Concept clé : Une database n'est PAS une table SQL. C'est une collection de pages, chaque page ayant un schéma de propriétés commun, affiché via des vues configurables.

1.3 Le cycle de vie d'une Database

Création                     Consultation                Modification
─────────                    ────────────                ────────────

1. L'utilisateur choisit     4. Les vues lisent le       7. Édition inline :
   un type de vue              schéma + les pages          - click → édite valeur
   (table, board...)                                       - drag → réorganise
                              5. Filtres/sorts/tri         - shortcuts → ajoute ligne
2. Notion crée :               appliqués
   - 1 Collection                                     8. Changement de vue = 
   - 1 CollectionView        6. Rendu en temps réel       même données,
   - 1 CollectionViewPage                                  autre présentation
   - + pages vides optionnelles
                                                        9. Ajout propriété =
3. L'utilisateur définit                                  nouveau champ sur
   les propriétés (schéma)                                TOUTES les pages


2. La Database Notion : anatomie complète

2.1 Propriétés (le schéma)

Notion supporte 21 types de propriétés. Toutes sont définies au niveau de la Collection :

Type              │ Description                │ Exemple de valeur
──────────────────┼────────────────────────────┼──────────────────
Title             │ Le titre de la page (obligatoire, unique)
Text              │ Texte libre                │ "Description..."
Number            │ Nombre (décimales, format) │ 42 / 12.5%
Select            │ Choix unique               │ "En cours"
Multi-select      │ Choix multiples            │ ["Frontend","Backend"]
Status            │ Select avec couleur forcée │ "Done" (vert)
Date              │ Date + heure optionnelle   │ 2026-07-15
Person            │ Mention utilisateur        │ ["bruno"]
Files & media     │ Uploads                    │ [url1, url2]
Checkbox          │ Booléen                    │ true / false
URL               │ Lien cliquable             │ "https://..."
Email             │ Email cliquable            │ "[email protected]"
Phone             │ Téléphone cliquable        │ "+1..."
Formula           │ Calcul (JS-like)           │ prop("Prix")*prop("Qté")  ← CLÉ
Relation          │ Lien vers autre database   │ [page_id_1, page_id_2]     ← CLÉ
Rollup            │ Agrégation via relation    │ sum, avg, count, min, max  ← CLÉ
Created time      │ Date de création (auto)    │ 2026-01-15T10:30:00Z
Created by        │ Auteur (auto)              │ "bruno"
Last edited time  │ Dernière édition (auto)    │ 2026-07-09T18:00:00Z
Last edited by    │ Dernier éditeur (auto)     │ "bruno"
Unique ID         │ ID incrémental (auto)      │ 42
Button            │ Déclenche action (nouveau) │ —
AI Summary        │ Résumé AI (nouveau)         │ "Ce projet..."

2.2 Views (les vues)

Une database peut avoir N vues, chaque vue avec :

┌─────────────────────────────────────────┐
│  Vue "Kanban"                           │
│  ├─ type: board                         │
│  ├─ group_by: "Status"                  │  ← Groupement
│  ├─ filter: {Status ≠ "Archivé"}        │  ← Filtres
│  ├─ sort: [Due Date ASC, Priority DESC] │  ← Tris
│  ├─ visible_properties: [Name, Assignee, Due Date]  │
│  ├─ card_size: medium                   │
│  └─ cover_image_property: "Files"       │
└─────────────────────────────────────────┘

6 types de vues :

Vue Usage Particularité
Table Liste classique Colonnes = propriétés
Board Kanban Groupé par propriété Select/Status
Timeline Gantt Axe X = propriété Date
Calendar Calendrier Groupé par propriété Date (jour/semaine/mois)
Gallery Cartes visuelles Cover image + preview
List Liste compacte Comme table mais 1 colonne + preview

Filtres avancés :

Filtre 1 : Status IS "En cours"         → filtre simple
Filtre 2 : Due Date IS BEFORE "today"   → filtre date
Filtre 3 : Priority CONTAINS "P1"       → filtre multi-select
Groupe : AND / OR entre les filtres
Sous-groupe : (FiltreA OR FiltreB) AND FiltreC

2.3 Relations et Rollups (les super-pouvoirs)

C'est LE mécanisme qui rend Notion puissant :

┌─────────────────────┐         ┌──────────────────────┐
│   DATABASE A        │         │   DATABASE B         │
│   « Projets »       │         │   « Tâches »         │
│                     │         │                      │
│ ┌─────────────┐     │ relation│ ┌──────────────┐     │
│ │ Nom         │     │◄───────►│ │ Titre        │     │
│ │ Description │     │         │ │ Priorité     │     │
│ │ Statut      │     │         │ │ Projet ──────┼──┐  │
│ └─────────────┘     │         │ │ Assignee     │  │  │
│                     │         │ └──────────────┘  │  │
│                     │         │                   │  │
│ ROLLUP « Nb tâches»│◄────────┼─── count(Tâches)  │  │
│ ROLLUP « Avancement│◄────────┼─── avg(%Complété) │  │
│ ROLLUP « Prochaine │◄────────┼─── min(Due Date)  │  │
└─────────────────────┘         └───────────────────┘──┘

Relation = pointeur bidirectionnel entre pages de 2 databases différentes
Rollup = agrégation (COUNT, SUM, AVG, MIN, MAX, RANGE, UNIQUE) sur une propriété de la database liée


3. Les Tasks Notion : un cas spécial de database

3.1 Architecture des tâches

Contrairement à ce qu'on pourrait penser, Notion n'a pas de module « Tasks » séparé.

Les tâches sont simplement des PAGES dans une DATABASE
qui possède des propriétés spécifiques de type tâche.

Voici comment Notion les distingue :

┌─────────────────────────────────────────────────────────────┐
│         DATABASE « Tasks » (ou n'importe quelle DB)          │
│                                                              │
│  Propriétés « tâche »         │  Propriétés « standard »     │
│  ──────────────────────────   │  ──────────────────────────  │
│  ✅ Sub-item (parent)      │  • Titre (obligatoire)         │
│  ✅ Dependencies (bloque)  │  • Status (Select)             │
│  ✅ Due Date               │  • Priority (Select)           │
│  ✅ Assignee               │  • Tags (Multi-select)         │
│  ✅ Reminder               │  • ...                         │
│                                                              │
│  La magie : ces propriétés SONT la database.                 │
│  Pas de modèle « Task » séparé.                              │
└─────────────────────────────────────────────────────────────┘

3.2 Sub-items (sous-tâches)

Les sous-tâches utilisent une relation auto-référencée :

┌──────────────────────────────────────┐
│  Table « tasks » (dans la DB)        │
│                                      │
│  id │ title          │ parent_id     │
│  ───┼────────────────┼───────────────│
│  1  │ Refonte UI     │ NULL          │  ← Tâche parent
│  2  │ Nouveau header │ 1             │  ← Sous-tâche de 1
│  3  │ Dark mode      │ 1             │  ← Sous-tâche de 1
│  4  │ Tests header   │ 2             │  ← Sous-sous-tâche de 2
│  5  │ Déploiement    │ NULL          │  ← Autre tâche parent
│                                      │
│  Propriétés spéciales :              │
│  ├─ « Sub-item » = relation vers     │
│  │   la même database                │
│  └─ « Parent item » = l'inverse      │
│      (auto-créé par Notion)          │
└──────────────────────────────────────┘

Comment ça fonctionne techniquement :

  1. L'utilisateur active « Sub-items » dans une database
  2. Notion crée automatiquement 2 propriétés de type Relation pointant vers la même database :
    • Sub-item (relation → même DB)
    • Parent item (relation inverse, auto-générée)
  3. Dans l'interface Kanban, les sous-tâches apparaissent imbriquées sous le parent
  4. La récursion est illimitée (tâche → sous-tâche → sous-sous-tâche → ...)

3.3 Dependencies (dépendances)

Les dépendances bloquantes sont une seconde relation auto-référencée :

┌──────────────────────────────────────┐
│  Relation « Bloque »                │
│                                      │
│  Tâche A ──bloque──► Tâche B        │
│  Tâche B ──bloquée par──► Tâche A   │
│                                      │
│  Règles métier :                     │
│  ├─ B ne peut pas être « Done »     │
│  │   tant que A n'est pas « Done »  │
│  ├─ Si A est déplacée, B suit      │
│  │   (optionnel, selon config)      │
│  └─ Timeline affiche les flèches    │
│      A ────────► B                  │
└──────────────────────────────────────┘

3.4 La vue « My Tasks » (vue agrégée)

La killer feature de Notion : un dashboard qui regroupe TOUTES les tâches, peu importe leur database d'origine.

┌──────────────────────────────────────────────────┐
│               MY TASKS                            │
│  ┌───────────────────────────────────────────┐   │
│  │ Projet Alpha      ┌──────────────────────┐│   │
│  │ ├─ Design maquette│ Assignee: Bruno      ││   │
│  │ └─ Tests UI       │ Due: 15 juillet      ││   │
│  │                   └──────────────────────┘│   │
│  │ Projet Beta                                │   │
│  │ ├─ API endpoint                             │   │
│  │ └─ Documentation                            │   │
│  │ Daily Tasks                                 │   │
│  │ └─ Réunion standup                          │   │
│  └───────────────────────────────────────────┘   │
│                                                    │
│  Agrège TOUTES les databases ayant :               │
│  ├─ Une propriété « Person » = current user       │
│  └─ Un Status ≠ « Done »                          │
└──────────────────────────────────────────────────┘

Fonctionnement interne :

  1. Notion scanne TOUTES les databases du workspace
  2. Filtre les pages où Assignee = current_user ET Status ≠ Done/Completed
  3. Regroupe par database d'origine (ou par projet parent)
  4. Affiche avec tri par Due Date ASC
  5. Cette vue est read-only logique (pas une vraie database)

4. Comment Notion connecte Database et Tasks

4.1 Le diagramme de dépendance conceptuel

                    DATABASE (concept racine)
                         │
          ┌──────────────┼──────────────┐
          ▼              ▼              ▼
    COLLECTION      PAGES (rows)    COLLECTION_VIEWS
    (schéma)        │              (présentations)
    │               │                  │
    │               ├─ Title           ├─ Table
    ├─ Text         ├─ Properties      ├─ Board
    ├─ Select       │  (valeurs        ├─ Calendar
    ├─ Date         │   par page)      ├─ Timeline
    ├─ Person       │                  ├─ Gallery
    ├─ Relation ────┼──► SELF          └─ List
    │   └── Sub-items │  ┌── Parent
    │   └── Deps      │  └── Blocked by
    ├─ Rollup ←──────┼──┘ (count, sum, avg via relation)
    ├─ Formula       │
    └─ ...           │
                     │
              ┌──────┴──────┐
              │   TASKS     │  (surcouche métier)
              │             │
              │ Ne sont PAS │
              │ un modèle   │
              │ séparé —    │
              │ ce sont des │
              │ PAGES avec  │
              │ propriétés  │
              │ spécifiques │
              └─────────────┘

4.2 Les invariants du modèle

  1. Une database = 1 Collection (schéma de propriétés)
  2. 1 Collection = N CollectionViews (vues)
  3. 1 CollectionView = 1 type (table | board | calendar | timeline | gallery | list)
  4. Les pages d'une database sont des blocs (comme toute page Notion)
  5. Une propriété « Relation » pointe vers une autre database (pas une table SQL)
  6. Une propriété « Rollup » agrège via une Relation existante
  7. Les sub-items sont une Relation auto-référencée
  8. Les dépendances sont une Relation auto-référencée avec contrainte métier

5. Implémentation dans FlowDeck : plan pas à pas

5.1 Phase 1 — Le concept de Database (semaine 1)

Objectif : Abstraire le concept de « board Gitea » en « database » réutilisable.

Nouvelle table : collections

CREATE TABLE collections (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    description TEXT DEFAULT '',
    icon TEXT DEFAULT '📋',
    
    -- Peut être liée à un projet Gitea (optionnel)
    gitea_owner TEXT,
    gitea_repo TEXT,
    
    -- Schéma de propriétés en JSON (flexible, évolutif)
    schema_json TEXT NOT NULL DEFAULT '[]',
    -- Format: [{"name":"Status","type":"select","options":["Todo","Done"],"position":0}, ...]
    
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Nouvelle table : collection_pages (remplace cards)

CREATE TABLE collection_pages (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    collection_id INTEGER NOT NULL REFERENCES collections(id) ON DELETE CASCADE,
    
    -- Peut être liée à une issue Gitea
    gitea_issue_id INTEGER,
    gitea_issue_number INTEGER,
    
    -- Titre de la page (obligatoire)
    title TEXT NOT NULL DEFAULT '',
    
    -- Position dans la collection
    position INTEGER NOT NULL DEFAULT 0,
    
    -- Sub-items (auto-référence)
    parent_id INTEGER REFERENCES collection_pages(id),
    
    -- Valeurs des propriétés en JSON
    property_values_json TEXT NOT NULL DEFAULT '{}',
    -- Format: {"Status":"Done","Priority":"P1","DueDate":"2026-07-15"}
    
    -- Métadonnées temporelles
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

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);

Nouvelle table : collection_views

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
    
    -- Configuration JSON
    config_json TEXT NOT NULL DEFAULT '{}',
    -- Format:
    -- {
    --   "group_by": "Status",           ← board/calendar uniquement
    --   "filters": [                     ← tous types
    --     {"property":"Status","operator":"is_not","value":"Done"},
    --     {"property":"DueDate","operator":"is_before","value":"today"}
    --   ],
    --   "filter_conjunction": "and",     ← "and" | "or"
    --   "sorts": [                       ← tous types
    --     {"property":"Priority","direction":"asc"},
    --     {"property":"DueDate","direction":"desc"}
    --   ],
    --   "visible_properties": ["Title","Status","Assignee","DueDate"],
    --   "card_size": "medium",           ← board/gallery
    --   "cover_property": "Files",       ← board/gallery
    --   "date_property": "DueDate",      ← calendar/timeline
    --   "date_range_property": "EndDate" ← timeline (optionnel)
    -- }
    
    position INTEGER NOT NULL DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

5.2 Phase 2 — Le système de propriétés (semaine 1-2)

Nouvelle table : collection_properties (remplace project_properties)

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',
    -- Types: title, text, number, select, multi_select, status,
    --        date, person, checkbox, url, email, phone,
    --        relation, rollup, formula,
    --        created_time, created_by, last_edited_time, last_edited_by
    
    -- Options pour select/multi_select/status
    options_json TEXT DEFAULT '[]',
    -- Format: [{"name":"Todo","color":"gray"},{"name":"Done","color":"green"}]
    
    -- Pour les relations
    related_collection_id INTEGER REFERENCES collections(id),
    reverse_name TEXT,  -- nom de la relation inverse (auto-généré)
    
    -- Pour les rollups
    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 les formules
    formula_expression TEXT,
    
    -- Position dans la liste des propriétés
    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)
);

Mapping des types Notion → FlowDeck :

PROPERTY_TYPES = {
    "title": {
        "storage": "string",
        "validation": "max 2000 chars",
        "indexed": True,
        "unique_per_collection": False,
    },
    "text": {
        "storage": "string",
        "validation": "any text",
    },
    "number": {
        "storage": "float",
        "validation": "numeric",
        "format_options": ["number", "percent", "currency", "dollar", "euro", "pound", "yen"],
    },
    "select": {
        "storage": "string (from options_json)",
        "validation": "must match one option",
    },
    "multi_select": {
        "storage": "JSON array of strings",
        "validation": "each must match option",
    },
    "status": {
        "storage": "string (from options_json) + color",
        "validation": "must match one option",
        "colors": ["gray","brown","orange","yellow","green","blue","purple","pink","red"],
    },
    "date": {
        "storage": "ISO 8601 string or range",
        "validation": "valid datetime",
        "include_time": True,  # configurable
    },
    "person": {
        "storage": "JSON array of {id, login, avatar_url}",
        "validation": "must be workspace user",
    },
    "checkbox": {
        "storage": "boolean",
        "validation": "true/false",
    },
    "relation": {
        "storage": "JSON array of page IDs",
        "validation": "must be existing page in related_collection",
        "bidirectional": True,
    },
    "rollup": {
        "storage": "computed — not stored directly",
        "functions": ["count","count_values","empty","not_empty",
                      "sum","average","median","min","max","range"],
        "depends_on": ["relation_property_id","target_property_id"],
    },
    "formula": {
        "storage": "computed — not stored directly",
        "expression": "JavaScript-like DSL",
        "functions_available": [
            "prop()", "now()", "today()", "if()", "concat()", "round()",
            "dateAdd()", "dateSubtract()", "formatDate()", "toNumber()",
            "contains()", "length()", "replace()", "replaceAll()",
        ],
    },
}

5.3 Phase 3 — Sub-items et Dépendances (semaine 2)

Implémentation des sub-items (relation auto-référencée)

# Dans le schema d'une database, activer Sub-items crée automatiquement :

def enable_sub_items(collection_id: int):
    """Active les sub-items sur une collection."""
    
    # 1. Créer la propriété relation « Parent item » (si pas déjà)
    parent_prop = create_property(
        collection_id=collection_id,
        name="Parent item",
        prop_type="relation",
        related_collection_id=collection_id,  # AUTO-RÉFÉRENCE
        reverse_name="Sub-item",              # relation inverse
        visible_in_views=False,               # cachée dans les vues
    )
    
    # 2. Marquer la collection comme ayant des sub-items
    mark_collection_feature(collection_id, "has_sub_items", True)
    
    # 3. Mettre à jour la vue board pour afficher l'imbrication
    # Le rendu Kanban doit maintenant supporter :
    # - Groupe parent → sous-tâches indentées
    # - Bouton « + Add sub-item » sous chaque carte
    # - État du parent = agrégation des enfants

Rendu des sub-items dans le board

<!-- Dans board_fragment.html, modifier le rendu des cartes -->
<div class="kanban-card" data-page-id="{{ page.id }}">
    <!-- Carte parent -->
    <div class="card-title">{{ page.title }}</div>
    
    <!-- Sous-tâches -->
    {% for sub in page.sub_items %}
    <div class="sub-item" data-page-id="{{ sub.id }}">
        ├─ {{ sub.title }}
        <span class="sub-status">{{ sub.properties.Status }}</span>
    </div>
    {% endfor %}
    
    <!-- Bouton ajout sous-tâche -->
    <button hx-post="/api/pages/{{ page.id }}/sub-items"
            hx-swap="beforeend">
        + Sub-item
    </button>
</div>

Implémentation des dépendances

def enable_dependencies(collection_id: int):
    """Active les dépendances bloquantes."""
    
    # Création de 2 propriétés relation
    blocks = create_property(
        collection_id=collection_id,
        name="Blocks",
        prop_type="relation",
        related_collection_id=collection_id,
        reverse_name="Blocked by",
    )
    
    # Contrainte métier : au niveau applicatif
    # (pas de contrainte SQL pour garder la flexibilité)

def check_dependency_constraint(page_id: int, new_status: str):
    """Vérifie qu'une page peut passer à 'Done'
    seulement si toutes les pages qu'elle bloque sont Done."""
    
    blocked_pages = get_blocked_pages(page_id)
    if new_status == "Done" and blocked_pages:
        not_done = [p for p in blocked_pages
                    if p.status not in ("Done", "Cancelled")]
        if not_done:
            raise DependencyError(
                f"Cannot mark as Done: still blocking {len(not_done)} tasks"
            )

5.4 Phase 4 — « My Tasks » (vue agrégée) (semaine 2-3)

# GET /my-tasks
async def my_tasks(request: Request):
    """Dashboard personnel : toutes les tâches assignées à l'utilisateur."""
    
    user = get_current_user(request)
    
    # 1. Scanner TOUTES les collections
    collections = db.fetch_all("SELECT * FROM collections")
    
    my_tasks = []
    for col in collections:
        schema = json.loads(col.schema_json)
        
        # 2. Vérifier si la collection a une propriété Person
        person_props = [p for p in schema if p["type"] == "person"]
        if not person_props:
            continue
        
        # 3. Récupérer les pages assignées à l'utilisateur
        pages = db.fetch_all("""
            SELECT cp.*, cpv.value as property_values
            FROM collection_pages cp
            JOIN collection_properties cprop
              ON cprop.collection_id = cp.collection_id
             AND cprop.prop_type = 'person'
            WHERE cp.collection_id = ?
              AND cp.property_values_json LIKE ?
        """, (col.id, f'%"{user.login}"%'))
        
        my_tasks.extend([
            {"collection": col, "page": p, "schema": schema}
            for p in pages
        ])
    
    # 4. Grouper par collection
    grouped = {}
    for task in my_tasks:
        col_name = task["collection"].name
        if col_name not in grouped:
            grouped[col_name] = []
        grouped[col_name].append(task)
    
    # 5. Trier par Due Date
    for tasks in grouped.values():
        tasks.sort(key=lambda t: t["page"].get_property("DueDate") or "9999")
    
    return render_template("my_tasks.html", tasks=grouped)

5.5 Phase 5 — Rétrocompatibilité avec Gitea

Stratégie de migration progressive :

# Phase 5a : Wrapper — les boards Gitea existants
# deviennent des collections avec une couche de compatibilité

class GiteaBoardCompat:
    """Adaptateur : ancien board Gitea → nouvelle Collection."""
    
    @staticmethod
    def from_board(board_row):
        """Convertit un board existant en collection."""
        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,
        }

# Phase 5b : Nouvelle route hybride
# GET /board/{owner}/{repo} → détecte si c'est un board Gitea legacy
# ou une collection pure, et route vers le bon renderer

5.6 Résumé du plan d'implémentation

Semaine 1                Semaine 2              Semaine 3
────────                 ─────────              ─────────

┌──────────────┐        ┌──────────────┐       ┌──────────────┐
│ Database     │        │ Properties   │       │ My Tasks     │
│ Concept      │   ──►  │ Avancées     │  ──►  │ Dashboard    │
│              │        │              │       │              │
│ • collections│        │ • Relations  │       │ • Vue agrégée│
│ • pages      │        │ • Rollups    │       │ • Filtres    │
│ • views      │        │ • Formules   │       │   globaux    │
│ • filters    │        │ • Auto-prop  │       │ • Calendrier │
│ • sorts      │        │   computed   │       │   intégré    │
│              │        │              │       │              │
│ Sub-items    │        │ Dependencies │       │ UI polish    │
│ (parent_id)  │   ──►  │ + contraintes│  ──►  │ + tests      │
└──────────────┘        └──────────────┘       └──────────────┘

6. Références

Différences clés FlowDeck vs Notion

Concept Notion FlowDeck (cible)
Database Collection + Pages + Views collections + collection_pages + collection_views
Schéma Properties sur la Collection collection_properties au niveau collection
Valeurs Dans chaque page (bloc) property_values_json dans chaque collection_page
Sub-items Relation auto-référencée parent_id auto-référence dans collection_pages
Dependencies Relation auto-référencée + contrainte Idem + check applicatif au changement de statut
My Tasks Scan cross-database + filtre Route /my-tasks + JOIN sur collection_pages
Gitea sync N/A (pas de Gitea) Les pages peuvent être liées à gitea_issue_id
Stockage Cloud distribué (PostgreSQL + cache) SQLite local (1 fichier)