"""FlowDeck — Database layer (SQLite with WAL mode).""" from __future__ import annotations import sqlite3 from contextlib import contextmanager from pathlib import Path def get_db_path() -> Path: from app.config import settings return settings.db_path def init_db(): """Create tables if they don't exist.""" db_path = get_db_path() if str(db_path) != ":memory:": db_path.parent.mkdir(parents=True, exist_ok=True) with get_conn() as conn: conn.execute("PRAGMA journal_mode=WAL") conn.execute("PRAGMA foreign_keys=ON") conn.executescript(""" CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, login TEXT NOT NULL UNIQUE, full_name TEXT NOT NULL DEFAULT '', email TEXT NOT NULL DEFAULT '', avatar_url TEXT NOT NULL DEFAULT '', is_admin BOOLEAN NOT NULL DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS user_tokens ( id INTEGER PRIMARY KEY AUTOINCREMENT, gitea_user_id INTEGER NOT NULL UNIQUE, gitea_token TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS boards ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_owner TEXT NOT NULL, project_name TEXT NOT NULL, columns_json TEXT NOT NULL DEFAULT '["Backlog","À faire","En cours","Révision","Terminé"]', wip_limits_json TEXT DEFAULT '{}', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(project_owner, project_name) ); CREATE TABLE IF NOT EXISTS cards ( id INTEGER PRIMARY KEY AUTOINCREMENT, board_id INTEGER NOT NULL REFERENCES boards(id), gitea_issue_id INTEGER, column_name TEXT NOT NULL DEFAULT 'Backlog', position INTEGER NOT NULL DEFAULT 0, priority TEXT DEFAULT 'Medium', due_date TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_owner TEXT NOT NULL, project_name TEXT NOT NULL, title TEXT NOT NULL DEFAULT 'Notes', content TEXT NOT NULL DEFAULT '', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(project_owner, project_name, title) ); CREATE TABLE IF NOT EXISTS col_mapping ( id INTEGER PRIMARY KEY AUTOINCREMENT, board_id INTEGER NOT NULL REFERENCES boards(id), column_name TEXT NOT NULL, gitea_label TEXT NOT NULL, close_issue BOOLEAN NOT NULL DEFAULT 0, UNIQUE(board_id, column_name) ); CREATE TABLE IF NOT EXISTS checklists ( id INTEGER PRIMARY KEY AUTOINCREMENT, board_id INTEGER NOT NULL, gitea_issue_id INTEGER NOT NULL, title TEXT NOT NULL DEFAULT 'Checklist', position INTEGER NOT NULL DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(board_id, gitea_issue_id, title) ); CREATE TABLE IF NOT EXISTS checklist_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, checklist_id INTEGER NOT NULL REFERENCES checklists(id) ON DELETE CASCADE, content TEXT NOT NULL DEFAULT '', checked BOOLEAN NOT NULL DEFAULT 0, position INTEGER NOT NULL DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS project_properties ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_owner TEXT NOT NULL, project_name TEXT NOT NULL, name TEXT NOT NULL, prop_type TEXT NOT NULL DEFAULT 'select', -- select, multi_select, date, person, text options_json TEXT DEFAULT '[]', -- for select/multi_select position INTEGER NOT NULL DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(project_owner, project_name, name) ); CREATE TABLE IF NOT EXISTS property_values ( id INTEGER PRIMARY KEY AUTOINCREMENT, property_id INTEGER NOT NULL REFERENCES project_properties(id) ON DELETE CASCADE, gitea_issue_id INTEGER NOT NULL, value TEXT NOT NULL DEFAULT '', UNIQUE(property_id, gitea_issue_id) ); CREATE TABLE IF NOT EXISTS ai_keywords ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_owner TEXT NOT NULL, project_name TEXT NOT NULL, keyword TEXT NOT NULL, color TEXT NOT NULL DEFAULT '#787774', usage_count INTEGER NOT NULL DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(project_owner, project_name, keyword) ); CREATE TABLE IF NOT EXISTS pages ( id INTEGER PRIMARY KEY AUTOINCREMENT, workspace TEXT NOT NULL, title TEXT NOT NULL DEFAULT 'New page', content TEXT NOT NULL DEFAULT '', parent_section TEXT DEFAULT 'Private', parent_id INTEGER REFERENCES pages(id), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); """) # Migration: add parent_id if missing (v1.0.0+) try: conn.execute("ALTER TABLE pages ADD COLUMN parent_id INTEGER REFERENCES pages(id)") except sqlite3.OperationalError: pass # Migration: add sort_order for drag & drop tree reordering (v1.1.0+) try: conn.execute("ALTER TABLE pages ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0") except sqlite3.OperationalError: pass conn.commit() @contextmanager def get_conn(): """Yield a SQLite connection (WAL mode, foreign keys on).""" db_path = get_db_path() conn = sqlite3.connect(str(db_path)) conn.row_factory = sqlite3.Row conn.execute("PRAGMA journal_mode=WAL") conn.execute("PRAGMA foreign_keys=ON") try: yield conn finally: conn.close()