{ "version": "1.0.0", "project": { "name": "FlowDeck", "description": "Clone complet de Notion intégré nativement à Gitea — Databases, Pages, Kanban, Calendar, Gallery, Timeline, List, Multi-Users. v2.1.0 avec API publique REST, Webhooks sortants configurables et support PWA.", "languages": [ "css", "dockerfile", "html", "javascript", "json", "markdown", "python", "toml", "yaml" ], "frameworks": [ "Docker", "Docker Compose", "FastAPI", "Pydantic", "Uvicorn", "pytest" ], "analyzedAt": "2026-07-10T12:53:26Z", "gitCommitHash": "c86c04ca220672fb012bb69916e930102be44ef4" }, "nodes": [ { "id": "file:app/routers/my_tasks.py", "type": "file", "name": "my_tasks.py", "summary": "Tableau de bord « Mes tâches » : affichage des tâches assignées à l'utilisateur avec filtrage par statut, date et priorité, agrégé par collection.", "tags": [ "api-handler", "dashboard", "tasks", "user-specific" ], "complexity": "moderate", "filePath": "app/routers/my_tasks.py" }, { "id": "file:app/routers/notes.py", "type": "file", "name": "notes.py", "summary": "Gestion des notes de projet (bloc-notes) : lecture et sauvegarde via templates Jinja2, liées à un projet Gitea (owner/repo).", "tags": [ "api-handler", "notes", "jinja2", "project-documents" ], "complexity": "moderate", "filePath": "app/routers/notes.py" }, { "id": "file:app/routers/pages.py", "type": "file", "name": "pages.py", "summary": "[FICHIER NON TROUVÉ — anciennement le routeur de pages individuelles, probablement fusionné dans board.py ou collections.py]", "tags": [ "api-handler", "pages", "crud", "fichier-manquant" ], "complexity": "simple", "filePath": "app/routers/pages.py" }, { "id": "file:app/routers/properties.py", "type": "file", "name": "properties.py", "summary": "[FICHIER NON TROUVÉ — anciennement le routeur de propriétés, probablement fusionné dans collections.py]", "tags": [ "api-handler", "properties", "schema", "fichier-manquant" ], "complexity": "simple", "filePath": "app/routers/properties.py" }, { "id": "function:app/routers/my_tasks.py:_get_current_user", "type": "function", "name": "_get_current_user", "summary": "Extrait l'utilisateur courant depuis le cookie de session via SessionManager.", "tags": [ "auth", "session", "user", "internal" ], "complexity": "simple", "filePath": "app/routers/my_tasks.py", "lineRange": [ 17, 19 ] }, { "id": "function:app/routers/my_tasks.py:my_tasks_dashboard", "type": "function", "name": "my_tasks_dashboard", "summary": "Page HTML du tableau de bord « Mes tâches » : liste les tâches assignées à l'utilisateur avec filtres et regroupement par statut.", "tags": [ "dashboard", "tasks", "html", "view" ], "complexity": "complex", "filePath": "app/routers/my_tasks.py", "lineRange": [ 23, 148 ] }, { "id": "function:app/routers/my_tasks.py:my_tasks_api", "type": "function", "name": "my_tasks_api", "summary": "Endpoint API retournant les tâches de l'utilisateur au format JSON.", "tags": [ "api", "tasks", "json" ], "complexity": "simple", "filePath": "app/routers/my_tasks.py", "lineRange": [ 152, 178 ] }, { "id": "function:app/routers/notes.py:get_notes", "type": "function", "name": "get_notes", "summary": "Affiche la page de notes d'un projet Gitea (édition en ligne avec template Jinja2).", "tags": [ "notes", "html", "view", "jinja2" ], "complexity": "simple", "filePath": "app/routers/notes.py", "lineRange": [ 16, 29 ] }, { "id": "function:app/routers/notes.py:save_notes", "type": "function", "name": "save_notes", "summary": "Sauvegarde le contenu des notes d'un projet Gitea dans la base de données.", "tags": [ "notes", "save", "api" ], "complexity": "simple", "filePath": "app/routers/notes.py", "lineRange": [ 33, 52 ] }, { "id": "file:app/__init__.py", "type": "file", "name": "__init__.py", "filePath": "app/__init__.py", "summary": "Fichier d'initialisation du package principal de l'application FlowDeck.", "tags": [ "entry-point", "package-init" ], "complexity": "simple" }, { "id": "file:app/db.py", "type": "file", "name": "db.py", "summary": "Module de base de données SQLite : initialisation du schéma (collections, pages, propriétés, webhooks, notes) et gestion des connexions.", "tags": [ "database", "sqlite", "schema", "persistence" ], "complexity": "complex", "filePath": "app/db.py", "languageNotes": "Utilise sqlite3 avec row_factory pour retourner des dictionnaires. Définit un schéma complet avec triggers et valeurs par défaut." }, { "id": "file:app/main.py", "type": "file", "name": "main.py", "summary": "Point d'entrée FastAPI de FlowDeck : configuration du lifespan, montage des routeurs, middleware CSRF, gestion de session et manifeste PWA.", "tags": [ "entry-point", "fastapi", "middleware", "pwa", "application" ], "complexity": "moderate", "filePath": "app/main.py", "languageNotes": "Pattern lifespan FastAPI pour l'initialisation asynchrone de la BDD. Utilise CORSMiddleware et mount pour les fichiers statiques." }, { "id": "file:app/routers/__init__.py", "type": "file", "name": "__init__.py", "summary": "Package d'initialisation du sous-module routers, marquant le répertoire comme package Python.", "tags": [ "package", "entry-point", "barrel" ], "complexity": "simple", "filePath": "app/routers/__init__.py" }, { "id": "file:app/routers/collections.py", "type": "file", "name": "collections.py", "filePath": "app/routers/collections.py", "summary": "Routeur FastAPI complet pour les collections Notion-style — CRUD, propriétés, relations, rollups, formules, vues (table/calendar/gallery/list/timeline), sous-éléments, dépendances et compatibilité Gitea Boards.", "tags": [ "api-handler", "crud", "collections", "notion-style", "views" ], "complexity": "complex", "languageNotes": "Routeur massif avec 39 endpoints organisés par sections (API REST + pages HTML), moteurs de rendu de vues embarqués, et lazy-imports pour les services." }, { "id": "function:app/db.py:get_db_path", "type": "function", "name": "get_db_path", "summary": "Retourne le chemin du fichier de base de données SQLite, situé dans le répertoire data/ du projet.", "tags": [ "utility", "path", "configuration" ], "complexity": "simple", "filePath": "app/db.py", "lineRange": [ 9, 11 ] }, { "id": "function:app/db.py:init_db", "type": "function", "name": "init_db", "summary": "Initialise le schéma complet de la base de données : tables collections, collection_pages, collection_properties, collection_views, webhooks, notes, triggers et index.", "tags": [ "database", "schema", "initialization", "migration" ], "complexity": "complex", "filePath": "app/db.py", "lineRange": [ 14, 295 ] }, { "id": "function:app/db.py:get_conn", "type": "function", "name": "get_conn", "summary": "Crée et retourne une connexion SQLite avec row_factory=dict et activation des clés étrangères.", "tags": [ "database", "connection", "factory" ], "complexity": "simple", "filePath": "app/db.py", "lineRange": [ 299, 309 ] }, { "id": "function:app/main.py:lifespan", "type": "function", "name": "lifespan", "summary": "Gestionnaire de cycle de vie FastAPI : initialise la BDD, les tables webhook, active WAL et les foreign keys au démarrage.", "tags": [ "lifecycle", "initialization", "fastapi", "startup" ], "complexity": "simple", "filePath": "app/main.py", "lineRange": [ 27, 37 ] }, { "id": "function:app/main.py:pwa_manifest", "type": "function", "name": "pwa_manifest", "summary": "Sert le manifeste PWA (application/json) pour l'installation en tant que Progressive Web App.", "tags": [ "pwa", "manifest", "endpoint" ], "complexity": "simple", "filePath": "app/main.py", "lineRange": [ 67, 76 ] }, { "id": "function:app/routers/collections.py:list_collections", "type": "function", "name": "list_collections", "filePath": "app/routers/collections.py", "lineRange": [ 21, 33 ], "summary": "Page HTML listant toutes les collections du workspace.", "tags": [ "api-handler", "collections", "html-rendering" ], "complexity": "simple" }, { "id": "function:app/routers/collections.py:list_collections_api", "type": "function", "name": "list_collections_api", "filePath": "app/routers/collections.py", "lineRange": [ 37, 43 ], "summary": "API REST — liste toutes les collections en JSON.", "tags": [ "api-handler", "collections", "rest-api" ], "complexity": "simple" }, { "id": "function:app/routers/collections.py:create_collection_api", "type": "function", "name": "create_collection_api", "filePath": "app/routers/collections.py", "lineRange": [ 47, 86 ], "summary": "API REST — crée une collection avec schéma, propriétés et vue par défaut.", "tags": [ "api-handler", "collections", "crud" ], "complexity": "moderate" }, { "id": "function:app/routers/collections.py:update_collection_api", "type": "function", "name": "update_collection_api", "filePath": "app/routers/collections.py", "lineRange": [ 93, 124 ], "summary": "API REST — met à jour les métadonnées d'une collection existante.", "tags": [ "api-handler", "collections", "crud" ], "complexity": "moderate" }, { "id": "function:app/routers/collections.py:delete_collection_api", "type": "function", "name": "delete_collection_api", "filePath": "app/routers/collections.py", "lineRange": [ 128, 140 ], "summary": "API REST — supprime une collection et ses pages en cascade.", "tags": [ "api-handler", "collections", "crud" ], "complexity": "simple" }, { "id": "function:app/routers/collections.py:get_page_api", "type": "function", "name": "get_page_api", "filePath": "app/routers/collections.py", "lineRange": [ 147, 156 ], "summary": "API REST — récupère une page unique par son ID.", "tags": [ "api-handler", "pages", "rest-api" ], "complexity": "simple" }, { "id": "function:app/routers/collections.py:update_page_api", "type": "function", "name": "update_page_api", "filePath": "app/routers/collections.py", "lineRange": [ 160, 191 ], "summary": "API REST — met à jour les propriétés d'une page (titre, icône, position, propriétés JSON).", "tags": [ "api-handler", "pages", "crud" ], "complexity": "moderate" }, { "id": "function:app/routers/collections.py:delete_page_api", "type": "function", "name": "delete_page_api", "filePath": "app/routers/collections.py", "lineRange": [ 195, 207 ], "summary": "API REST — supprime une page de sa collection.", "tags": [ "api-handler", "pages", "crud" ], "complexity": "simple" }, { "id": "function:app/routers/collections.py:list_boards_as_collections", "type": "function", "name": "list_boards_as_collections", "filePath": "app/routers/collections.py", "lineRange": [ 214, 218 ], "summary": "API REST — liste les boards Gitea comme pseudo-collections.", "tags": [ "api-handler", "gitea-integration", "rest-api" ], "complexity": "simple" }, { "id": "function:app/routers/collections.py:get_board_as_collection", "type": "function", "name": "get_board_as_collection", "filePath": "app/routers/collections.py", "lineRange": [ 222, 233 ], "summary": "API REST — récupère un board Gitea spécifique avec ses issues converties en cartes.", "tags": [ "api-handler", "gitea-integration", "rest-api" ], "complexity": "simple" }, { "id": "function:app/routers/collections.py:sync_board_to_collection", "type": "function", "name": "sync_board_to_collection", "filePath": "app/routers/collections.py", "lineRange": [ 237, 247 ], "summary": "API REST — synchronise un board Gitea vers une collection réelle.", "tags": [ "api-handler", "gitea-integration", "synchronization" ], "complexity": "simple" }, { "id": "function:app/routers/collections.py:list_property_types_api", "type": "function", "name": "list_property_types_api", "filePath": "app/routers/collections.py", "lineRange": [ 254, 257 ], "summary": "API REST — liste tous les types de propriétés disponibles.", "tags": [ "api-handler", "properties", "rest-api" ], "complexity": "simple" }, { "id": "function:app/routers/collections.py:list_properties_api", "type": "function", "name": "list_properties_api", "filePath": "app/routers/collections.py", "lineRange": [ 261, 271 ], "summary": "API REST — liste toutes les propriétés d'une collection.", "tags": [ "api-handler", "properties", "rest-api" ], "complexity": "simple" }, { "id": "function:app/routers/collections.py:create_property_api", "type": "function", "name": "create_property_api", "filePath": "app/routers/collections.py", "lineRange": [ 275, 314 ], "summary": "API REST — crée une nouvelle propriété sur une collection avec validation d'unicité.", "tags": [ "api-handler", "properties", "crud" ], "complexity": "moderate" }, { "id": "function:app/routers/collections.py:update_property_api", "type": "function", "name": "update_property_api", "filePath": "app/routers/collections.py", "lineRange": [ 318, 346 ], "summary": "API REST — met à jour une propriété existante.", "tags": [ "api-handler", "properties", "crud" ], "complexity": "moderate" }, { "id": "function:app/routers/collections.py:delete_property_api", "type": "function", "name": "delete_property_api", "filePath": "app/routers/collections.py", "lineRange": [ 350, 361 ], "summary": "API REST — supprime une propriété d'une collection.", "tags": [ "api-handler", "properties", "crud" ], "complexity": "simple" }, { "id": "function:app/routers/collections.py:create_relation_property", "type": "function", "name": "create_relation_property", "filePath": "app/routers/collections.py", "lineRange": [ 368, 416 ], "summary": "API REST — crée une propriété de relation bidirectionnelle entre deux collections.", "tags": [ "api-handler", "relations", "crud" ], "complexity": "moderate" }, { "id": "function:app/routers/collections.py:link_pages", "type": "function", "name": "link_pages", "filePath": "app/routers/collections.py", "lineRange": [ 420, 490 ], "summary": "API REST — lie deux pages via une propriété de relation avec mise à jour bidirectionnelle.", "tags": [ "api-handler", "relations", "linking" ], "complexity": "complex" }, { "id": "function:app/routers/collections.py:compute_rollup", "type": "function", "name": "compute_rollup", "filePath": "app/routers/collections.py", "lineRange": [ 494, 516 ], "summary": "API REST — calcule une agrégation rollup via le RollupEngine.", "tags": [ "api-handler", "rollup", "aggregation" ], "complexity": "moderate" }, { "id": "function:app/routers/collections.py:evaluate_formula", "type": "function", "name": "evaluate_formula", "filePath": "app/routers/collections.py", "lineRange": [ 520, 537 ], "summary": "API REST — évalue une expression de formule via le FormulaEngine.", "tags": [ "api-handler", "formula", "evaluation" ], "complexity": "moderate" }, { "id": "function:app/routers/collections.py:get_view_api", "type": "function", "name": "get_view_api", "filePath": "app/routers/collections.py", "lineRange": [ 544, 550 ], "summary": "API REST — récupère la configuration d'une vue.", "tags": [ "api-handler", "views", "rest-api" ], "complexity": "simple" }, { "id": "function:app/routers/collections.py:update_view_config", "type": "function", "name": "update_view_config", "filePath": "app/routers/collections.py", "lineRange": [ 554, 578 ], "summary": "API REST — met à jour la configuration d'une vue (group_by, card_size, filtres, tris).", "tags": [ "api-handler", "views", "configuration" ], "complexity": "moderate" }, { "id": "function:app/routers/collections.py:save_view_as", "type": "function", "name": "save_view_as", "filePath": "app/routers/collections.py", "lineRange": [ 582, 608 ], "summary": "API REST — sauvegarde l'état actuel d'une vue comme nouvelle vue nommée.", "tags": [ "api-handler", "views", "crud" ], "complexity": "moderate" }, { "id": "function:app/routers/collections.py:list_views_api", "type": "function", "name": "list_views_api", "filePath": "app/routers/collections.py", "lineRange": [ 612, 619 ], "summary": "API REST — liste toutes les vues d'une collection.", "tags": [ "api-handler", "views", "rest-api" ], "complexity": "simple" }, { "id": "function:app/routers/collections.py:list_sub_items", "type": "function", "name": "list_sub_items", "filePath": "app/routers/collections.py", "lineRange": [ 626, 633 ], "summary": "API REST — liste les sous-éléments d'une page.", "tags": [ "api-handler", "sub-items", "rest-api" ], "complexity": "simple" }, { "id": "function:app/routers/collections.py:create_sub_item", "type": "function", "name": "create_sub_item", "filePath": "app/routers/collections.py", "lineRange": [ 637, 664 ], "summary": "API REST — crée un sous-élément sous une page parente.", "tags": [ "api-handler", "sub-items", "crud" ], "complexity": "moderate" }, { "id": "function:app/routers/collections.py:aggregate_child_status", "type": "function", "name": "aggregate_child_status", "filePath": "app/routers/collections.py", "lineRange": [ 668, 688 ], "summary": "API REST — agrège le statut des enfants (compte done/total).", "tags": [ "api-handler", "aggregation", "status" ], "complexity": "moderate" }, { "id": "function:app/routers/collections.py:set_dependencies", "type": "function", "name": "set_dependencies", "filePath": "app/routers/collections.py", "lineRange": [ 692, 715 ], "summary": "API REST — définit les dépendances bloquantes d'une page.", "tags": [ "api-handler", "dependencies", "blocking" ], "complexity": "moderate" }, { "id": "function:app/routers/collections.py:check_dependencies", "type": "function", "name": "check_dependencies", "filePath": "app/routers/collections.py", "lineRange": [ 719, 760 ], "summary": "API REST — vérifie si une page peut transitionner selon ses dépendances.", "tags": [ "api-handler", "dependencies", "validation" ], "complexity": "complex" }, { "id": "function:app/routers/collections.py:view_collection", "type": "function", "name": "view_collection", "filePath": "app/routers/collections.py", "lineRange": [ 768, 791 ], "summary": "Rendu HTML d'une collection dans le type de vue demandé (table, calendar, gallery, etc.).", "tags": [ "api-handler", "views", "html-rendering" ], "complexity": "moderate" }, { "id": "function:app/routers/collections.py:_render_view", "type": "function", "name": "_render_view", "filePath": "app/routers/collections.py", "lineRange": [ 796, 806 ], "summary": "Dispatch de rendu — route vers le renderer approprié selon le type de vue.", "tags": [ "utility", "views", "rendering" ], "complexity": "simple" }, { "id": "function:app/routers/collections.py:_base_html", "type": "function", "name": "_base_html", "filePath": "app/routers/collections.py", "lineRange": [ 809, 829 ], "summary": "Template HTML de base avec en-tête, onglets de vue et styles CSS communs.", "tags": [ "utility", "html-rendering", "templating" ], "complexity": "moderate" }, { "id": "function:app/routers/collections.py:_render_calendar", "type": "function", "name": "_render_calendar", "filePath": "app/routers/collections.py", "lineRange": [ 832, 896 ], "summary": "Rendu calendrier — grille mensuelle avec navigation et pages positionnées par date.", "tags": [ "rendering", "calendar-view", "html" ], "complexity": "complex" }, { "id": "function:app/routers/collections.py:_render_gallery", "type": "function", "name": "_render_gallery", "filePath": "app/routers/collections.py", "lineRange": [ 899, 942 ], "summary": "Rendu galerie — cartes avec couverture optionnelle et propriétés affichées.", "tags": [ "rendering", "gallery-view", "html" ], "complexity": "moderate" }, { "id": "function:app/routers/collections.py:_render_list", "type": "function", "name": "_render_list", "filePath": "app/routers/collections.py", "lineRange": [ 945, 969 ], "summary": "Rendu liste — éléments avec icône, titre et aperçu des propriétés.", "tags": [ "rendering", "list-view", "html" ], "complexity": "moderate" }, { "id": "function:app/routers/collections.py:_render_timeline", "type": "function", "name": "_render_timeline", "filePath": "app/routers/collections.py", "lineRange": [ 972, 1028 ], "summary": "Rendu timeline — barres horizontales positionnées par plage de dates.", "tags": [ "rendering", "timeline-view", "html" ], "complexity": "complex" }, { "id": "function:app/routers/collections.py:_render_table", "type": "function", "name": "_render_table", "filePath": "app/routers/collections.py", "lineRange": [ 1031, 1047 ], "summary": "Rendu tableau — lignes avec icône, titre et cellules de propriétés.", "tags": [ "rendering", "table-view", "html" ], "complexity": "moderate" }, { "id": "function:app/routers/collections.py:get_collection_api", "type": "function", "name": "get_collection_api", "filePath": "app/routers/collections.py", "lineRange": [ 1051, 1073 ], "summary": "API REST — récupère une collection complète avec ses pages et vues.", "tags": [ "api-handler", "collections", "rest-api" ], "complexity": "moderate" }, { "id": "function:app/routers/collections.py:create_page_api", "type": "function", "name": "create_page_api", "filePath": "app/routers/collections.py", "lineRange": [ 1077, 1112 ], "summary": "API REST — crée une page dans une collection avec lien optionnel vers une issue Gitea.", "tags": [ "api-handler", "pages", "crud" ], "complexity": "moderate" }, { "id": "file:app/auth/__init__.py", "type": "file", "name": "__init__.py", "filePath": "app/auth/__init__.py", "summary": "Module d'initialisation du package d'authentification, ré-exporte GiteaOAuth, SessionManager et get_current_user comme API publique du module auth.", "tags": [ "entry-point", "barrel", "authentication" ], "complexity": "simple", "languageNotes": "Fichier barrel Python (__init__.py) servant de façade publique pour le package auth." }, { "id": "file:app/auth/oauth.py", "type": "file", "name": "oauth.py", "filePath": "app/auth/oauth.py", "summary": "Client OAuth2 pour l'authentification des utilisateurs via Gitea — gère la construction d'URL d'autorisation, l'échange de code contre token, et la récupération des informations utilisateur via l'API Gitea.", "tags": [ "authentication", "oauth2", "api-client", "service" ], "complexity": "moderate" }, { "id": "class:app/auth/oauth.py:GiteaOAuth", "type": "class", "name": "GiteaOAuth", "filePath": "app/auth/oauth.py", "lineRange": [ 14, 73 ], "summary": "Client OAuth2 Gitea implémentant le flux authorization code grant — construit l'URL d'autorisation, échange le code contre un token d'accès, et récupère les données utilisateur depuis l'API Gitea. Exposé en singleton via gitea_oauth.", "tags": [ "oauth2-client", "authentication", "api-client", "singleton" ], "complexity": "moderate" }, { "id": "file:app/config.py", "type": "file", "name": "config.py", "filePath": "app/config.py", "summary": "Configuration centrale de l'application via pydantic-settings — définit les paramètres Gitea (URL, token, OAuth, webhook), les paramètres d'application (secret key, host, port, langue), la base de données SQLite, le rate limiting et les intervalles de synchronisation.", "tags": [ "configuration", "settings", "pydantic", "entry-point" ], "complexity": "simple" }, { "id": "class:app/config.py:Settings", "type": "class", "name": "Settings", "filePath": "app/config.py", "lineRange": [ 8, 50 ], "summary": "Modèle de configuration Pydantic chargeant les variables d'environnement depuis un fichier .env — définit 17 champs de configuration (Gitea, OAuth, webhook, base de données, rate limiting, synchronisation) avec des valeurs par défaut et une propriété calculée db_path pour résoudre le chemin SQLite.", "tags": [ "pydantic-settings", "configuration", "environment" ], "complexity": "moderate", "languageNotes": "Utilise pydantic-settings pour le chargement automatique des variables d'environnement avec fichier .env." }, { "id": "file:app/routers/auth.py", "type": "file", "name": "auth.py", "filePath": "app/routers/auth.py", "summary": "Routeur FastAPI pour l'authentification — expose les endpoints de connexion OAuth2 (login avec redirection, callback de validation), déconnexion (logout avec suppression de cookie) et récupération de l'utilisateur courant, avec fallback vers un compte admin local quand OAuth est désactivé.", "tags": [ "api-handler", "authentication", "oauth2", "fastapi" ], "complexity": "moderate" }, { "id": "function:app/routers/auth.py:login", "type": "function", "name": "login", "filePath": "app/routers/auth.py", "lineRange": [ 19, 39 ], "summary": "Endpoint GET /auth/login — redirige vers la page d'autorisation OAuth2 Gitea avec un état anti-CSRF, ou crée une session admin locale quand OAuth est désactivé.", "tags": [ "api-endpoint", "authentication", "oauth2", "login" ], "complexity": "moderate" }, { "id": "function:app/routers/auth.py:callback", "type": "function", "name": "callback", "filePath": "app/routers/auth.py", "lineRange": [ 43, 92 ], "summary": "Endpoint GET /auth/callback — traite le retour OAuth2 Gitea : valide l'état anti-CSRF, échange le code contre un token d'accès, récupère les infos utilisateur, les persiste en base (UPSERT) et crée une session avec cookie.", "tags": [ "api-endpoint", "oauth2-callback", "authentication", "session" ], "complexity": "moderate" }, { "id": "function:app/routers/auth.py:logout", "type": "function", "name": "logout", "filePath": "app/routers/auth.py", "lineRange": [ 96, 100 ], "summary": "Endpoint GET /auth/logout — supprime le cookie de session flowdeck_session et redirige vers la page d'accueil.", "tags": [ "api-endpoint", "session", "authentication", "logout" ], "complexity": "simple" }, { "id": "function:app/routers/auth.py:current_user", "type": "function", "name": "current_user", "filePath": "app/routers/auth.py", "lineRange": [ 104, 110 ], "summary": "Endpoint GET /auth/user — retourne l'état d'authentification et les informations de l'utilisateur courant en JSON, ou {\"authenticated\": false} si non connecté.", "tags": [ "api-endpoint", "user", "authentication", "status" ], "complexity": "simple" }, { "id": "file:app/routers/webhooks.py", "type": "file", "name": "webhooks.py", "filePath": "app/routers/webhooks.py", "summary": "Routeur FastAPI pour la réception et la gestion des webhooks Gitea — vérifie les signatures HMAC SHA-256, traite les événements issues (création, fermeture, labels, suppression), pull requests (invalidation cache) et repository (vidage cache), avec endpoints d'enregistrement et de statut des webhooks.", "tags": [ "api-handler", "webhook", "event-handler", "gitea", "fastapi" ], "complexity": "moderate" }, { "id": "function:app/routers/webhooks.py:verify_signature", "type": "function", "name": "verify_signature", "filePath": "app/routers/webhooks.py", "lineRange": [ 19, 35 ], "summary": "Vérifie la signature HMAC SHA-256 des webhooks Gitea en comparant l'en-tête X-Gitea-Signature avec le hash du corps de la requête, avec bypass si aucun secret n'est configuré.", "tags": [ "security", "hmac", "webhook", "validation" ], "complexity": "simple" }, { "id": "function:app/routers/webhooks.py:receive_webhook", "type": "function", "name": "receive_webhook", "filePath": "app/routers/webhooks.py", "lineRange": [ 39, 56 ], "summary": "Endpoint POST /api/webhook — point d'entrée principal des webhooks Gitea qui vérifie la signature HMAC puis dispatche vers les handlers spécialisés selon le type d'événement (issues, pull_request, repository).", "tags": [ "api-endpoint", "webhook", "event-dispatcher" ], "complexity": "simple" }, { "id": "function:app/routers/webhooks.py:_handle_issue_event", "type": "function", "name": "_handle_issue_event", "filePath": "app/routers/webhooks.py", "lineRange": [ 59, 122 ], "summary": "Traite les événements webhook Gitea de type issues — ouverture/réouverture (crée une carte dans la colonne appropriée), fermeture (déplace vers la colonne Terminé), mise à jour de labels (recolonne selon le mapping), suppression (supprime la carte), avec persistance en base via le mapping colonnes/labels.", "tags": [ "event-handler", "issue", "webhook", "database", "kanban" ], "complexity": "moderate" }, { "id": "function:app/routers/webhooks.py:_handle_pr_event", "type": "function", "name": "_handle_pr_event", "filePath": "app/routers/webhooks.py", "lineRange": [ 125, 133 ], "summary": "Traite les événements webhook pull request Gitea en invalidant le cache des issues pour le dépôt concerné, forçant un rafraîchissement du tableau de bord.", "tags": [ "event-handler", "pull-request", "cache" ], "complexity": "simple" }, { "id": "function:app/routers/webhooks.py:_handle_repo_event", "type": "function", "name": "_handle_repo_event", "filePath": "app/routers/webhooks.py", "lineRange": [ 136, 140 ], "summary": "Traite les événements repository Gitea (création, suppression) en vidant le cache complet du client Gitea.", "tags": [ "event-handler", "repository", "cache" ], "complexity": "simple" }, { "id": "function:app/routers/webhooks.py:register_webhook", "type": "function", "name": "register_webhook", "filePath": "app/routers/webhooks.py", "lineRange": [ 144, 165 ], "summary": "Endpoint POST /api/webhook/register/{owner}/{repo} — enregistre un webhook sur un dépôt Gitea distant avec détection d'enregistrement existant pour éviter les doublons.", "tags": [ "api-endpoint", "webhook", "gitea", "registration" ], "complexity": "simple" }, { "id": "function:app/routers/webhooks.py:webhook_status", "type": "function", "name": "webhook_status", "filePath": "app/routers/webhooks.py", "lineRange": [ 169, 181 ], "summary": "Endpoint GET /api/webhook/status/{owner}/{repo} — vérifie si un webhook est déjà enregistré pour un dépôt Gitea donné en comparant les URLs.", "tags": [ "api-endpoint", "webhook", "status", "gitea" ], "complexity": "simple" }, { "id": "file:app/routers/dashboard.py", "type": "file", "name": "dashboard.py", "filePath": "app/routers/dashboard.py", "summary": "Tableau de bord principal — liste les projets Gitea avec recherche et filtrage, pages de gestion (trash, library, accounts) et rendu des pages Markdown avec contexte workspace.", "tags": [ "dashboard", "entry-point", "ui-rendering", "gitea-integration" ], "complexity": "moderate", "languageNotes": null }, { "id": "file:app/routers/workspace.py", "type": "file", "name": "workspace.py", "filePath": "app/routers/workspace.py", "summary": "Routeur workspace complet v2.0 — gestion des espaces de travail, membres, commentaires, historique, favoris, templates, import/export CSV, webhooks sortants et partage public.", "tags": [ "workspace", "collaboration", "crud", "templates", "webhooks" ], "complexity": "complex", "languageNotes": null }, { "id": "function:app/routers/dashboard.py:_sidebar_data", "type": "function", "name": "_sidebar_data", "filePath": "app/routers/dashboard.py", "lineRange": [ 17, 45 ], "summary": "Construit les données de la barre latérale — workspace, pages récentes, favoris.", "tags": [ "utility", "sidebar", "ui-data" ], "complexity": "moderate" }, { "id": "function:app/routers/dashboard.py:trash_page", "type": "function", "name": "trash_page", "filePath": "app/routers/dashboard.py", "lineRange": [ 49, 55 ], "summary": "Page HTML de la corbeille avec template Jinja2.", "tags": [ "ui-rendering", "pages", "html" ], "complexity": "simple" }, { "id": "function:app/routers/dashboard.py:library_page", "type": "function", "name": "library_page", "filePath": "app/routers/dashboard.py", "lineRange": [ 59, 68 ], "summary": "Page HTML de la bibliothèque listant les pages récentes et privées.", "tags": [ "ui-rendering", "pages", "html" ], "complexity": "simple" }, { "id": "function:app/routers/dashboard.py:view_page_root", "type": "function", "name": "view_page_root", "filePath": "app/routers/dashboard.py", "lineRange": [ 72, 94 ], "summary": "Rendu d'une page Markdown avec contexte workspace et sous-pages chargées.", "tags": [ "ui-rendering", "pages", "markdown" ], "complexity": "moderate" }, { "id": "function:app/routers/dashboard.py:accounts_page", "type": "function", "name": "accounts_page", "filePath": "app/routers/dashboard.py", "lineRange": [ 98, 108 ], "summary": "Panneau de gestion des comptes utilisateurs avec template Jinja2.", "tags": [ "ui-rendering", "accounts", "admin" ], "complexity": "moderate" }, { "id": "function:app/routers/dashboard.py:dashboard", "type": "function", "name": "dashboard", "filePath": "app/routers/dashboard.py", "lineRange": [ 112, 135 ], "summary": "Endpoint principal du dashboard — liste les repos Gitea avec recherche et filtrage.", "tags": [ "dashboard", "entry-point", "gitea-integration" ], "complexity": "moderate" }, { "id": "function:app/routers/workspace.py:_current_user", "type": "function", "name": "_current_user", "filePath": "app/routers/workspace.py", "lineRange": [ 21, 23 ], "summary": "Utilitaire interne — extrait l'utilisateur courant du cookie de session.", "tags": [ "utility", "authentication", "internal" ], "complexity": "simple" }, { "id": "function:app/routers/workspace.py:list_workspaces", "type": "function", "name": "list_workspaces", "filePath": "app/routers/workspace.py", "lineRange": [ 29, 32 ], "summary": "API REST — liste tous les espaces de travail.", "tags": [ "api-handler", "workspace", "rest-api" ], "complexity": "simple" }, { "id": "function:app/routers/workspace.py:create_workspace", "type": "function", "name": "create_workspace", "filePath": "app/routers/workspace.py", "lineRange": [ 36, 52 ], "summary": "API REST — crée un workspace avec son propriétaire comme admin.", "tags": [ "api-handler", "workspace", "crud" ], "complexity": "moderate" }, { "id": "function:app/routers/workspace.py:list_members", "type": "function", "name": "list_members", "filePath": "app/routers/workspace.py", "lineRange": [ 58, 64 ], "summary": "API REST — liste les membres d'un workspace avec leurs profils.", "tags": [ "api-handler", "members", "rest-api" ], "complexity": "simple" }, { "id": "function:app/routers/workspace.py:add_member", "type": "function", "name": "add_member", "filePath": "app/routers/workspace.py", "lineRange": [ 68, 80 ], "summary": "API REST — ajoute un membre à un workspace avec rôle.", "tags": [ "api-handler", "members", "crud" ], "complexity": "moderate" }, { "id": "function:app/routers/workspace.py:update_member_role", "type": "function", "name": "update_member_role", "filePath": "app/routers/workspace.py", "lineRange": [ 84, 93 ], "summary": "API REST — modifie le rôle d'un membre dans un workspace.", "tags": [ "api-handler", "members", "crud" ], "complexity": "simple" }, { "id": "function:app/routers/workspace.py:remove_member", "type": "function", "name": "remove_member", "filePath": "app/routers/workspace.py", "lineRange": [ 97, 101 ], "summary": "API REST — retire un membre d'un workspace.", "tags": [ "api-handler", "members", "crud" ], "complexity": "simple" }, { "id": "function:app/routers/workspace.py:list_comments", "type": "function", "name": "list_comments", "filePath": "app/routers/workspace.py", "lineRange": [ 107, 113 ], "summary": "API REST — liste les commentaires d'une page avec infos utilisateur.", "tags": [ "api-handler", "comments", "rest-api" ], "complexity": "simple" }, { "id": "function:app/routers/workspace.py:add_comment", "type": "function", "name": "add_comment", "filePath": "app/routers/workspace.py", "lineRange": [ 117, 131 ], "summary": "API REST — ajoute un commentaire (avec support de réponse via parent_id).", "tags": [ "api-handler", "comments", "crud" ], "complexity": "moderate" }, { "id": "function:app/routers/workspace.py:update_comment", "type": "function", "name": "update_comment", "filePath": "app/routers/workspace.py", "lineRange": [ 135, 145 ], "summary": "API REST — met à jour le corps ou le statut résolu d'un commentaire.", "tags": [ "api-handler", "comments", "crud" ], "complexity": "moderate" }, { "id": "function:app/routers/workspace.py:page_history", "type": "function", "name": "page_history", "filePath": "app/routers/workspace.py", "lineRange": [ 151, 157 ], "summary": "API REST — liste l'historique des modifications d'une page (50 dernières).", "tags": [ "api-handler", "history", "rest-api" ], "complexity": "simple" }, { "id": "function:app/routers/workspace.py:record_history", "type": "function", "name": "record_history", "filePath": "app/routers/workspace.py", "lineRange": [ 161, 172 ], "summary": "API REST — enregistre une entrée dans l'historique avec snapshot JSON.", "tags": [ "api-handler", "history", "crud" ], "complexity": "moderate" }, { "id": "function:app/routers/workspace.py:list_favorites", "type": "function", "name": "list_favorites", "filePath": "app/routers/workspace.py", "lineRange": [ 178, 190 ], "summary": "API REST — liste les favoris de l'utilisateur avec titres de pages/collections.", "tags": [ "api-handler", "favorites", "rest-api" ], "complexity": "moderate" }, { "id": "function:app/routers/workspace.py:add_favorite", "type": "function", "name": "add_favorite", "filePath": "app/routers/workspace.py", "lineRange": [ 194, 207 ], "summary": "API REST — ajoute une page ou collection aux favoris.", "tags": [ "api-handler", "favorites", "crud" ], "complexity": "simple" }, { "id": "function:app/routers/workspace.py:remove_favorite", "type": "function", "name": "remove_favorite", "filePath": "app/routers/workspace.py", "lineRange": [ 211, 215 ], "summary": "API REST — retire un favori.", "tags": [ "api-handler", "favorites", "crud" ], "complexity": "simple" }, { "id": "function:app/routers/workspace.py:list_db_templates", "type": "function", "name": "list_db_templates", "filePath": "app/routers/workspace.py", "lineRange": [ 221, 224 ], "summary": "API REST — liste les templates de base de données.", "tags": [ "api-handler", "templates", "rest-api" ], "complexity": "simple" }, { "id": "function:app/routers/workspace.py:create_db_template", "type": "function", "name": "create_db_template", "filePath": "app/routers/workspace.py", "lineRange": [ 228, 237 ], "summary": "API REST — crée un template de base de données avec schéma JSON.", "tags": [ "api-handler", "templates", "crud" ], "complexity": "moderate" }, { "id": "function:app/routers/workspace.py:apply_db_template", "type": "function", "name": "apply_db_template", "filePath": "app/routers/workspace.py", "lineRange": [ 241, 257 ], "summary": "API REST — applique un template pour créer une nouvelle collection.", "tags": [ "api-handler", "templates", "application" ], "complexity": "moderate" }, { "id": "function:app/routers/workspace.py:list_page_templates", "type": "function", "name": "list_page_templates", "filePath": "app/routers/workspace.py", "lineRange": [ 261, 266 ], "summary": "API REST — liste les templates de page pour une collection.", "tags": [ "api-handler", "templates", "rest-api" ], "complexity": "simple" }, { "id": "function:app/routers/workspace.py:create_page_template", "type": "function", "name": "create_page_template", "filePath": "app/routers/workspace.py", "lineRange": [ 270, 278 ], "summary": "API REST — crée un template de page avec propriétés par défaut.", "tags": [ "api-handler", "templates", "crud" ], "complexity": "simple" }, { "id": "function:app/routers/workspace.py:apply_page_template", "type": "function", "name": "apply_page_template", "filePath": "app/routers/workspace.py", "lineRange": [ 282, 296 ], "summary": "API REST — applique un template de page pour créer une nouvelle page.", "tags": [ "api-handler", "templates", "application" ], "complexity": "moderate" }, { "id": "function:app/routers/workspace.py:import_csv", "type": "function", "name": "import_csv", "filePath": "app/routers/workspace.py", "lineRange": [ 302, 324 ], "summary": "API REST — importe des données CSV dans une collection comme pages.", "tags": [ "api-handler", "import", "csv" ], "complexity": "moderate" }, { "id": "function:app/routers/workspace.py:export_csv", "type": "function", "name": "export_csv", "filePath": "app/routers/workspace.py", "lineRange": [ 328, 352 ], "summary": "API REST — exporte les pages d'une collection au format CSV avec StreamingResponse.", "tags": [ "api-handler", "export", "csv" ], "complexity": "moderate" }, { "id": "function:app/routers/workspace.py:list_webhooks", "type": "function", "name": "list_webhooks", "filePath": "app/routers/workspace.py", "lineRange": [ 358, 361 ], "summary": "API REST — liste les abonnements webhook sortants.", "tags": [ "api-handler", "webhooks", "rest-api" ], "complexity": "simple" }, { "id": "function:app/routers/workspace.py:create_webhook", "type": "function", "name": "create_webhook", "filePath": "app/routers/workspace.py", "lineRange": [ 365, 378 ], "summary": "API REST — enregistre un nouveau webhook sortant (URL, événement, secret).", "tags": [ "api-handler", "webhooks", "crud" ], "complexity": "moderate" }, { "id": "function:app/routers/workspace.py:delete_webhook", "type": "function", "name": "delete_webhook", "filePath": "app/routers/workspace.py", "lineRange": [ 382, 386 ], "summary": "API REST — supprime un abonnement webhook.", "tags": [ "api-handler", "webhooks", "crud" ], "complexity": "simple" }, { "id": "function:app/routers/workspace.py:public_view", "type": "function", "name": "public_view", "filePath": "app/routers/workspace.py", "lineRange": [ 392, 411 ], "summary": "Vue publique en lecture seule d'une collection sans authentification.", "tags": [ "api-handler", "public", "sharing" ], "complexity": "moderate" }, { "id": "file:app/auth/session.py", "type": "file", "name": "session.py", "filePath": "app/auth/session.py", "summary": "Gestionnaire de sessions utilisateur avec cookies signés via itsdangerous. Stocke et récupère les tokens OAuth Gitea dans SQLite.", "tags": [ "session", "authentication", "oauth", "security", "middleware" ], "complexity": "moderate", "languageNotes": "Utilise URLSafeTimedSerializer pour la signature/cryptage des cookies avec expiration de 7 jours." }, { "id": "class:app/auth/session.py:SessionManager", "type": "class", "name": "SessionManager", "filePath": "app/auth/session.py", "lineRange": [ 14, 58 ], "summary": "Gestionnaire statique de sessions — crée, décode, stocke et récupère les tokens OAuth Gitea via cookies signés et stockage SQLite.", "tags": [ "session", "authentication", "token-storage", "singleton" ], "complexity": "moderate" }, { "id": "function:app/auth/session.py:get_current_user", "type": "function", "name": "get_current_user", "filePath": "app/auth/session.py", "lineRange": [ 62, 68 ], "summary": "Dépendance FastAPI qui extrait l'utilisateur courant depuis le cookie de session signé.", "tags": [ "middleware", "authentication", "fastapi-dependency" ], "complexity": "simple" }, { "id": "file:app/services/gitea_client.py", "type": "file", "name": "gitea_client.py", "filePath": "app/services/gitea_client.py", "summary": "Client asynchrone pour l'API Gitea v1 avec cache TTL en mémoire. Fournit un singleton `gitea` couvrant les dépôts, issues, labels, milestones, webhooks et collaborateurs.", "tags": [ "service", "gitea-integration", "api-client", "caching" ], "complexity": "moderate", "languageNotes": "Client httpx asynchrone avec pattern singleton, cache TTL configurable et invalidation sélective par préfixe." }, { "id": "class:app/services/gitea_client.py:GiteaClient", "type": "class", "name": "GiteaClient", "filePath": "app/services/gitea_client.py", "lineRange": [ 15, 295 ], "summary": "Client asynchrone pour l'API Gitea v1 avec cache TTL intégré. Gère les opérations CRUD sur les dépôts, issues, labels, milestones, webhooks et collaborateurs. Le cache est invalidé automatiquement lors des mutations d'issues.", "tags": [ "api-client", "gitea-integration", "caching", "singleton" ], "complexity": "complex", "languageNotes": "Pattern singleton exposé via la variable module `gitea`, cache dict avec entrées (datetime, valeur), TTL via settings.gitea_cache_ttl." }, { "id": "function:app/services/gitea_client.py:get_user_repos", "type": "function", "name": "get_user_repos", "filePath": "app/services/gitea_client.py", "lineRange": [ 37, 52 ], "summary": "Liste les dépôts de l'utilisateur connecté avec pagination et cache TTL.", "tags": [ "repositories", "gitea-api", "cached" ], "complexity": "simple" }, { "id": "function:app/services/gitea_client.py:get_org_repos", "type": "function", "name": "get_org_repos", "filePath": "app/services/gitea_client.py", "lineRange": [ 54, 69 ], "summary": "Liste les dépôts d'une organisation Gitea avec pagination et cache TTL.", "tags": [ "repositories", "organizations", "gitea-api", "cached" ], "complexity": "simple" }, { "id": "function:app/services/gitea_client.py:get_user_orgs", "type": "function", "name": "get_user_orgs", "filePath": "app/services/gitea_client.py", "lineRange": [ 71, 84 ], "summary": "Liste les organisations auxquelles l'utilisateur appartient, avec cache TTL.", "tags": [ "organizations", "gitea-api", "cached" ], "complexity": "simple" }, { "id": "function:app/services/gitea_client.py:get_issues", "type": "function", "name": "get_issues", "filePath": "app/services/gitea_client.py", "lineRange": [ 88, 105 ], "summary": "Récupère les issues d'un dépôt avec filtrage par état et pagination, avec cache TTL.", "tags": [ "issues", "gitea-api", "cached" ], "complexity": "simple" }, { "id": "function:app/services/gitea_client.py:get_issue", "type": "function", "name": "get_issue", "filePath": "app/services/gitea_client.py", "lineRange": [ 107, 114 ], "summary": "Récupère le détail d'une issue spécifique par son ID (non caché).", "tags": [ "issues", "gitea-api" ], "complexity": "simple" }, { "id": "function:app/services/gitea_client.py:create_issue", "type": "function", "name": "create_issue", "filePath": "app/services/gitea_client.py", "lineRange": [ 116, 138 ], "summary": "Crée une nouvelle issue dans Gitea avec titre, corps, labels, milestone et assigné. Invalide le cache issues du dépôt.", "tags": [ "issues", "gitea-api", "mutation" ], "complexity": "moderate" }, { "id": "function:app/services/gitea_client.py:update_issue", "type": "function", "name": "update_issue", "filePath": "app/services/gitea_client.py", "lineRange": [ 140, 152 ], "summary": "Met à jour partiellement une issue (PATCH) avec kwargs dynamiques. Invalide le cache issues.", "tags": [ "issues", "gitea-api", "mutation" ], "complexity": "simple" }, { "id": "function:app/services/gitea_client.py:update_issue_labels", "type": "function", "name": "update_issue_labels", "filePath": "app/services/gitea_client.py", "lineRange": [ 154, 166 ], "summary": "Remplace tous les labels d'une issue (PUT) avec invalidation du cache issues.", "tags": [ "labels", "gitea-api", "mutation" ], "complexity": "simple" }, { "id": "function:app/services/gitea_client.py:get_issue_comments", "type": "function", "name": "get_issue_comments", "filePath": "app/services/gitea_client.py", "lineRange": [ 174, 184 ], "summary": "Récupère les commentaires d'une issue (non caché).", "tags": [ "comments", "issues", "gitea-api" ], "complexity": "simple" }, { "id": "function:app/services/gitea_client.py:get_labels", "type": "function", "name": "get_labels", "filePath": "app/services/gitea_client.py", "lineRange": [ 194, 208 ], "summary": "Récupère les labels d'un dépôt avec cache TTL.", "tags": [ "labels", "gitea-api", "cached" ], "complexity": "simple" }, { "id": "function:app/services/gitea_client.py:get_milestones", "type": "function", "name": "get_milestones", "filePath": "app/services/gitea_client.py", "lineRange": [ 212, 229 ], "summary": "Récupère les milestones d'un dépôt avec filtrage par état et cache TTL.", "tags": [ "milestones", "gitea-api", "cached" ], "complexity": "simple" }, { "id": "function:app/services/gitea_client.py:list_webhooks", "type": "function", "name": "list_webhooks", "filePath": "app/services/gitea_client.py", "lineRange": [ 233, 241 ], "summary": "Liste les webhooks configurés pour un dépôt.", "tags": [ "webhooks", "gitea-api" ], "complexity": "simple" }, { "id": "function:app/services/gitea_client.py:create_webhook", "type": "function", "name": "create_webhook", "filePath": "app/services/gitea_client.py", "lineRange": [ 243, 267 ], "summary": "Crée un webhook Gitea pour un dépôt avec URL, secret et événements sélectionnables (issues, PR, repository, commentaires par défaut).", "tags": [ "webhooks", "gitea-api", "mutation" ], "complexity": "moderate" }, { "id": "function:app/services/gitea_client.py:delete_webhook", "type": "function", "name": "delete_webhook", "filePath": "app/services/gitea_client.py", "lineRange": [ 269, 276 ], "summary": "Supprime un webhook par son ID. Retourne True si le statut HTTP est 204.", "tags": [ "webhooks", "gitea-api", "mutation" ], "complexity": "simple" }, { "id": "function:app/services/gitea_client.py:get_collaborators", "type": "function", "name": "get_collaborators", "filePath": "app/services/gitea_client.py", "lineRange": [ 280, 295 ], "summary": "Liste les collaborateurs d'un dépôt avec cache TTL.", "tags": [ "collaborators", "gitea-api", "cached" ], "complexity": "simple" }, { "id": "file:app/routers/api.py", "type": "file", "name": "api.py", "filePath": "app/routers/api.py", "summary": "API REST v1 pour FlowDeck — expose les endpoints de santé, statistiques, gestion des projets Gitea, CRUD des issues, checklists, profils utilisateur et métadonnées des cartes.", "tags": [ "api-handler", "entry-point", "rest", "gitea-integration" ], "complexity": "complex", "languageNotes": "FastAPI avec router préfixé /api, rate limiting in-memory, 22 fonctions de route." }, { "id": "file:app/routers/board.py", "type": "file", "name": "board.py", "filePath": "app/routers/board.py", "summary": "Router principal du tableau Kanban Notion-style avec vues multiples (kanban, table, statut, charge d'équipe, détaillée), gestion des pages Markdown, propriétés custom, mots-clés IA et synchronisation Gitea.", "tags": [ "api-handler", "kanban", "views", "pages", "notion-clone" ], "complexity": "complex", "languageNotes": "Routeur FastAPI préfixé /board avec 31 fonctions, rendu Jinja2 côté serveur, arbre de pages récursif." }, { "id": "function:app/routers/api.py:_check_rate_limit", "type": "function", "name": "_check_rate_limit", "filePath": "app/routers/api.py", "lineRange": [ 25, 38 ], "summary": "Limiteur de débit simple à fenêtre glissante en mémoire, par IP. Retourne True si la requête est autorisée.", "tags": [ "rate-limiting", "middleware", "security" ], "complexity": "simple" }, { "id": "function:app/routers/api.py:health", "type": "function", "name": "health", "filePath": "app/routers/api.py", "lineRange": [ 42, 63 ], "summary": "Endpoint de santé vérifiant la connectivité base de données et API Gitea, retourne le statut global (ok/dégradé).", "tags": [ "health-check", "endpoint", "monitoring" ], "complexity": "simple" }, { "id": "function:app/routers/api.py:stats", "type": "function", "name": "stats", "filePath": "app/routers/api.py", "lineRange": [ 67, 80 ], "summary": "Retourne les statistiques globales du dashboard : nombre de boards, cartes, notes et utilisateurs.", "tags": [ "statistics", "dashboard", "endpoint" ], "complexity": "simple" }, { "id": "function:app/routers/api.py:projects", "type": "function", "name": "projects", "filePath": "app/routers/api.py", "lineRange": [ 84, 96 ], "summary": "Liste les dépôts Gitea de l'utilisateur avec recherche textuelle et filtre d'archivage optionnel.", "tags": [ "gitea-integration", "search", "endpoint" ], "complexity": "simple" }, { "id": "function:app/routers/api.py:move_card", "type": "function", "name": "move_card", "filePath": "app/routers/api.py", "lineRange": [ 100, 166 ], "summary": "Déplace une carte vers une colonne, met à jour la position en base locale et synchronise les labels Gitea si un mapping de colonne existe.", "tags": [ "card-management", "gitea-sync", "endpoint" ], "complexity": "complex" }, { "id": "function:app/routers/api.py:_get_status_labels", "type": "function", "name": "_get_status_labels", "filePath": "app/routers/api.py", "lineRange": [ 169, 175 ], "summary": "Récupère tous les labels Gitea mappés aux colonnes d'un board donné.", "tags": [ "helper", "label-mapping", "database" ], "complexity": "simple" }, { "id": "function:app/routers/api.py:set_col_mapping", "type": "function", "name": "set_col_mapping", "filePath": "app/routers/api.py", "lineRange": [ 179, 205 ], "summary": "Définit ou met à jour le mapping entre une colonne Kanban et un label Gitea, avec option de fermeture automatique de l'issue.", "tags": [ "label-mapping", "configuration", "endpoint" ], "complexity": "moderate" }, { "id": "function:app/routers/api.py:delete_col_mapping", "type": "function", "name": "delete_col_mapping", "filePath": "app/routers/api.py", "lineRange": [ 209, 230 ], "summary": "Supprime un mapping colonne-label existant pour un board.", "tags": [ "label-mapping", "configuration", "endpoint" ], "complexity": "simple" }, { "id": "function:app/routers/api.py:get_board_config", "type": "function", "name": "get_board_config", "filePath": "app/routers/api.py", "lineRange": [ 234, 251 ], "summary": "Récupère la configuration complète d'un board : colonnes, limites WIP et mappings de labels.", "tags": [ "configuration", "board", "endpoint" ], "complexity": "moderate" }, { "id": "function:app/routers/api.py:update_board_config", "type": "function", "name": "update_board_config", "filePath": "app/routers/api.py", "lineRange": [ 255, 277 ], "summary": "Crée ou met à jour la configuration d'un board (colonnes JSON et limites WIP).", "tags": [ "configuration", "board", "endpoint" ], "complexity": "moderate" }, { "id": "function:app/routers/api.py:create_issue", "type": "function", "name": "create_issue", "filePath": "app/routers/api.py", "lineRange": [ 283, 320 ], "summary": "Crée une nouvelle issue dans Gitea et ajoute automatiquement une carte dans le board local avec la colonne déterminée par les labels.", "tags": [ "issue-crud", "gitea-integration", "endpoint" ], "complexity": "moderate" }, { "id": "function:app/routers/api.py:update_issue_api", "type": "function", "name": "update_issue_api", "filePath": "app/routers/api.py", "lineRange": [ 324, 378 ], "summary": "Met à jour partiellement une issue Gitea (titre, corps, état, labels, milestone, assigné) et recalcule la colonne de la carte associée.", "tags": [ "issue-crud", "gitea-integration", "endpoint" ], "complexity": "complex" }, { "id": "function:app/routers/api.py:get_issue_detail", "type": "function", "name": "get_issue_detail", "filePath": "app/routers/api.py", "lineRange": [ 385, 454 ], "summary": "Récupère le détail complet d'une issue (données, commentaires, checklists locaux) avec double format JSON et HTML pour les modales.", "tags": [ "issue-crud", "detail", "endpoint", "html-rendering" ], "complexity": "complex" }, { "id": "function:app/routers/api.py:create_checklist", "type": "function", "name": "create_checklist", "filePath": "app/routers/api.py", "lineRange": [ 460, 480 ], "summary": "Crée une nouvelle checklist associée à une issue dans la base locale.", "tags": [ "checklist", "endpoint", "database" ], "complexity": "simple" }, { "id": "function:app/routers/api.py:add_checklist_item", "type": "function", "name": "add_checklist_item", "filePath": "app/routers/api.py", "lineRange": [ 484, 498 ], "summary": "Ajoute un élément à une checklist existante.", "tags": [ "checklist", "endpoint", "database" ], "complexity": "simple" }, { "id": "function:app/routers/api.py:toggle_checklist_item", "type": "function", "name": "toggle_checklist_item", "filePath": "app/routers/api.py", "lineRange": [ 502, 520 ], "summary": "Bascule l'état coché d'un élément de checklist ou met à jour son contenu.", "tags": [ "checklist", "endpoint", "database" ], "complexity": "simple" }, { "id": "function:app/routers/api.py:delete_checklist_item", "type": "function", "name": "delete_checklist_item", "filePath": "app/routers/api.py", "lineRange": [ 524, 529 ], "summary": "Supprime un élément de checklist.", "tags": [ "checklist", "endpoint", "database" ], "complexity": "simple" }, { "id": "function:app/routers/api.py:delete_checklist", "type": "function", "name": "delete_checklist", "filePath": "app/routers/api.py", "lineRange": [ 533, 539 ], "summary": "Supprime une checklist et tous ses éléments associés.", "tags": [ "checklist", "endpoint", "database" ], "complexity": "simple" }, { "id": "function:app/routers/api.py:get_my_profile", "type": "function", "name": "get_my_profile", "filePath": "app/routers/api.py", "lineRange": [ 545, 556 ], "summary": "Récupère le profil de l'utilisateur connecté via le cookie de session.", "tags": [ "user-management", "profile", "endpoint" ], "complexity": "simple" }, { "id": "function:app/routers/api.py:update_my_profile", "type": "function", "name": "update_my_profile", "filePath": "app/routers/api.py", "lineRange": [ 560, 573 ], "summary": "Met à jour le nom complet et l'email de l'utilisateur connecté dans la base locale.", "tags": [ "user-management", "profile", "endpoint" ], "complexity": "simple" }, { "id": "function:app/routers/api.py:update_card", "type": "function", "name": "update_card", "filePath": "app/routers/api.py", "lineRange": [ 579, 613 ], "summary": "Met à jour les métadonnées d'une carte (priorité, date d'échéance) avec construction dynamique de la requête SQL.", "tags": [ "card-management", "endpoint", "database" ], "complexity": "moderate" }, { "id": "function:app/routers/api.py:get_collaborators", "type": "function", "name": "get_collaborators", "filePath": "app/routers/api.py", "lineRange": [ 619, 625 ], "summary": "Récupère la liste des collaborateurs d'un dépôt Gitea pour le sélecteur d'assignation.", "tags": [ "collaborators", "gitea-integration", "endpoint" ], "complexity": "simple" }, { "id": "function:app/routers/board.py:_issue_column", "type": "function", "name": "_issue_column", "filePath": "app/routers/board.py", "lineRange": [ 27, 38 ], "summary": "Détermine la colonne d'une issue en fonction de son état (fermée → dernière colonne) ou du mapping label→colonne.", "tags": [ "helper", "column-mapping", "label-mapping" ], "complexity": "simple" }, { "id": "function:app/routers/board.py:_map_issue_to_card", "type": "function", "name": "_map_issue_to_card", "filePath": "app/routers/board.py", "lineRange": [ 41, 115 ], "summary": "Transforme une issue Gitea en carte FlowDeck avec statut, icône, assigné, tag, propriétés custom et mots-clés IA.", "tags": [ "mapper", "gitea-integration", "card-model" ], "complexity": "complex" }, { "id": "function:app/routers/board.py:_build_page_tree", "type": "function", "name": "_build_page_tree", "filePath": "app/routers/board.py", "lineRange": [ 118, 140 ], "summary": "Construit récursivement l'arbre de pages avec profondeur maximale configurable pour éviter la récursion infinie.", "tags": [ "page-tree", "recursive", "helper" ], "complexity": "moderate" }, { "id": "function:app/routers/board.py:_sidebar_data", "type": "function", "name": "_sidebar_data", "filePath": "app/routers/board.py", "lineRange": [ 143, 176 ], "summary": "Prépare les données de la barre latérale : workspace, pages récentes/privées, vues du projet et informations utilisateur.", "tags": [ "sidebar", "navigation", "helper" ], "complexity": "moderate" }, { "id": "function:app/routers/board.py:_extract_ai_keywords", "type": "function", "name": "_extract_ai_keywords", "filePath": "app/routers/board.py", "lineRange": [ 179, 210 ], "summary": "Extrait et persiste les mots-clés IA à partir des labels d'issue et du corps (mots > 3 caractères), avec incrémentation du compteur d'usage.", "tags": [ "ai-keywords", "nlp", "extraction", "helper" ], "complexity": "moderate" }, { "id": "function:app/routers/board.py:_get_project_properties", "type": "function", "name": "_get_project_properties", "filePath": "app/routers/board.py", "lineRange": [ 213, 219 ], "summary": "Récupère la liste des propriétés custom définies pour un projet.", "tags": [ "properties", "database", "helper" ], "complexity": "simple" }, { "id": "function:app/routers/board.py:_get_dynamic_groups", "type": "function", "name": "_get_dynamic_groups", "filePath": "app/routers/board.py", "lineRange": [ 222, 230 ], "summary": "Retourne les groupes dynamiques à partir des labels Gitea avec fallback sur des groupes par défaut.", "tags": [ "groups", "dynamic", "helper" ], "complexity": "simple" }, { "id": "function:app/routers/board.py:_apply_filters", "type": "function", "name": "_apply_filters", "filePath": "app/routers/board.py", "lineRange": [ 237, 252 ], "summary": "Applique des filtres sur une liste de cartes : statut, assigné, tag et mot-clé.", "tags": [ "filtering", "cards", "helper" ], "complexity": "simple" }, { "id": "function:app/routers/board.py:_apply_sorts", "type": "function", "name": "_apply_sorts", "filePath": "app/routers/board.py", "lineRange": [ 255, 272 ], "summary": "Trie une liste de cartes selon les spécifications de tri (nom, statut, assigné, date limite) avec ordre ascendant/descendant.", "tags": [ "sorting", "cards", "helper" ], "complexity": "moderate" }, { "id": "function:app/routers/board.py:library_page", "type": "function", "name": "library_page", "filePath": "app/routers/board.py", "lineRange": [ 278, 289 ], "summary": "Affiche la page bibliothèque avec onglets Récents, Favoris, Partagés, Privés en rendu Jinja2.", "tags": [ "page", "library", "endpoint", "html-rendering" ], "complexity": "simple" }, { "id": "function:app/routers/board.py:list_trash", "type": "function", "name": "list_trash", "filePath": "app/routers/board.py", "lineRange": [ 294, 297 ], "summary": "Liste les pages dans la corbeille (soft-delete) triées par date de suppression décroissante.", "tags": [ "trash", "endpoint", "database" ], "complexity": "simple" }, { "id": "function:app/routers/board.py:restore_page", "type": "function", "name": "restore_page", "filePath": "app/routers/board.py", "lineRange": [ 301, 305 ], "summary": "Restaure une page supprimée en annulant le soft-delete.", "tags": [ "trash", "restore", "endpoint" ], "complexity": "simple" }, { "id": "function:app/routers/board.py:permanent_delete", "type": "function", "name": "permanent_delete", "filePath": "app/routers/board.py", "lineRange": [ 309, 314 ], "summary": "Supprime définitivement une page de la corbeille et réattribue ses enfants au niveau racine.", "tags": [ "trash", "delete", "endpoint" ], "complexity": "simple" }, { "id": "function:app/routers/board.py:trash_page", "type": "function", "name": "trash_page", "filePath": "app/routers/board.py", "lineRange": [ 318, 322 ], "summary": "Affiche la page corbeille en HTML via le template Jinja2 trash.html.", "tags": [ "trash", "page", "endpoint", "html-rendering" ], "complexity": "simple" }, { "id": "function:app/routers/board.py:board", "type": "function", "name": "board", "filePath": "app/routers/board.py", "lineRange": [ 328, 334 ], "summary": "Point d'entrée principal du tableau Kanban, rend le template board.html avec les données sidebar et la vue initiale.", "tags": [ "board", "entry-point", "endpoint", "html-rendering" ], "complexity": "simple" }, { "id": "function:app/routers/board.py:board_view", "type": "function", "name": "board_view", "filePath": "app/routers/board.py", "lineRange": [ 340, 411 ], "summary": "Fragment de vue dynamique (kanban, table, statut, charge équipe, détaillée) avec filtrage, tri et rendu du template approprié.", "tags": [ "views", "kanban", "endpoint", "html-rendering" ], "complexity": "complex" }, { "id": "function:app/routers/board.py:create_property", "type": "function", "name": "create_property", "filePath": "app/routers/board.py", "lineRange": [ 422, 435 ], "summary": "Crée une propriété custom pour un projet avec type (select, etc.) et options sérialisées en JSON.", "tags": [ "properties", "custom-fields", "endpoint" ], "complexity": "simple" }, { "id": "function:app/routers/board.py:delete_property", "type": "function", "name": "delete_property", "filePath": "app/routers/board.py", "lineRange": [ 439, 446 ], "summary": "Supprime une propriété custom d'un projet.", "tags": [ "properties", "custom-fields", "endpoint" ], "complexity": "simple" }, { "id": "function:app/routers/board.py:set_property_value", "type": "function", "name": "set_property_value", "filePath": "app/routers/board.py", "lineRange": [ 450, 464 ], "summary": "Définit ou met à jour la valeur d'une propriété custom pour une issue spécifique (INSERT OR REPLACE).", "tags": [ "properties", "custom-fields", "endpoint" ], "complexity": "simple" }, { "id": "function:app/routers/board.py:get_ai_keywords", "type": "function", "name": "get_ai_keywords", "filePath": "app/routers/board.py", "lineRange": [ 470, 476 ], "summary": "Récupère les 30 mots-clés IA les plus utilisés pour un projet, triés par compteur d'usage.", "tags": [ "ai-keywords", "endpoint", "database" ], "complexity": "simple" }, { "id": "function:app/routers/board.py:extract_ai_keywords", "type": "function", "name": "extract_ai_keywords", "filePath": "app/routers/board.py", "lineRange": [ 480, 489 ], "summary": "Déclenche la ré-extraction des mots-clés IA sur toutes les issues d'un dépôt Gitea.", "tags": [ "ai-keywords", "extraction", "endpoint" ], "complexity": "simple" }, { "id": "function:app/routers/board.py:create_page", "type": "function", "name": "create_page", "filePath": "app/routers/board.py", "lineRange": [ 495, 518 ], "summary": "Crée une nouvelle page Markdown avec calcul automatique de l'ordre de tri pour l'insertion dans l'arbre.", "tags": [ "pages", "crud", "endpoint" ], "complexity": "moderate" }, { "id": "function:app/routers/board.py:get_page", "type": "function", "name": "get_page", "filePath": "app/routers/board.py", "lineRange": [ 522, 528 ], "summary": "Récupère une page Markdown par son ID.", "tags": [ "pages", "crud", "endpoint" ], "complexity": "simple" }, { "id": "function:app/routers/board.py:update_page", "type": "function", "name": "update_page", "filePath": "app/routers/board.py", "lineRange": [ 532, 544 ], "summary": "Met à jour le titre, le contenu ou le format d'une page Markdown existante.", "tags": [ "pages", "crud", "endpoint" ], "complexity": "simple" }, { "id": "function:app/routers/board.py:save_page_blocks", "type": "function", "name": "save_page_blocks", "filePath": "app/routers/board.py", "lineRange": [ 548, 564 ], "summary": "Sauvegarde le contenu d'une page au format blocks JSON (éditeur Notion-style) avec titre optionnel.", "tags": [ "pages", "blocks", "notion-editor", "endpoint" ], "complexity": "moderate" }, { "id": "function:app/routers/board.py:move_page", "type": "function", "name": "move_page", "filePath": "app/routers/board.py", "lineRange": [ 568, 603 ], "summary": "Déplace et réordonne une page dans l'arbre (drag & drop) avec réindexation des pages voisines.", "tags": [ "pages", "drag-drop", "reorder", "endpoint" ], "complexity": "complex" }, { "id": "function:app/routers/board.py:delete_page", "type": "function", "name": "delete_page", "filePath": "app/routers/board.py", "lineRange": [ 607, 616 ], "summary": "Déplace une page vers la corbeille (soft delete) avec horodatage de suppression.", "tags": [ "pages", "soft-delete", "endpoint" ], "complexity": "simple" }, { "id": "function:app/routers/board.py:view_page", "type": "function", "name": "view_page", "filePath": "app/routers/board.py", "lineRange": [ 620, 631 ], "summary": "Affiche une page en HTML via le template page_editor.html avec les données sidebar.", "tags": [ "pages", "rendering", "endpoint", "html-rendering" ], "complexity": "simple" }, { "id": "function:app/routers/board.py:sync_project", "type": "function", "name": "sync_project", "filePath": "app/routers/board.py", "lineRange": [ 634, 658 ], "summary": "Synchronisation bidirectionnelle complète : récupère toutes les issues Gitea, met à jour les cartes locales et extrait les mots-clés IA.", "tags": [ "sync", "gitea-integration", "endpoint" ], "complexity": "moderate" }, { "id": "service:.dockerignore", "type": "service", "name": ".dockerignore", "filePath": ".dockerignore", "summary": "Fichier de configuration Docker qui exclut les caches Python, environnements virtuels, artefacts de build et fichiers de développement du contexte de build, optimisant la taille et la sécurité de l'image.", "tags": [ "containerization", "infrastructure", "build-system" ], "complexity": "simple" }, { "id": "service:docker-compose.yml", "type": "service", "name": "docker-compose.yml", "filePath": "docker-compose.yml", "summary": "Configuration Docker Compose orchestrant le service FlowDeck avec montage de volume persistant, injection de variables d'environnement et politique de redémarrage automatique.", "tags": [ "orchestration", "infrastructure", "containerization" ], "complexity": "simple" }, { "id": "service:docker-compose.yml:flowdeck", "type": "service", "name": "flowdeck", "filePath": "docker-compose.yml", "summary": "Service principal FlowDeck exposé sur le port 8080, avec volume de données nommé persistant et redémarrage automatique en cas d'arrêt.", "tags": [ "containerization", "orchestration", "web-service" ], "complexity": "simple" }, { "id": "service:Dockerfile", "type": "service", "name": "Dockerfile", "filePath": "Dockerfile", "summary": "Image Docker basée sur Python 3.12-slim pour FlowDeck, avec installation de curl, build des dépendances Python, healthcheck HTTP sur /api/health et exposition du port 8080.", "tags": [ "containerization", "infrastructure", "deployment" ], "complexity": "simple", "languageNotes": "Image Python slim optimisée avec healthcheck HTTP intégré (intervalle 30s, timeout 5s) et nettoyage des caches APT pour réduire la taille de l'image." }, { "id": "service:Dockerfile:python", "type": "service", "name": "python", "filePath": "Dockerfile", "summary": "Étage de base Python 3.12-slim configuré pour exécuter l'application FastAPI via Uvicorn avec healthcheck et exposition du port 8080.", "tags": [ "containerization", "web-service", "python" ], "complexity": "simple" }, { "id": "config:.env.example", "type": "config", "name": ".env.example", "filePath": ".env.example", "summary": "Fichier de configuration d'exemple définissant 13 variables d'environnement réparties en trois groupes : intégration Gitea (URL, token, OAuth, webhook), paramètres applicatifs (secret, hôte, port, log, langue), base de données (DATABASE_URL) et synchronisation (intervalle, cache TTL).", "tags": [ "configuration", "environnement", "variables", "gitea", "securite" ], "complexity": "simple" }, { "id": "document:ARCHITECTURE.md", "type": "document", "name": "ARCHITECTURE.md", "filePath": "ARCHITECTURE.md", "summary": "Document d'architecture complet en 15 chapitres couvrant l'ensemble du système FlowDeck : modèle de données (21 tables), routage API (85+ endpoints), frontend, authentification OAuth2 Gitea, système de propriétés avec formula/rollup engines, vues multiples, sub-items et dépendances, éditeur de blocs, multi-utilisateurs et déploiement Docker.", "tags": [ "documentation", "architecture", "reference", "technique" ], "complexity": "complex" }, { "id": "document:CHANGELOG.md", "type": "document", "name": "CHANGELOG.md", "filePath": "CHANGELOG.md", "summary": "Historique des versions de FlowDeck de v1.0.0 (Production) à v2.1.0 (Intégrations Avancées), détaillant les fonctionnalités ajoutées et les tests associés pour chaque release majeure.", "tags": [ "documentation", "changelog", "versions", "historique" ], "complexity": "moderate" }, { "id": "document:CONTRIBUTING.md", "type": "document", "name": "CONTRIBUTING.md", "filePath": "CONTRIBUTING.md", "summary": "Guide de contribution au projet FlowDeck en 6 sections couvrant le setup de développement, l'exécution des tests, les conventions de code, les références à l'architecture et la roadmap.", "tags": [ "documentation", "contribution", "developpement" ], "complexity": "simple" }, { "id": "document:README.md", "type": "document", "name": "README.md", "filePath": "README.md", "summary": "Documentation principale du projet FlowDeck — clone Notion intégré Gitea. Présente le quick start, les fonctionnalités par version (Database, Vues, Sub-items, My Tasks, Multi-User, API), la stack technique (Python/FastAPI), la configuration via variables d'environnement et l'exécution des tests.", "tags": [ "documentation", "entry-point", "presentation", "quick-start" ], "complexity": "moderate" }, { "id": "document:ROADMAP.md", "type": "document", "name": "ROADMAP.md", "filePath": "ROADMAP.md", "summary": "Feuille de route détaillée de FlowDeck listant chronologiquement les versions complétées (v0.2.0 à v2.0.0, 17 releases) avec leurs fonctionnalités, et les intégrations avancées planifiées pour v2.1.0. Inclut un résumé des phases.", "tags": [ "documentation", "roadmap", "planification", "versions" ], "complexity": "moderate" }, { "id": "document:TESTING.md", "type": "document", "name": "TESTING.md", "filePath": "TESTING.md", "summary": "Grille de tests visuels exhaustive en 14 sections couvrant : dashboard, board Kanban, drag & drop, création d'issues, vues, filtres et tri, notes, sidebar, workspaces multi-user, commentaires, favoris, webhooks/API, PWA/responsive et éditeur Notion-like. Inclut le résumé du dernier run et les bugs corrigés.", "tags": [ "documentation", "tests", "qualite", "qa" ], "complexity": "moderate" }, { "id": "document:WORKLOAD.md", "type": "document", "name": "WORKLOAD.md", "filePath": "WORKLOAD.md", "summary": "Document de suivi de charge présentant l'avancement global du projet, les blocs complétés et l'architecture technique synthétique : 21 tables, 11 routeurs, 6 services, 85+ endpoints, stack Python/FastAPI/JavaScript.", "tags": [ "documentation", "suivi", "metriques", "architecture" ], "complexity": "simple" }, { "id": "document:docs/NOTION_DATABASE_TASKS_GUIDE.md", "type": "document", "name": "NOTION_DATABASE_TASKS_GUIDE.md", "filePath": "docs/NOTION_DATABASE_TASKS_GUIDE.md", "summary": "Guide technique complet (822 lignes, 36 sections) détaillant l'architecture Database & Tasks de Notion et le plan d'implémentation par phases dans FlowDeck.", "tags": [ "documentation", "guide", "architecture", "database" ], "complexity": "complex" }, { "id": "file:app/templates/base.html", "type": "file", "name": "base.html", "filePath": "app/templates/base.html", "summary": "Template maître de l'application FlowDeck — définit le shell HTML complet avec sidebar Notion (sections Recents, Favorites, Agents, Shared, Private), barre de navigation supérieure, menu contextuel et la macro Jinja2 récursive `render_tree_item` pour l'arborescence des pages. Intègre HTMX, Alpine.js et SortableJS. Le composant Alpine `appState()` gère l'état de la sidebar, le drag-and-drop, le menu contextuel, les opérations CRUD sur les pages et le thème clair/sombre.", "tags": [ "layout", "entry-point", "sidebar", "jinja2", "alpinejs" ], "complexity": "complex", "languageNotes": "Utilise les macros Jinja2 récursives pour le rendu d'arborescence, les blocs `{% block %}` pour le contenu dynamique, et Alpine.js pour la réactivité côté client. Combine HTMX (navigation sans rechargement) avec Alpine.js pour les interactions UI riches." }, { "id": "file:app/templates/accounts.html", "type": "file", "name": "accounts.html", "filePath": "app/templates/accounts.html", "summary": "Page de gestion de compte utilisateur — permet d'éditer le profil (nom, email), de connecter/déconnecter un compte Gitea via OAuth, et d'afficher la liste des membres du workspace avec leurs rôles (Admin/Membre). Utilise Alpine.js `accountsData()` pour la sauvegarde du profil via PUT /api/users/me.", "tags": [ "account-management", "oauth", "user-profile", "workspace", "alpinejs" ], "complexity": "moderate" }, { "id": "file:app/templates/board.html", "type": "file", "name": "board.html", "filePath": "app/templates/board.html", "summary": "Page principale du tableau Kanban — affiche une image de couverture, le titre du repo, une description pliable, et des onglets de vue (Kanban, Detailed board, Table view, Status overview, Team Load) chargés dynamiquement via HTMX. Inclut une barre d'outils de filtrage (`filterSystem()`) et de tri (`sortSystem()`), un formulaire de création d'issue, et une modale pour les détails de carte.", "tags": [ "kanban", "board", "htmx", "alpinejs", "filtering" ], "complexity": "complex" }, { "id": "file:app/templates/board_fragment.html", "type": "file", "name": "board_fragment.html", "filePath": "app/templates/board_fragment.html", "summary": "Fragment HTMX du tableau Kanban — affiche les colonnes de statut (To-do, In progress, Complete) avec des groupes repliables, des cartes drag-and-drop compatibles SortableJS, et des boutons d'ajout de carte/groupe. Utilise Alpine.js `kanbanBoard()` pour la gestion d'état et ouvre la modale de détail carte via HTMX.", "tags": [ "kanban", "htmx-fragment", "sortablejs", "alpinejs", "drag-and-drop" ], "complexity": "simple" }, { "id": "file:app/templates/card.html", "type": "file", "name": "card.html", "filePath": "app/templates/card.html", "summary": "Partiel Jinja2 réutilisable pour le rendu d'une carte Kanban simple — affiche l'icône, le titre, l'avatar de l'assigné, le tag coloré et la date d'échéance. Composant purement présentiel sans logique JavaScript.", "tags": [ "partial", "kanban-card", "jinja2", "ui-component" ], "complexity": "simple" }, { "id": "file:app/templates/card_detail.html", "type": "file", "name": "card_detail.html", "filePath": "app/templates/card_detail.html", "summary": "Contenu de la modale de détail d'issue — affiche le titre éditable, les propriétés (statut, assigné, deadline, labels), la description, les checklists avec items cochables, et les commentaires. Utilise Alpine.js `cardDetail()` pour les mises à jour de champs, le toggle de checklist et les appels API vers /api/checklist-items et /api/checklists.", "tags": [ "modal", "issue-detail", "checklist", "comments", "alpinejs" ], "complexity": "moderate" }, { "id": "file:app/templates/dashboard.html", "type": "file", "name": "dashboard.html", "filePath": "app/templates/dashboard.html", "summary": "Page d'accueil listant les projets Gitea — affiche une barre de recherche avec HTMX (recherche asynchrone avec délai de 300ms), un toggle pour afficher les projets archivés, et la liste des repos sous forme de cartes Notion avec badge de langage et indicateur privé/public.", "tags": [ "dashboard", "projects", "htmx", "search", "gitea" ], "complexity": "simple" }, { "id": "file:app/templates/library.html", "type": "file", "name": "library.html", "filePath": "app/templates/library.html", "summary": "Page bibliothèque avec onglets (Recents, Favorites, Shared, Private) et un tableau listant toutes les pages avec colonnes : nom, auteur, source, dernière modification, dernière visite. Utilise une macro Jinja2 `page_row()` pour le rendu des lignes et un bouton New page.", "tags": [ "library", "pages", "table-view", "jinja2", "tabs" ], "complexity": "moderate" }, { "id": "file:app/templates/status_overview.html", "type": "file", "name": "status_overview.html", "filePath": "app/templates/status_overview.html", "summary": "Fragment HTMX affichant un diagramme donut SVG de style Notion — visualise la répartition des issues par statut (Todo, In Progress, Complete) avec des segments colorés en stroke-dasharray. Inclut une légende avec pourcentages calculés côté serveur Jinja2.", "tags": [ "chart", "donut", "svg", "htmx-fragment", "status" ], "complexity": "simple", "languageNotes": "Génère un diagramme donut SVG purement en Jinja2 avec calculs de stroke-dasharray et stroke-dashoffset côté serveur." }, { "id": "file:app/templates/team_load.html", "type": "file", "name": "team_load.html", "filePath": "app/templates/team_load.html", "summary": "Fragment HTMX affichant un graphique à barres empilées de la charge de travail par membre d'équipe — chaque barre montre la proportion de tâches Todo/Progress/Complete avec hauteur proportionnelle. Inclut l'avatar du membre et une légende. Utilise Alpine.js `teamLoad()` comme stub.", "tags": [ "chart", "team-workload", "stacked-bar", "htmx-fragment", "alpinejs" ], "complexity": "simple" }, { "id": "file:images/Guide-Technique-Complet-pour-Recr-er-l-diteur-Notion.html", "type": "file", "name": "Guide-Technique-Complet-pour-Recr-er-l-diteur-Notion.html", "filePath": "images/Guide-Technique-Complet-pour-Recr-er-l-diteur-Notion.html", "summary": "Guide technique complet (niveau Expert, 3 à 6 mois) détaillant l'architecture et l'implémentation pour recréer l'éditeur de blocs Notion — modèle de données, gestion des sélections, rendu collaboratif en temps réel et système de plugins.", "tags": [ "documentation", "guide-technique", "editeur", "reference-notion", "architecture" ], "complexity": "complex" }, { "id": "file:images/Guide-complet-de-d-veloppement-du-panneau-lat-ral-de-navigat.html", "type": "file", "name": "Guide-complet-de-d-veloppement-du-panneau-lat-ral-de-navigat.html", "filePath": "images/Guide-complet-de-d-veloppement-du-panneau-lat-ral-de-navigat.html", "summary": "Guide complet (niveau Avancé, 40 à 60 heures) couvrant le développement du panneau latéral de navigation style Notion — arborescence de pages, drag-and-drop, recherche et intégration avec le routeur.", "tags": [ "documentation", "guide-technique", "navigation", "panneau-lateral", "reference-notion" ], "complexity": "complex" }, { "id": "file:images/Guide-pour-d-velopper-un-clone-de-Notion-Bases-de-donn-es-T-.html", "type": "file", "name": "Guide-pour-d-velopper-un-clone-de-Notion-Bases-de-donn-es-T-.html", "filePath": "images/Guide-pour-d-velopper-un-clone-de-Notion-Bases-de-donn-es-T-.html", "summary": "Guide (niveau Avancé, 3 à 6 mois) décortiquant les mécanismes visuels et fonctionnels des bases de données Notion — vues tableau, calendrier, kanban, systèmes de filtres, tri et gestion des tâches.", "tags": [ "documentation", "guide-technique", "base-de-donnees", "taches", "reference-notion" ], "complexity": "complex" }, { "id": "pipeline:.gitea/workflows/ci.yml", "type": "pipeline", "name": "ci.yml", "filePath": ".gitea/workflows/ci.yml", "summary": "Workflow CI/CD Gitea exécutant les tests pytest et le linting flake8 à chaque push et pull request sur la branche main.", "tags": [ "ci-cd", "test", "lint", "automation" ], "complexity": "simple" }, { "id": "file:.ua/.understandignore", "type": "file", "name": ".understandignore", "filePath": ".ua/.understandignore", "summary": "Fichier de règles d'exclusion pour Understand-Anything, définissant les fichiers et dossiers à ignorer lors de l'analyse du projet.", "tags": [ "configuration", "ignore-rules", "tooling" ], "complexity": "simple" }, { "id": "config:.ua/config.json", "type": "config", "name": "config.json", "filePath": ".ua/config.json", "summary": "Configuration du projet pour Understand-Anything, spécifiant la langue de sortie des analyses.", "tags": [ "configuration", "tooling" ], "complexity": "simple" }, { "id": "file:app/middleware/__init__.py", "type": "file", "name": "__init__.py", "filePath": "app/middleware/__init__.py", "summary": "Fichier d'initialisation du package middleware, rendant le module importable.", "tags": [ "entry-point", "package-init" ], "complexity": "simple" }, { "id": "file:app/middleware/csrf.py", "type": "file", "name": "csrf.py", "filePath": "app/middleware/csrf.py", "summary": "Middleware ASGI de protection CSRF pour FastAPI, générant et validant des tokens pour sécuriser les requêtes POST/PUT/DELETE.", "tags": [ "middleware", "security", "csrf" ], "complexity": "moderate" }, { "id": "file:app/models/__init__.py", "type": "file", "name": "__init__.py", "filePath": "app/models/__init__.py", "summary": "Fichier d'initialisation du package models.", "tags": [ "entry-point", "package-init" ], "complexity": "simple" }, { "id": "file:app/schemas/__init__.py", "type": "file", "name": "__init__.py", "filePath": "app/schemas/__init__.py", "summary": "Fichier d'initialisation du package schemas.", "tags": [ "entry-point", "package-init" ], "complexity": "simple" }, { "id": "file:app/services/__init__.py", "type": "file", "name": "__init__.py", "filePath": "app/services/__init__.py", "summary": "Fichier d'initialisation du package services.", "tags": [ "entry-point", "package-init" ], "complexity": "simple" }, { "id": "file:app/services/formula_engine.py", "type": "file", "name": "formula_engine.py", "filePath": "app/services/formula_engine.py", "summary": "Moteur de formules compatible Notion avec 24 fonctions intégrées (if, concat, round, date_add, etc.) pour l'évaluation d'expressions dans les propriétés calculées.", "tags": [ "service", "formula-engine", "computation", "notion" ], "complexity": "complex" }, { "id": "file:app/services/property_types.py", "type": "file", "name": "property_types.py", "filePath": "app/services/property_types.py", "summary": "Définition des types de propriétés (texte, nombre, select, date, etc.) et fonctions de validation, auto-génération et formatage pour le système dynamique de FlowDeck.", "tags": [ "service", "validation", "property-types" ], "complexity": "complex" }, { "id": "file:images/Kanban board - Kanban board.url", "type": "file", "name": "Kanban board - Kanban board.url", "filePath": "images/Kanban board - Kanban board.url", "summary": "Fichier de raccourci URL pointant vers une ressource externe de tableau Kanban.", "tags": [ "url", "shortcut", "resource" ], "complexity": "simple" }, { "id": "file:static/css/app.css", "type": "file", "name": "app.css", "filePath": "static/css/app.css", "summary": "Feuille de style principale de FlowDeck (695 lignes, ~1900 lignes effectives) définissant l'apparence complète : layout, couleurs, animations et composants.", "tags": [ "styling", "css", "frontend", "layout" ], "complexity": "complex" }, { "id": "file:static/js/app.js", "type": "file", "name": "app.js", "filePath": "static/js/app.js", "summary": "Code JavaScript frontend gérant l'interactivité du tableau Kanban, le drag-and-drop via SortableJS, les interactions utilisateur et la communication API.", "tags": [ "frontend", "javascript", "kanban", "drag-and-drop" ], "complexity": "complex" }, { "id": "file:tests/test_app.py", "type": "file", "name": "test_app.py", "filePath": "tests/test_app.py", "summary": "Suite de tests exhaustive (74 fonctions) couvrant l'API REST, l'authentification, la protection CSRF, les collections, les propriétés, les vues, les sous-éléments, les dépendances et les webhooks.", "tags": [ "test", "integration", "api", "suite" ], "complexity": "complex" }, { "id": "class:app/middleware/csrf.py:CSRFMiddleware", "type": "class", "name": "CSRFMiddleware", "filePath": "app/middleware/csrf.py", "lineRange": [ 11, 49 ], "summary": "Middleware ASGI qui vérifie les tokens CSRF sur les requêtes mutantes (POST/PUT/DELETE), avec exclusion des chemins publics et renouvellement automatique du cookie.", "tags": [ "middleware", "security", "csrf-protection", "asgi" ], "complexity": "moderate" }, { "id": "class:app/services/formula_engine.py:FormulaEngine", "type": "class", "name": "FormulaEngine", "filePath": "app/services/formula_engine.py", "lineRange": [ 8, 223 ], "summary": "Moteur d'évaluation de formules reproduisant le comportement Notion avec 24 fonctions intégrées : if, concat, round, date_add, replace, join, opérateurs logiques, etc.", "tags": [ "formula-engine", "computation", "notion-compatible", "eval" ], "complexity": "complex", "languageNotes": "Parser d'expressions récursif avec gestion des parenthèses et arguments échappés par virgule" }, { "id": "function:app/services/property_types.py:validate_property_value", "type": "function", "name": "validate_property_value", "filePath": "app/services/property_types.py", "lineRange": [ 109, 149 ], "summary": "Valide une valeur de propriété selon son type (texte, nombre, date, select, multi-select, personne, URL, email, etc.).", "tags": [ "validation", "property-types", "type-checking" ], "complexity": "moderate" }, { "id": "function:app/services/property_types.py:get_auto_property_value", "type": "function", "name": "get_auto_property_value", "filePath": "app/services/property_types.py", "lineRange": [ 152, 160 ], "summary": "Génère une valeur automatique pour les propriétés de type date de création et utilisateur.", "tags": [ "auto-generation", "property-types", "date" ], "complexity": "simple" }, { "id": "function:app/services/property_types.py:get_next_unique_id", "type": "function", "name": "get_next_unique_id", "filePath": "app/services/property_types.py", "lineRange": [ 163, 170 ], "summary": "Génère le prochain ID unique incrémentiel pour les pages d'une collection via une requête SQL MAX+1.", "tags": [ "unique-id", "sql", "collection" ], "complexity": "simple" }, { "id": "function:app/services/property_types.py:format_number", "type": "function", "name": "format_number", "filePath": "app/services/property_types.py", "lineRange": [ 173, 187 ], "summary": "Formate un nombre selon un format spécifié (pourcentage, devise avec symbole, décimales personnalisées).", "tags": [ "formatting", "number", "display" ], "complexity": "simple" }, { "id": "function:tests/test_app.py:client", "type": "function", "name": "client", "filePath": "tests/test_app.py", "lineRange": [ 10, 26 ], "summary": "Fixture pytest configurant un client de test avec base de données temporaire, override de dépendances d'authentification et nettoyage après chaque test.", "tags": [ "test", "fixture", "setup" ], "complexity": "moderate" }, { "id": "function:tests/test_app.py:test_db_tables_exist", "type": "function", "name": "test_db_tables_exist", "filePath": "tests/test_app.py", "lineRange": [ 197, 211 ], "summary": "Vérifie que toutes les tables essentielles (projects, cards, activities, comments, etc.) existent dans la base de données.", "tags": [ "test", "database", "schema" ], "complexity": "simple" }, { "id": "function:tests/test_app.py:test_collections_api_crud", "type": "function", "name": "test_collections_api_crud", "filePath": "tests/test_app.py", "lineRange": [ 240, 287 ], "summary": "Teste le cycle CRUD complet de l'API collections : création, lecture, mise à jour du nom et suppression.", "tags": [ "test", "api", "crud", "collections" ], "complexity": "moderate" }, { "id": "function:tests/test_app.py:test_collections_pages_crud", "type": "function", "name": "test_collections_pages_crud", "filePath": "tests/test_app.py", "lineRange": [ 290, 331 ], "summary": "Teste le cycle CRUD des pages de collection : création avec propriétés, lecture, mise à jour et suppression.", "tags": [ "test", "api", "crud", "pages" ], "complexity": "moderate" }, { "id": "function:tests/test_app.py:test_collections_db_page_renders", "type": "function", "name": "test_collections_db_page_renders", "filePath": "tests/test_app.py", "lineRange": [ 341, 352 ], "summary": "Vérifie le rendu HTML de la page d'une collection de type database avec contenu attendu.", "tags": [ "test", "rendering", "html", "collection" ], "complexity": "simple" }, { "id": "function:tests/test_app.py:test_property_types_api", "type": "function", "name": "test_property_types_api", "filePath": "tests/test_app.py", "lineRange": [ 366, 375 ], "summary": "Teste l'endpoint API retournant la liste des types de propriétés disponibles.", "tags": [ "test", "api", "property-types" ], "complexity": "simple" }, { "id": "function:tests/test_app.py:test_collection_properties_crud", "type": "function", "name": "test_collection_properties_crud", "filePath": "tests/test_app.py", "lineRange": [ 378, 448 ], "summary": "Teste le cycle CRUD complet des propriétés de collection : création, liste, mise à jour du nom/type/options et suppression.", "tags": [ "test", "api", "crud", "properties" ], "complexity": "complex" }, { "id": "function:tests/test_app.py:test_collection_properties_duplicate", "type": "function", "name": "test_collection_properties_duplicate", "filePath": "tests/test_app.py", "lineRange": [ 451, 460 ], "summary": "Vérifie que la création d'une propriété avec un nom déjà existant est refusée.", "tags": [ "test", "validation", "duplicate" ], "complexity": "simple" }, { "id": "function:tests/test_app.py:test_collection_properties_validation", "type": "function", "name": "test_collection_properties_validation", "filePath": "tests/test_app.py", "lineRange": [ 463, 471 ], "summary": "Teste la validation des propriétés : rejet des noms vides et des types invalides.", "tags": [ "test", "validation", "properties" ], "complexity": "simple" }, { "id": "function:tests/test_app.py:test_create_relation_property", "type": "function", "name": "test_create_relation_property", "filePath": "tests/test_app.py", "lineRange": [ 476, 493 ], "summary": "Teste la création d'une propriété de type relation entre deux collections et vérifie sa configuration.", "tags": [ "test", "relation", "property-types" ], "complexity": "moderate" }, { "id": "function:tests/test_app.py:test_link_pages_via_relation", "type": "function", "name": "test_link_pages_via_relation", "filePath": "tests/test_app.py", "lineRange": [ 496, 525 ], "summary": "Teste la liaison de pages via une propriété de relation et la récupération des pages liées.", "tags": [ "test", "relation", "linking" ], "complexity": "moderate" }, { "id": "function:tests/test_app.py:test_rollup_compute", "type": "function", "name": "test_rollup_compute", "filePath": "tests/test_app.py", "lineRange": [ 528, 569 ], "summary": "Teste le calcul des rollups (agrégations count, sum, min, max, average) sur les propriétés des pages liées.", "tags": [ "test", "rollup", "aggregation" ], "complexity": "complex" }, { "id": "function:tests/test_app.py:test_formula_evaluate", "type": "function", "name": "test_formula_evaluate", "filePath": "tests/test_app.py", "lineRange": [ 572, 603 ], "summary": "Teste l'évaluation de formules via l'API avec différents contextes de propriétés et expressions imbriquées.", "tags": [ "test", "formula", "evaluation" ], "complexity": "moderate" }, { "id": "function:tests/test_app.py:test_views_calendar", "type": "function", "name": "test_views_calendar", "filePath": "tests/test_app.py", "lineRange": [ 614, 622 ], "summary": "Teste la création et le rendu HTML d'une vue calendrier avec propriété de date.", "tags": [ "test", "view", "calendar" ], "complexity": "simple" }, { "id": "function:tests/test_app.py:test_views_gallery", "type": "function", "name": "test_views_gallery", "filePath": "tests/test_app.py", "lineRange": [ 625, 633 ], "summary": "Teste la création et le rendu HTML d'une vue galerie avec carte de couverture.", "tags": [ "test", "view", "gallery" ], "complexity": "simple" }, { "id": "function:tests/test_app.py:test_views_list", "type": "function", "name": "test_views_list", "filePath": "tests/test_app.py", "lineRange": [ 636, 644 ], "summary": "Teste la création et le rendu HTML d'une vue liste.", "tags": [ "test", "view", "list" ], "complexity": "simple" }, { "id": "function:tests/test_app.py:test_views_timeline", "type": "function", "name": "test_views_timeline", "filePath": "tests/test_app.py", "lineRange": [ 647, 655 ], "summary": "Teste la création et le rendu HTML d'une vue timeline avec dates de début et fin.", "tags": [ "test", "view", "timeline" ], "complexity": "simple" }, { "id": "function:tests/test_app.py:test_view_config_update", "type": "function", "name": "test_view_config_update", "filePath": "tests/test_app.py", "lineRange": [ 680, 692 ], "summary": "Teste la mise à jour de la configuration d'une vue (filtres, tri, propriétés visibles).", "tags": [ "test", "view", "configuration" ], "complexity": "simple" }, { "id": "function:tests/test_app.py:test_save_view_as", "type": "function", "name": "test_save_view_as", "filePath": "tests/test_app.py", "lineRange": [ 695, 709 ], "summary": "Teste la fonctionnalité « Enregistrer sous » pour créer une nouvelle vue à partir d'une existante.", "tags": [ "test", "view", "save-as" ], "complexity": "simple" }, { "id": "function:tests/test_app.py:test_sub_items_crud", "type": "function", "name": "test_sub_items_crud", "filePath": "tests/test_app.py", "lineRange": [ 724, 742 ], "summary": "Teste le cycle CRUD des sous-éléments (sub-items) : création d'un enfant, liste et suppression.", "tags": [ "test", "sub-items", "crud" ], "complexity": "moderate" }, { "id": "function:tests/test_app.py:test_status_aggregate", "type": "function", "name": "test_status_aggregate", "filePath": "tests/test_app.py", "lineRange": [ 745, 761 ], "summary": "Teste l'agrégation des statuts des sous-éléments pour déterminer le statut global d'une page parent.", "tags": [ "test", "status", "aggregation" ], "complexity": "moderate" }, { "id": "function:tests/test_app.py:test_dependencies_check", "type": "function", "name": "test_dependencies_check", "filePath": "tests/test_app.py", "lineRange": [ 764, 782 ], "summary": "Teste la vérification des dépendances : une tâche avec dépendances non résolues est marquée comme bloquée.", "tags": [ "test", "dependencies", "blocking" ], "complexity": "moderate" }, { "id": "function:tests/test_app.py:test_dependencies_blocked", "type": "function", "name": "test_dependencies_blocked", "filePath": "tests/test_app.py", "lineRange": [ 785, 800 ], "summary": "Teste qu'une tâche dont toutes les dépendances sont résolues n'est plus marquée comme bloquée.", "tags": [ "test", "dependencies", "resolution" ], "complexity": "moderate" }, { "id": "function:tests/test_app.py:test_my_tasks_api", "type": "function", "name": "test_my_tasks_api", "filePath": "tests/test_app.py", "lineRange": [ 812, 824 ], "summary": "Teste l'API « My Tasks » retournant les tâches assignées à l'utilisateur courant avec leurs statuts.", "tags": [ "test", "api", "my-tasks" ], "complexity": "simple" }, { "id": "function:tests/test_app.py:test_workspace_crud", "type": "function", "name": "test_workspace_crud", "filePath": "tests/test_app.py", "lineRange": [ 839, 851 ], "summary": "Teste le cycle CRUD des workspaces : création, lecture, et liste des workspaces.", "tags": [ "test", "api", "workspace", "crud" ], "complexity": "simple" }, { "id": "function:tests/test_app.py:test_comments_crud", "type": "function", "name": "test_comments_crud", "filePath": "tests/test_app.py", "lineRange": [ 854, 867 ], "summary": "Teste le cycle CRUD des commentaires sur une page : ajout, liste et suppression.", "tags": [ "test", "api", "comments", "crud" ], "complexity": "simple" }, { "id": "function:tests/test_app.py:test_favorites_crud", "type": "function", "name": "test_favorites_crud", "filePath": "tests/test_app.py", "lineRange": [ 870, 883 ], "summary": "Teste le cycle CRUD des favoris : ajout d'une page aux favoris, liste et suppression.", "tags": [ "test", "api", "favorites", "crud" ], "complexity": "simple" }, { "id": "function:tests/test_app.py:test_csv_import_export", "type": "function", "name": "test_csv_import_export", "filePath": "tests/test_app.py", "lineRange": [ 886, 899 ], "summary": "Teste l'import et l'export CSV des pages d'une collection.", "tags": [ "test", "csv", "import", "export" ], "complexity": "simple" }, { "id": "function:tests/test_app.py:test_public_api_pages", "type": "function", "name": "test_public_api_pages", "filePath": "tests/test_app.py", "lineRange": [ 967, 975 ], "summary": "Teste l'accès à l'API publique pour lister les pages d'une collection avec une clé API valide.", "tags": [ "test", "public-api", "pages" ], "complexity": "simple" }, { "id": "function:tests/test_app.py:test_db_templates", "type": "function", "name": "test_db_templates", "filePath": "tests/test_app.py", "lineRange": [ 912, 924 ], "summary": "Teste les templates de base de données : création d'un template et création d'une page à partir de celui-ci.", "tags": [ "test", "templates", "database" ], "complexity": "simple" }, { "id": "function:tests/test_app.py:test_page_history", "type": "function", "name": "test_page_history", "filePath": "tests/test_app.py", "lineRange": [ 927, 938 ], "summary": "Teste l'historique des modifications d'une page : mise à jour du titre et vérification de l'entrée d'historique.", "tags": [ "test", "history", "audit" ], "complexity": "simple" }, { "id": "function:tests/test_app.py:test_webhooks_crud", "type": "function", "name": "test_webhooks_crud", "filePath": "tests/test_app.py", "lineRange": [ 978, 988 ], "summary": "Teste le cycle CRUD des webhooks : création, liste et suppression.", "tags": [ "test", "webhooks", "crud" ], "complexity": "simple" }, { "id": "file:app/routers/public_api.py", "type": "file", "name": "public_api.py", "summary": "API publique REST v2.1.0 : génération/vérification de tokens, endpoints CRUD pour pages et collections accessibles via Bearer token.", "tags": [ "api", "public-api", "authentication", "rest" ], "complexity": "moderate", "filePath": "app/routers/public_api.py" }, { "id": "file:app/services/collection_adapter.py", "type": "file", "name": "collection_adapter.py", "summary": "Adaptateur de compatibilité GiteaBoardCompat : conversion des boards Gitea legacy vers le modèle Collection, avec rétrocompatibilité totale.", "tags": [ "adapter", "compatibility", "collections", "gitea" ], "complexity": "complex", "filePath": "app/services/collection_adapter.py" }, { "id": "file:app/services/rollup_engine.py", "type": "file", "name": "rollup_engine.py", "summary": "Moteur d'agrégation RollupEngine : calcul de métriques (count, sum, average, min, max, etc.) sur les propriétés liées par relation.", "tags": [ "engine", "aggregation", "rollup", "properties" ], "complexity": "moderate", "filePath": "app/services/rollup_engine.py" }, { "id": "file:app/services/webhook_outbound.py", "type": "file", "name": "webhook_outbound.py", "summary": "Dispatcher de webhooks sortants : envoi asynchrone d'événements (page.created, collection.updated, etc.) aux abonnés enregistrés.", "tags": [ "webhook", "outbound", "events", "async" ], "complexity": "simple", "filePath": "app/services/webhook_outbound.py" } ], "edges": [ { "source": "file:app/routers/my_tasks.py", "target": "function:app/routers/my_tasks.py:_get_current_user", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/my_tasks.py", "target": "function:app/routers/my_tasks.py:my_tasks_dashboard", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/my_tasks.py", "target": "function:app/routers/my_tasks.py:my_tasks_api", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/notes.py", "target": "function:app/routers/notes.py:get_notes", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/notes.py", "target": "function:app/routers/notes.py:save_notes", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/my_tasks.py", "target": "function:app/routers/my_tasks.py:_get_current_user", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/my_tasks.py", "target": "function:app/routers/my_tasks.py:my_tasks_dashboard", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/my_tasks.py", "target": "function:app/routers/my_tasks.py:my_tasks_api", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/notes.py", "target": "function:app/routers/notes.py:get_notes", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/notes.py", "target": "function:app/routers/notes.py:save_notes", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/my_tasks.py:my_tasks_dashboard", "target": "function:app/db.py:get_conn", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/my_tasks.py:my_tasks_api", "target": "function:app/db.py:get_conn", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/notes.py:get_notes", "target": "function:app/db.py:get_conn", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/notes.py:save_notes", "target": "function:app/db.py:get_conn", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/my_tasks.py:_get_current_user", "target": "class:app/auth/session.py:SessionManager", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/notes.py:get_notes", "target": "class:app/auth/session.py:SessionManager", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/notes.py:save_notes", "target": "class:app/auth/session.py:SessionManager", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/my_tasks.py", "target": "file:app/db.py", "type": "depends_on", "direction": "forward", "weight": 0.6 }, { "source": "file:app/routers/notes.py", "target": "file:app/db.py", "type": "depends_on", "direction": "forward", "weight": 0.6 }, { "source": "file:app/routers/my_tasks.py", "target": "file:app/routers/collections.py", "type": "depends_on", "direction": "forward", "weight": 0.6 }, { "source": "file:app/routers/notes.py", "target": "file:app/routers/pages.py", "type": "depends_on", "direction": "forward", "weight": 0.6 }, { "source": "file:app/routers/pages.py", "target": "file:app/db.py", "type": "depends_on", "direction": "forward", "weight": 0.6 }, { "source": "file:app/routers/pages.py", "target": "file:app/routers/board.py", "type": "depends_on", "direction": "forward", "weight": 0.6 }, { "source": "file:app/routers/properties.py", "target": "file:app/db.py", "type": "depends_on", "direction": "forward", "weight": 0.6 }, { "source": "file:app/routers/pages.py", "target": "file:app/routers/collections.py", "type": "related", "direction": "forward", "weight": 0.5 }, { "source": "file:app/routers/properties.py", "target": "file:app/routers/collections.py", "type": "related", "direction": "forward", "weight": 0.5 }, { "source": "file:app/db.py", "target": "function:app/db.py:get_db_path", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/db.py", "target": "function:app/db.py:init_db", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/db.py", "target": "function:app/db.py:get_conn", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/main.py", "target": "function:app/main.py:lifespan", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/main.py", "target": "function:app/main.py:pwa_manifest", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:list_collections", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:list_collections_api", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:create_collection_api", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:update_collection_api", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:delete_collection_api", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:get_page_api", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:update_page_api", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:delete_page_api", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:list_boards_as_collections", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:get_board_as_collection", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:sync_board_to_collection", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:list_property_types_api", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:list_properties_api", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:create_property_api", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:update_property_api", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:delete_property_api", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:create_relation_property", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:link_pages", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:compute_rollup", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:evaluate_formula", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:get_view_api", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:update_view_config", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:save_view_as", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:list_views_api", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:list_sub_items", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:create_sub_item", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:aggregate_child_status", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:set_dependencies", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:check_dependencies", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:view_collection", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:_render_view", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:_base_html", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:_render_calendar", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:_render_gallery", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:_render_list", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:_render_timeline", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:_render_table", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:get_collection_api", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:create_page_api", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/db.py", "target": "function:app/db.py:get_db_path", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/db.py", "target": "function:app/db.py:init_db", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/db.py", "target": "function:app/db.py:get_conn", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/main.py", "target": "function:app/main.py:lifespan", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/main.py", "target": "function:app/main.py:pwa_manifest", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:list_collections", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:list_collections_api", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:create_collection_api", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:update_collection_api", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:delete_collection_api", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:get_page_api", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:update_page_api", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:delete_page_api", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:list_boards_as_collections", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:get_board_as_collection", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:sync_board_to_collection", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:list_property_types_api", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:list_properties_api", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:create_property_api", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:update_property_api", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:delete_property_api", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:create_relation_property", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:link_pages", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:compute_rollup", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:evaluate_formula", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:get_view_api", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:update_view_config", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:save_view_as", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:list_views_api", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:list_sub_items", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:create_sub_item", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:aggregate_child_status", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:set_dependencies", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:check_dependencies", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:view_collection", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:_render_view", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:_base_html", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:_render_calendar", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:_render_gallery", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:_render_list", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:_render_timeline", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:_render_table", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:get_collection_api", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "function:app/routers/collections.py:create_page_api", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "function:app/main.py:lifespan", "target": "function:app/db.py:init_db", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/main.py:lifespan", "target": "function:app/db.py:get_conn", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/collections.py:list_collections", "target": "function:app/db.py:get_conn", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/collections.py:list_collections_api", "target": "function:app/db.py:get_conn", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/collections.py:create_collection_api", "target": "function:app/db.py:get_conn", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/collections.py:update_collection_api", "target": "function:app/db.py:get_conn", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/collections.py:delete_collection_api", "target": "function:app/db.py:get_conn", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/collections.py:get_page_api", "target": "function:app/db.py:get_conn", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/collections.py:update_page_api", "target": "function:app/db.py:get_conn", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/collections.py:delete_page_api", "target": "function:app/db.py:get_conn", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/collections.py:list_properties_api", "target": "function:app/db.py:get_conn", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/collections.py:create_property_api", "target": "function:app/db.py:get_conn", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/collections.py:update_property_api", "target": "function:app/db.py:get_conn", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/collections.py:delete_property_api", "target": "function:app/db.py:get_conn", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/collections.py:create_relation_property", "target": "function:app/db.py:get_conn", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/collections.py:get_collection_api", "target": "function:app/db.py:get_conn", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/collections.py:create_page_api", "target": "function:app/db.py:get_conn", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "file:app/main.py", "target": "file:app/db.py", "type": "depends_on", "direction": "forward", "weight": 0.6 }, { "source": "file:app/routers/collections.py", "target": "file:app/db.py", "type": "depends_on", "direction": "forward", "weight": 0.6 }, { "source": "file:app/auth/oauth.py", "target": "file:app/config.py", "type": "imports", "direction": "forward", "weight": 0.7 }, { "source": "file:app/routers/auth.py", "target": "file:app/db.py", "type": "imports", "direction": "forward", "weight": 0.7 }, { "source": "file:app/routers/auth.py", "target": "file:app/auth/oauth.py", "type": "imports", "direction": "forward", "weight": 0.7 }, { "source": "file:app/routers/auth.py", "target": "file:app/auth/session.py", "type": "imports", "direction": "forward", "weight": 0.7 }, { "source": "file:app/routers/webhooks.py", "target": "file:app/db.py", "type": "imports", "direction": "forward", "weight": 0.7 }, { "source": "file:app/routers/webhooks.py", "target": "file:app/auth/session.py", "type": "imports", "direction": "forward", "weight": 0.7 }, { "source": "file:app/auth/oauth.py", "target": "class:app/auth/oauth.py:GiteaOAuth", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/config.py", "target": "class:app/config.py:Settings", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/auth.py", "target": "function:app/routers/auth.py:login", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/auth.py", "target": "function:app/routers/auth.py:callback", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/auth.py", "target": "function:app/routers/auth.py:logout", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/auth.py", "target": "function:app/routers/auth.py:current_user", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/webhooks.py", "target": "function:app/routers/webhooks.py:verify_signature", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/webhooks.py", "target": "function:app/routers/webhooks.py:receive_webhook", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/webhooks.py", "target": "function:app/routers/webhooks.py:_handle_issue_event", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/webhooks.py", "target": "function:app/routers/webhooks.py:_handle_pr_event", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/webhooks.py", "target": "function:app/routers/webhooks.py:_handle_repo_event", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/webhooks.py", "target": "function:app/routers/webhooks.py:register_webhook", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/webhooks.py", "target": "function:app/routers/webhooks.py:webhook_status", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/auth/oauth.py", "target": "class:app/auth/oauth.py:GiteaOAuth", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/config.py", "target": "class:app/config.py:Settings", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/auth.py", "target": "function:app/routers/auth.py:login", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/auth.py", "target": "function:app/routers/auth.py:callback", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/auth.py", "target": "function:app/routers/auth.py:logout", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/auth.py", "target": "function:app/routers/auth.py:current_user", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/webhooks.py", "target": "function:app/routers/webhooks.py:verify_signature", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/webhooks.py", "target": "function:app/routers/webhooks.py:receive_webhook", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/webhooks.py", "target": "function:app/routers/webhooks.py:_handle_issue_event", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/webhooks.py", "target": "function:app/routers/webhooks.py:_handle_pr_event", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/webhooks.py", "target": "function:app/routers/webhooks.py:_handle_repo_event", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/webhooks.py", "target": "function:app/routers/webhooks.py:register_webhook", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/webhooks.py", "target": "function:app/routers/webhooks.py:webhook_status", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/auth.py:login", "target": "function:app/db.py:get_conn", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/auth.py:login", "target": "class:app/auth/session.py:SessionManager", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/auth.py:login", "target": "class:app/auth/oauth.py:GiteaOAuth", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/auth.py:callback", "target": "class:app/auth/oauth.py:GiteaOAuth", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/auth.py:callback", "target": "function:app/db.py:get_conn", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/auth.py:callback", "target": "class:app/auth/session.py:SessionManager", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/auth.py:current_user", "target": "function:app/auth/session.py:get_current_user", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/webhooks.py:_handle_issue_event", "target": "function:app/db.py:get_conn", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/webhooks.py:_handle_issue_event", "target": "function:app/routers/board.py:_issue_column", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "file:app/auth/__init__.py", "target": "file:app/auth/oauth.py", "type": "depends_on", "direction": "forward", "weight": 0.6 }, { "source": "file:app/auth/__init__.py", "target": "file:app/auth/session.py", "type": "depends_on", "direction": "forward", "weight": 0.6 }, { "source": "file:app/auth/oauth.py", "target": "file:app/config.py", "type": "depends_on", "direction": "forward", "weight": 0.6 }, { "source": "file:app/routers/auth.py", "target": "file:app/config.py", "type": "depends_on", "direction": "forward", "weight": 0.6 }, { "source": "file:app/routers/webhooks.py", "target": "file:app/config.py", "type": "depends_on", "direction": "forward", "weight": 0.6 }, { "source": "file:app/routers/dashboard.py", "target": "function:app/routers/dashboard.py:_sidebar_data", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/dashboard.py", "target": "function:app/routers/dashboard.py:trash_page", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/dashboard.py", "target": "function:app/routers/dashboard.py:library_page", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/dashboard.py", "target": "function:app/routers/dashboard.py:view_page_root", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/dashboard.py", "target": "function:app/routers/dashboard.py:accounts_page", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/dashboard.py", "target": "function:app/routers/dashboard.py:dashboard", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:_current_user", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:list_workspaces", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:create_workspace", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:list_members", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:add_member", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:update_member_role", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:remove_member", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:list_comments", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:add_comment", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:update_comment", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:page_history", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:record_history", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:list_favorites", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:add_favorite", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:remove_favorite", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:list_db_templates", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:create_db_template", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:apply_db_template", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:list_page_templates", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:create_page_template", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:apply_page_template", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:import_csv", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:export_csv", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:list_webhooks", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:create_webhook", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:delete_webhook", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:public_view", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/dashboard.py", "target": "function:app/routers/dashboard.py:_sidebar_data", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/dashboard.py", "target": "function:app/routers/dashboard.py:trash_page", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/dashboard.py", "target": "function:app/routers/dashboard.py:library_page", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/dashboard.py", "target": "function:app/routers/dashboard.py:view_page_root", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/dashboard.py", "target": "function:app/routers/dashboard.py:accounts_page", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/dashboard.py", "target": "function:app/routers/dashboard.py:dashboard", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:_current_user", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:list_workspaces", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:create_workspace", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:list_members", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:add_member", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:update_member_role", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:remove_member", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:list_comments", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:add_comment", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:update_comment", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:page_history", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:record_history", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:list_favorites", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:add_favorite", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:remove_favorite", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:list_db_templates", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:create_db_template", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:apply_db_template", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:list_page_templates", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:create_page_template", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:apply_page_template", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:import_csv", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:export_csv", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:list_webhooks", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:create_webhook", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:delete_webhook", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/workspace.py", "target": "function:app/routers/workspace.py:public_view", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/dashboard.py:_sidebar_data", "target": "class:app/auth/session.py:SessionManager", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/dashboard.py:accounts_page", "target": "class:app/auth/session.py:SessionManager", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/workspace.py:_current_user", "target": "class:app/auth/session.py:SessionManager", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/dashboard.py", "target": "file:app/auth/session.py", "type": "depends_on", "direction": "forward", "weight": 0.6 }, { "source": "file:app/routers/workspace.py", "target": "file:app/auth/session.py", "type": "depends_on", "direction": "forward", "weight": 0.6 }, { "source": "file:app/routers/dashboard.py", "target": "file:app/routers/workspace.py", "type": "related", "direction": "forward", "weight": 0.5 }, { "source": "file:app/auth/session.py", "target": "class:app/auth/session.py:SessionManager", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/auth/session.py", "target": "function:app/auth/session.py:get_current_user", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/auth/session.py", "target": "class:app/auth/session.py:SessionManager", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/auth/session.py", "target": "function:app/auth/session.py:get_current_user", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "class:app/auth/session.py:SessionManager", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/collections.py", "target": "file:app/routers/workspace.py", "type": "related", "direction": "forward", "weight": 0.5 }, { "source": "file:app/services/gitea_client.py", "target": "class:app/services/gitea_client.py:GiteaClient", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/services/gitea_client.py", "target": "function:app/services/gitea_client.py:get_user_repos", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/services/gitea_client.py", "target": "function:app/services/gitea_client.py:get_org_repos", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/services/gitea_client.py", "target": "function:app/services/gitea_client.py:get_user_orgs", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/services/gitea_client.py", "target": "function:app/services/gitea_client.py:get_issues", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/services/gitea_client.py", "target": "function:app/services/gitea_client.py:get_issue", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/services/gitea_client.py", "target": "function:app/services/gitea_client.py:create_issue", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/services/gitea_client.py", "target": "function:app/services/gitea_client.py:update_issue", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/services/gitea_client.py", "target": "function:app/services/gitea_client.py:update_issue_labels", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/services/gitea_client.py", "target": "function:app/services/gitea_client.py:get_issue_comments", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/services/gitea_client.py", "target": "function:app/services/gitea_client.py:get_labels", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/services/gitea_client.py", "target": "function:app/services/gitea_client.py:get_milestones", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/services/gitea_client.py", "target": "function:app/services/gitea_client.py:list_webhooks", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/services/gitea_client.py", "target": "function:app/services/gitea_client.py:create_webhook", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/services/gitea_client.py", "target": "function:app/services/gitea_client.py:delete_webhook", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/services/gitea_client.py", "target": "function:app/services/gitea_client.py:get_collaborators", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/services/gitea_client.py", "target": "class:app/services/gitea_client.py:GiteaClient", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:_check_rate_limit", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:health", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:stats", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:projects", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:move_card", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:_get_status_labels", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:set_col_mapping", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:delete_col_mapping", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:get_board_config", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:update_board_config", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:create_issue", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:update_issue_api", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:get_issue_detail", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:create_checklist", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:add_checklist_item", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:toggle_checklist_item", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:delete_checklist_item", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:delete_checklist", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:get_my_profile", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:update_my_profile", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:update_card", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:get_collaborators", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:_issue_column", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:_map_issue_to_card", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:_build_page_tree", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:_sidebar_data", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:_extract_ai_keywords", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:_get_project_properties", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:_get_dynamic_groups", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:_apply_filters", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:_apply_sorts", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:library_page", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:list_trash", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:restore_page", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:permanent_delete", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:trash_page", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:board", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:board_view", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:create_property", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:delete_property", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:set_property_value", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:get_ai_keywords", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:extract_ai_keywords", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:create_page", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:get_page", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:update_page", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:save_page_blocks", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:move_page", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:delete_page", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:view_page", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:sync_project", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:_check_rate_limit", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:health", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:stats", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:projects", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:move_card", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:_get_status_labels", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:set_col_mapping", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:delete_col_mapping", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:get_board_config", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:update_board_config", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:create_issue", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:update_issue_api", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:get_issue_detail", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:create_checklist", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:add_checklist_item", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:toggle_checklist_item", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:delete_checklist_item", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:delete_checklist", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:get_my_profile", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:update_my_profile", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:update_card", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/api.py", "target": "function:app/routers/api.py:get_collaborators", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:_issue_column", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:_map_issue_to_card", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:_build_page_tree", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:_sidebar_data", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:_extract_ai_keywords", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:_get_project_properties", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:_get_dynamic_groups", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:_apply_filters", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:_apply_sorts", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:library_page", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:list_trash", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:restore_page", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:permanent_delete", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:trash_page", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:board", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:board_view", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:create_property", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:delete_property", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:set_property_value", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:get_ai_keywords", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:extract_ai_keywords", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:create_page", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:get_page", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:update_page", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:save_page_blocks", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:move_page", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:delete_page", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:view_page", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/routers/board.py", "target": "function:app/routers/board.py:sync_project", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/api.py:move_card", "target": "function:app/routers/api.py:_check_rate_limit", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/api.py:move_card", "target": "function:app/routers/api.py:_get_status_labels", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/api.py:create_issue", "target": "function:app/routers/api.py:_check_rate_limit", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/api.py:create_issue", "target": "function:app/routers/board.py:_issue_column", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/api.py:update_issue_api", "target": "function:app/routers/api.py:_check_rate_limit", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/api.py:update_issue_api", "target": "function:app/routers/board.py:_issue_column", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/api.py:get_issue_detail", "target": "function:app/routers/board.py:_map_issue_to_card", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/board.py:board_view", "target": "function:app/routers/board.py:_map_issue_to_card", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/board.py:board_view", "target": "function:app/routers/board.py:_apply_filters", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/board.py:board_view", "target": "function:app/routers/board.py:_apply_sorts", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/board.py:sync_project", "target": "function:app/routers/board.py:_issue_column", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/board.py:sync_project", "target": "function:app/routers/board.py:_extract_ai_keywords", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/board.py:extract_ai_keywords", "target": "function:app/routers/board.py:_extract_ai_keywords", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "function:app/routers/board.py:_sidebar_data", "target": "function:app/routers/board.py:_build_page_tree", "type": "calls", "direction": "forward", "weight": 0.8 }, { "source": "service:docker-compose.yml", "target": "service:Dockerfile", "type": "depends_on", "direction": "forward", "weight": 0.6 }, { "source": "service:docker-compose.yml", "target": "service:docker-compose.yml:flowdeck", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "service:Dockerfile", "target": "service:Dockerfile:python", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "service:Dockerfile", "target": "service:.dockerignore", "type": "related", "direction": "forward", "weight": 0.5 }, { "source": "config:.env.example", "target": "document:README.md", "type": "related", "direction": "forward", "weight": 0.5 }, { "source": "document:ARCHITECTURE.md", "target": "document:README.md", "type": "related", "direction": "forward", "weight": 0.5 }, { "source": "document:ARCHITECTURE.md", "target": "document:WORKLOAD.md", "type": "related", "direction": "forward", "weight": 0.5 }, { "source": "document:ARCHITECTURE.md", "target": "document:docs/NOTION_DATABASE_TASKS_GUIDE.md", "type": "related", "direction": "forward", "weight": 0.5 }, { "source": "document:CHANGELOG.md", "target": "document:ROADMAP.md", "type": "related", "direction": "forward", "weight": 0.5 }, { "source": "document:CHANGELOG.md", "target": "document:README.md", "type": "related", "direction": "forward", "weight": 0.5 }, { "source": "document:CONTRIBUTING.md", "target": "document:TESTING.md", "type": "related", "direction": "forward", "weight": 0.5 }, { "source": "document:CONTRIBUTING.md", "target": "document:ARCHITECTURE.md", "type": "related", "direction": "forward", "weight": 0.5 }, { "source": "document:CONTRIBUTING.md", "target": "document:ROADMAP.md", "type": "related", "direction": "forward", "weight": 0.5 }, { "source": "document:README.md", "target": "document:TESTING.md", "type": "related", "direction": "forward", "weight": 0.5 }, { "source": "document:README.md", "target": "document:ROADMAP.md", "type": "related", "direction": "forward", "weight": 0.5 }, { "source": "document:README.md", "target": "document:ARCHITECTURE.md", "type": "related", "direction": "forward", "weight": 0.5 }, { "source": "document:ROADMAP.md", "target": "document:CHANGELOG.md", "type": "related", "direction": "forward", "weight": 0.5 }, { "source": "document:WORKLOAD.md", "target": "document:ROADMAP.md", "type": "related", "direction": "forward", "weight": 0.5 }, { "source": "document:WORKLOAD.md", "target": "document:ARCHITECTURE.md", "type": "related", "direction": "forward", "weight": 0.5 }, { "source": "document:docs/NOTION_DATABASE_TASKS_GUIDE.md", "target": "document:ARCHITECTURE.md", "type": "related", "direction": "forward", "weight": 0.5 }, { "source": "document:docs/NOTION_DATABASE_TASKS_GUIDE.md", "target": "document:ROADMAP.md", "type": "related", "direction": "forward", "weight": 0.5 }, { "source": "file:app/templates/accounts.html", "target": "file:app/templates/base.html", "type": "depends_on", "direction": "forward", "weight": 0.6 }, { "source": "file:app/templates/board.html", "target": "file:app/templates/base.html", "type": "depends_on", "direction": "forward", "weight": 0.6 }, { "source": "file:app/templates/dashboard.html", "target": "file:app/templates/base.html", "type": "depends_on", "direction": "forward", "weight": 0.6 }, { "source": "file:app/templates/library.html", "target": "file:app/templates/base.html", "type": "depends_on", "direction": "forward", "weight": 0.6 }, { "source": "file:app/templates/board_fragment.html", "target": "file:app/templates/card.html", "type": "depends_on", "direction": "forward", "weight": 0.6 }, { "source": "file:app/templates/board.html", "target": "file:app/templates/board_fragment.html", "type": "depends_on", "direction": "forward", "weight": 0.6 }, { "source": "file:app/templates/board.html", "target": "file:app/templates/status_overview.html", "type": "depends_on", "direction": "forward", "weight": 0.6 }, { "source": "file:app/templates/board.html", "target": "file:app/templates/team_load.html", "type": "depends_on", "direction": "forward", "weight": 0.6 }, { "source": "file:app/templates/board.html", "target": "file:app/templates/card_detail.html", "type": "depends_on", "direction": "forward", "weight": 0.6 }, { "source": "file:app/templates/board_fragment.html", "target": "file:app/templates/card_detail.html", "type": "depends_on", "direction": "forward", "weight": 0.6 }, { "source": "file:images/Guide-Technique-Complet-pour-Recr-er-l-diteur-Notion.html", "target": "file:images/Guide-complet-de-d-veloppement-du-panneau-lat-ral-de-navigat.html", "type": "related", "direction": "forward", "weight": 0.5 }, { "source": "file:images/Guide-Technique-Complet-pour-Recr-er-l-diteur-Notion.html", "target": "file:images/Guide-pour-d-velopper-un-clone-de-Notion-Bases-de-donn-es-T-.html", "type": "related", "direction": "forward", "weight": 0.5 }, { "source": "file:images/Guide-complet-de-d-veloppement-du-panneau-lat-ral-de-navigat.html", "target": "file:images/Guide-pour-d-velopper-un-clone-de-Notion-Bases-de-donn-es-T-.html", "type": "related", "direction": "forward", "weight": 0.5 }, { "source": "file:app/middleware/csrf.py", "target": "class:app/middleware/csrf.py:CSRFMiddleware", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/middleware/csrf.py", "target": "class:app/middleware/csrf.py:CSRFMiddleware", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/services/formula_engine.py", "target": "class:app/services/formula_engine.py:FormulaEngine", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/services/formula_engine.py", "target": "class:app/services/formula_engine.py:FormulaEngine", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/services/property_types.py", "target": "function:app/services/property_types.py:validate_property_value", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/services/property_types.py", "target": "function:app/services/property_types.py:validate_property_value", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/services/property_types.py", "target": "function:app/services/property_types.py:get_auto_property_value", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/services/property_types.py", "target": "function:app/services/property_types.py:get_auto_property_value", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/services/property_types.py", "target": "function:app/services/property_types.py:get_next_unique_id", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/services/property_types.py", "target": "function:app/services/property_types.py:get_next_unique_id", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:app/services/property_types.py", "target": "function:app/services/property_types.py:format_number", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:app/services/property_types.py", "target": "function:app/services/property_types.py:format_number", "type": "exports", "direction": "forward", "weight": 0.8 }, { "source": "file:tests/test_app.py", "target": "function:tests/test_app.py:client", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:tests/test_app.py", "target": "function:tests/test_app.py:test_db_tables_exist", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:tests/test_app.py", "target": "function:tests/test_app.py:test_collections_api_crud", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:tests/test_app.py", "target": "function:tests/test_app.py:test_collections_pages_crud", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:tests/test_app.py", "target": "function:tests/test_app.py:test_collections_db_page_renders", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:tests/test_app.py", "target": "function:tests/test_app.py:test_property_types_api", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:tests/test_app.py", "target": "function:tests/test_app.py:test_collection_properties_crud", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:tests/test_app.py", "target": "function:tests/test_app.py:test_collection_properties_duplicate", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:tests/test_app.py", "target": "function:tests/test_app.py:test_collection_properties_validation", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:tests/test_app.py", "target": "function:tests/test_app.py:test_create_relation_property", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:tests/test_app.py", "target": "function:tests/test_app.py:test_link_pages_via_relation", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:tests/test_app.py", "target": "function:tests/test_app.py:test_rollup_compute", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:tests/test_app.py", "target": "function:tests/test_app.py:test_formula_evaluate", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:tests/test_app.py", "target": "function:tests/test_app.py:test_views_calendar", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:tests/test_app.py", "target": "function:tests/test_app.py:test_views_gallery", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:tests/test_app.py", "target": "function:tests/test_app.py:test_views_list", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:tests/test_app.py", "target": "function:tests/test_app.py:test_views_timeline", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:tests/test_app.py", "target": "function:tests/test_app.py:test_view_config_update", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:tests/test_app.py", "target": "function:tests/test_app.py:test_save_view_as", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:tests/test_app.py", "target": "function:tests/test_app.py:test_sub_items_crud", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:tests/test_app.py", "target": "function:tests/test_app.py:test_status_aggregate", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:tests/test_app.py", "target": "function:tests/test_app.py:test_dependencies_check", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:tests/test_app.py", "target": "function:tests/test_app.py:test_dependencies_blocked", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:tests/test_app.py", "target": "function:tests/test_app.py:test_my_tasks_api", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:tests/test_app.py", "target": "function:tests/test_app.py:test_workspace_crud", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:tests/test_app.py", "target": "function:tests/test_app.py:test_comments_crud", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:tests/test_app.py", "target": "function:tests/test_app.py:test_favorites_crud", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:tests/test_app.py", "target": "function:tests/test_app.py:test_csv_import_export", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:tests/test_app.py", "target": "function:tests/test_app.py:test_public_api_pages", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:tests/test_app.py", "target": "function:tests/test_app.py:test_db_templates", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:tests/test_app.py", "target": "function:tests/test_app.py:test_page_history", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "file:tests/test_app.py", "target": "function:tests/test_app.py:test_webhooks_crud", "type": "contains", "direction": "forward", "weight": 1.0 }, { "source": "pipeline:.gitea/workflows/ci.yml", "target": "file:tests/test_app.py", "type": "triggers", "direction": "forward", "weight": 0.6 }, { "source": "document:docs/NOTION_DATABASE_TASKS_GUIDE.md", "target": "file:app/services/formula_engine.py", "type": "documents", "direction": "forward", "weight": 0.5 }, { "source": "document:docs/NOTION_DATABASE_TASKS_GUIDE.md", "target": "file:app/services/property_types.py", "type": "documents", "direction": "forward", "weight": 0.5 }, { "source": "file:static/js/app.js", "target": "file:static/css/app.css", "type": "depends_on", "direction": "forward", "weight": 0.6 }, { "source": "file:app/auth/__init__.py", "target": "file:app/auth/oauth.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/auth/__init__.py", "target": "file:app/auth/session.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/auth/session.py", "target": "file:app/config.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/main.py", "target": "file:app/config.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/main.py", "target": "file:app/db.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/main.py", "target": "file:app/middleware/csrf.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/main.py", "target": "file:app/routers/__init__.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/main.py", "target": "file:app/routers/api.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/main.py", "target": "file:app/routers/auth.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/main.py", "target": "file:app/routers/board.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/main.py", "target": "file:app/routers/collections.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/main.py", "target": "file:app/routers/dashboard.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/main.py", "target": "file:app/routers/my_tasks.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/main.py", "target": "file:app/routers/notes.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/main.py", "target": "file:app/routers/webhooks.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/main.py", "target": "file:app/routers/workspace.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/main.py", "target": "file:app/services/gitea_client.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/middleware/__init__.py", "target": "file:app/middleware/csrf.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/routers/api.py", "target": "file:app/auth/session.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/routers/api.py", "target": "file:app/config.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/routers/api.py", "target": "file:app/db.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/routers/api.py", "target": "file:app/routers/board.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/routers/api.py", "target": "file:app/services/gitea_client.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/routers/auth.py", "target": "file:app/config.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/routers/board.py", "target": "file:app/auth/session.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/routers/board.py", "target": "file:app/db.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/routers/board.py", "target": "file:app/services/gitea_client.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/routers/collections.py", "target": "file:app/auth/session.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/routers/collections.py", "target": "file:app/db.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/routers/dashboard.py", "target": "file:app/auth/session.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/routers/dashboard.py", "target": "file:app/db.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/routers/dashboard.py", "target": "file:app/services/gitea_client.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/routers/my_tasks.py", "target": "file:app/auth/session.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/routers/my_tasks.py", "target": "file:app/db.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/routers/notes.py", "target": "file:app/db.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/routers/webhooks.py", "target": "file:app/config.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/routers/webhooks.py", "target": "file:app/routers/board.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/routers/workspace.py", "target": "file:app/auth/session.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/routers/workspace.py", "target": "file:app/db.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/services/gitea_client.py", "target": "file:app/config.py", "type": "imports", "direction": "forward", "weight": 0.7, "recoveredFromImportMap": true }, { "source": "file:app/main.py", "target": "file:app/routers/public_api.py", "type": "imports", "direction": "forward", "weight": 0.7 }, { "source": "file:app/main.py", "target": "file:app/services/webhook_outbound.py", "type": "imports", "direction": "forward", "weight": 0.7 }, { "source": "file:app/routers/public_api.py", "target": "file:app/db.py", "type": "imports", "direction": "forward", "weight": 0.7 }, { "source": "file:app/services/collection_adapter.py", "target": "file:app/db.py", "type": "imports", "direction": "forward", "weight": 0.7 }, { "source": "file:app/services/rollup_engine.py", "target": "file:app/db.py", "type": "imports", "direction": "forward", "weight": 0.7 }, { "source": "file:app/services/webhook_outbound.py", "target": "file:app/db.py", "type": "imports", "direction": "forward", "weight": 0.7 } ], "layers": [ { "id": "layer:api", "name": "Couche API", "description": "Routeurs FastAPI exposant les endpoints HTTP REST : tableaux Kanban, collections, tableaux de bord, webhooks, authentification, API publique et espaces de travail.", "nodeIds": [ "file:app/routers/__init__.py", "file:app/routers/api.py", "file:app/routers/auth.py", "file:app/routers/board.py", "file:app/routers/collections.py", "file:app/routers/dashboard.py", "file:app/routers/my_tasks.py", "file:app/routers/notes.py", "file:app/routers/pages.py", "file:app/routers/properties.py", "file:app/routers/public_api.py", "file:app/routers/webhooks.py", "file:app/routers/workspace.py" ] }, { "id": "layer:service", "name": "Couche Services", "description": "Logique métier centrale : client Gitea avec cache, moteur de formules compatible Notion, types de propriétés, agrégation rollup, webhooks sortants, adaptateur de compatibilité, OAuth et gestion des sessions.", "nodeIds": [ "file:app/services/__init__.py", "file:app/services/collection_adapter.py", "file:app/services/formula_engine.py", "file:app/services/gitea_client.py", "file:app/services/property_types.py", "file:app/services/rollup_engine.py", "file:app/services/webhook_outbound.py", "file:app/auth/oauth.py", "file:app/auth/session.py" ] }, { "id": "layer:data", "name": "Couche Données", "description": "Schéma SQLite, modèles de données et définition des schémas Pydantic pour la persistance des collections, pages et propriétés.", "nodeIds": [ "file:app/db.py", "file:app/models/__init__.py", "file:app/schemas/__init__.py" ] }, { "id": "layer:middleware", "name": "Couche Intermédiaire", "description": "Middleware ASGI pour FastAPI : protection CSRF par tokens et initialisation du package.", "nodeIds": [ "file:app/middleware/__init__.py", "file:app/middleware/csrf.py" ] }, { "id": "layer:presentation", "name": "Couche Présentation", "description": "Templates Jinja2 (pages HTML rendues serveur) et ressources statiques (CSS Tailwind, JavaScript vanilla pour l'interactivité Kanban et le drag-and-drop).", "nodeIds": [ "file:app/templates/accounts.html", "file:app/templates/base.html", "file:app/templates/board.html", "file:app/templates/board_fragment.html", "file:app/templates/card.html", "file:app/templates/card_detail.html", "file:app/templates/dashboard.html", "file:app/templates/library.html", "file:app/templates/status_overview.html", "file:app/templates/team_load.html", "file:static/css/app.css", "file:static/js/app.js" ] }, { "id": "layer:config", "name": "Couche Configuration", "description": "Point d'entrée FastAPI (factory, lifespan, montage des routeurs), configuration centralisée pydantic-settings, initialisation des packages, variables d'environnement et configuration de l'outil d'analyse.", "nodeIds": [ "file:app/__init__.py", "file:app/main.py", "file:app/config.py", "file:app/auth/__init__.py", "config:.env.example", "config:.ua/config.json", "file:.ua/.understandignore" ] }, { "id": "layer:infrastructure", "name": "Infrastructure", "description": "Conteneurisation Docker (image Python 3.12-slim, orchestration Compose, .dockerignore) et pipeline CI/CD Gitea pour tests pytest et linting flake8.", "nodeIds": [ "service:Dockerfile", "service:Dockerfile:python", "service:docker-compose.yml", "service:docker-compose.yml:flowdeck", "service:.dockerignore", "pipeline:.gitea/workflows/ci.yml" ] }, { "id": "layer:documentation", "name": "Documentation", "description": "Documentation projet (README, architecture, changelog, contribution, roadmap, tests, charge de travail), guide technique Notion et références HTML de développement.", "nodeIds": [ "document:README.md", "document:ARCHITECTURE.md", "document:CHANGELOG.md", "document:CONTRIBUTING.md", "document:ROADMAP.md", "document:TESTING.md", "document:WORKLOAD.md", "document:docs/NOTION_DATABASE_TASKS_GUIDE.md", "file:images/Guide-Technique-Complet-pour-Recr-er-l-diteur-Notion.html", "file:images/Guide-complet-de-d-veloppement-du-panneau-lat-ral-de-navigat.html", "file:images/Guide-pour-d-velopper-un-clone-de-Notion-Bases-de-donn-es-T-.html", "file:images/Kanban board - Kanban board.url" ] }, { "id": "layer:test", "name": "Tests", "description": "Suite de tests exhaustive (74 fonctions) couvrant l'API REST, l'authentification OAuth, les collections et les webhooks via pytest.", "nodeIds": [ "file:tests/test_app.py" ] } ], "tour": [ { "order": 1, "title": "Vue d'ensemble du projet", "description": "FlowDeck est un clone complet de Notion intégré nativement à Gitea. Ce fichier présente le quick start, les fonctionnalités par version (Databases, Vues Kanban/Calendar/Gallery, Sub-items, My Tasks, Multi-User, API publique v2.1), la stack technique (Python 3.12, FastAPI, SQLite, Jinja2, Tailwind CSS) et la configuration via variables d'environnement. C'est le point de départ idéal pour comprendre ce que fait le projet avant de plonger dans son architecture.", "nodeIds": [ "document:README.md" ] }, { "order": 2, "title": "Point d'entrée de l'application", "description": "Le fichier main.py est le point d'entrée FastAPI de FlowDeck. Il configure le gestionnaire de cycle de vie (lifespan) qui initialise la base de données au démarrage, monte tous les routeurs (API, board, collections, auth, webhooks, workspace, dashboard, my_tasks, notes), active le middleware CSRF, gère les sessions utilisateur via des cookies signés, et sert le manifeste PWA pour l'installation en tant qu'application web progressive.", "nodeIds": [ "file:app/main.py" ], "languageLesson": "Le pattern lifespan de FastAPI (gestionnaire asynchrone avec yield) permet d'exécuter du code d'initialisation avant que l'application ne commence à servir des requêtes, et du code de nettoyage à l'arrêt. C'est l'équivalent moderne des événements startup/shutdown de Starlette." }, { "order": 3, "title": "Base de données et configuration", "description": "Le module db.py définit le schéma SQLite complet de FlowDeck avec 21 tables : collections, pages, propriétés, vues, webhooks, notes, historique, commentaires, favoris, templates et workspaces. La fonction get_conn — le nœud le plus utilisé de toute l'application avec 25 dépendances entrantes — fournit une connexion SQLite avec row_factory configurée pour retourner des dictionnaires. Le fichier .env.example documente les 13 variables d'environnement nécessaires pour l'intégration Gitea, la sécurité et la persistance.", "nodeIds": [ "file:app/db.py", "config:.env.example" ], "languageLesson": "SQLite avec row_factory=sqlite3.Row permet d'accéder aux colonnes par nom plutôt que par index, rendant le code plus lisible. L'activation des clés étrangères (PRAGMA foreign_keys=ON) et du mode WAL (Write-Ahead Logging) améliore respectivement l'intégrité référentielle et les performances en écriture concurrente." }, { "order": 4, "title": "Authentification OAuth2 Gitea", "description": "FlowDeck utilise l'authentification OAuth2 de Gitea pour identifier les utilisateurs. Le module session.py gère les sessions persistantes dans la base de données avec des cookies signés, tandis que oauth.py implémente le flux OAuth2 complet (redirection vers Gitea, échange du code d'autorisation, récupération du token). La classe SessionManager — 11 dépendances entrantes — est le point central d'accès à l'identité de l'utilisateur dans tous les routeurs.", "nodeIds": [ "file:app/auth/session.py", "file:app/auth/oauth.py" ], "languageLesson": "Le flux OAuth2 Authorization Code Grant est le standard pour les applications web : le serveur redirige l'utilisateur vers le fournisseur d'identité (Gitea), qui renvoie un code d'autorisation à usage unique. Ce code est échangé côté serveur contre un token d'accès, sans jamais exposer le token au navigateur." }, { "order": 5, "title": "Cœur du système : Collections Notion-style", "description": "Le routeur collections.py est le fichier le plus vaste de FlowDeck avec 39 endpoints et 83 dépendances sortantes. Il implémente le CRUD complet des collections, des pages, des propriétés (21 types dont relations et rollups), des vues (table, board, calendar, gallery, list, timeline), des sous-éléments et des dépendances. Ce module est le cœur de l'abstraction « base de données Notion » — chaque collection est un conteneur de pages avec un schéma de propriétés dynamique et des vues configurables.", "nodeIds": [ "file:app/routers/collections.py" ], "languageLesson": "Les routeurs FastAPI utilisent APIRouter pour regrouper les endpoints par domaine fonctionnel. Le préfixe d'URL et les tags sont définis au niveau du routeur, ce qui permet de les monter sur l'application principale avec app.include_router()." }, { "order": 6, "title": "Moteurs de calcul : Propriétés, Formules et Rollups", "description": "Les trois moteurs de services définissent la puissance du modèle de données de FlowDeck. property_types.py implémente les 21 types de propriétés avec validation et formatage ; formula_engine.py évalue des expressions avec 24 fonctions intégrées (if, concat, round, date_add, etc.) pour les colonnes calculées ; rollup_engine.py agrège les données des collections liées via 12 opérations (sum, average, count, min, max, etc.). Ces moteurs transforment FlowDeck d'un simple tableur en une véritable base de données relationnelle.", "nodeIds": [ "file:app/services/property_types.py", "file:app/services/formula_engine.py", "file:app/services/rollup_engine.py" ], "languageLesson": "Les propriétés de type « rollup » et « formule » sont une abstraction puissante empruntée à Notion. Un rollup agrège des valeurs d'une collection liée via une relation, tandis qu'une formule applique un calcul sur les propriétés d'une même page — le tout sans écrire de SQL, grâce à un moteur d'évaluation d'expressions personnalisé." }, { "order": 7, "title": "Tableau Kanban et pages Markdown", "description": "Le routeur board.py (61 dépendances sortantes) est le deuxième fichier le plus complexe de FlowDeck. Il gère le tableau Kanban principal avec ses vues dynamiques (Kanban, Table, Status Overview, Team Load), le système de filtrage et de tri des cartes, la gestion des propriétés custom par projet, et les opérations CRUD sur les pages Markdown avec un arbre de pages récursif. La sidebar est alimentée par des données workspace, pages récentes et favoris.", "nodeIds": [ "file:app/routers/board.py" ], "languageLesson": "Le pattern de « soft delete » utilisé pour la corbeille conserve les données en base avec un marqueur deleted_at plutôt que de les supprimer physiquement. Cela permet la restauration et préserve l'intégrité référentielle des sous-pages, qui sont réassignées au niveau racine lors d'une suppression définitive." }, { "order": 8, "title": "Interface utilisateur : Templates et interactivité", "description": "Le template maître base.html définit le shell HTML complet de l'application : une sidebar Notion avec sections Recents, Favorites, Agents, Shared et Private, une barre de navigation supérieure, un menu contextuel, et la macro Jinja2 récursive render_tree_item pour l'arborescence des pages. Le composant Alpine.js appState() centralise la gestion d'état (sidebar, drag-and-drop avec SortableJS, CRUD pages, thème clair/sombre), tandis que HTMX assure la navigation sans rechargement de page.", "nodeIds": [ "file:app/templates/base.html" ], "languageLesson": "La combinaison HTMX + Alpine.js offre une approche « HTML-first » du frontend : HTMX gère le chargement asynchrone des fragments HTML depuis le serveur (hx-get, hx-post), tandis qu'Alpine.js apporte la réactivité locale (x-data, x-show, @click) sans framework JavaScript lourd. Résultat : une SPA sans build step, sans bundler, et avec un état qui vit dans le DOM." }, { "order": 9, "title": "Multi-utilisateurs et collaboration", "description": "Le routeur workspace.py (57 dépendances sortantes) implémente toutes les fonctionnalités collaboratives de FlowDeck : gestion des workspaces multi-utilisateurs avec rôles (Admin/Membre), système de commentaires sur les pages avec historique, favoris, templates de base de données et de pages, import/export CSV, et partage public des collections. Ces fonctionnalités transforment FlowDeck d'un outil individuel en une plateforme de collaboration d'équipe.", "nodeIds": [ "file:app/routers/workspace.py" ], "languageLesson": "L'import CSV utilise le module csv de Python avec DictReader pour mapper automatiquement les colonnes du fichier aux propriétés de la collection. Les templates de pages stockent une configuration JSON complète (propriétés, contenu, icône) qui est clonée lors de l'application, comme les templates Notion." }, { "order": 10, "title": "Intégration native Gitea", "description": "Le client gitea_client.py encapsule l'API REST de Gitea dans une classe Python avec 16 méthodes : gestion des repositories (user/org), des issues (CRUD, labels, statut), des jalons, des collaborateurs et des webhooks. C'est le pont entre FlowDeck et Gitea — chaque board Kanban est un repository Gitea, chaque carte est une issue, et la synchronisation bidirectionnelle maintient la cohérence entre les deux systèmes.", "nodeIds": [ "file:app/services/gitea_client.py" ], "languageLesson": "Le pattern « API Client wrapper » isole toute la logique d'appel HTTP dans une classe dédiée avec gestion des headers d'authentification (Bearer token), parsing des réponses JSON, et conversion des codes d'erreur HTTP en exceptions Python. Cela permet aux routeurs de travailler avec des objets métier sans se soucier des détails de transport." }, { "order": 11, "title": "Webhooks et API publique REST", "description": "FlowDeck expose une API REST publique (v2.1) protégée par clé d'API, et un système de webhooks bidirectionnel. Le routeur webhooks.py reçoit les événements de Gitea (issues, PR, repo) avec vérification de signature HMAC, tandis que webhook_outbound.py permet de configurer des webhooks sortants pour notifier des services externes des changements dans FlowDeck. Cette couche d'intégration rend FlowDeck extensible et automatisable.", "nodeIds": [ "file:app/routers/webhooks.py", "file:app/services/webhook_outbound.py" ], "languageLesson": "La vérification de signature HMAC-SHA256 des webhooks entrants est essentielle pour la sécurité : le serveur calcule le hash du corps de la requête avec le secret partagé et le compare au header X-Hub-Signature-256 envoyé par Gitea. Sans cette vérification, n'importe qui pourrait injecter de fausses notifications dans le système." }, { "order": 12, "title": "Conteneurisation et déploiement", "description": "Le Dockerfile produit une image basée sur Python 3.12-slim avec healthcheck HTTP intégré sur /api/health et exposition du port 8080. Le docker-compose.yml orchestre le service avec un volume nommé persistant pour la base SQLite, l'injection des variables d'environnement et une politique de redémarrage automatique (restart: unless-stopped). Cette configuration permet de déployer FlowDeck en une seule commande docker compose up -d sur n'importe quel serveur.", "nodeIds": [ "service:Dockerfile", "service:docker-compose.yml" ], "languageLesson": "Le healthcheck Docker (HEALTHCHECK CMD curl -f http://localhost:8080/api/health) permet à Docker de savoir si le conteneur est vivant. Combiné avec restart: unless-stopped dans docker-compose, le conteneur redémarre automatiquement si l'application crashe, assurant une haute disponibilité sans orchestration complexe." }, { "order": 13, "title": "Documentation d'architecture", "description": "Le document ARCHITECTURE.md est la référence technique complète de FlowDeck en 15 chapitres. Il couvre l'ensemble du système : modèle de données (21 tables SQL), routage API (85+ endpoints), architecture frontend (templates Jinja2, Alpine.js, HTMX), flux d'authentification OAuth2, système de propriétés avec moteurs de formules et rollups, vues multiples, sous-éléments et dépendances, éditeur de blocs, et déploiement Docker. C'est la carte de référence pour tout développeur rejoignant le projet.", "nodeIds": [ "document:ARCHITECTURE.md" ] } ] }