c5b049edba
Self-hosted personal task list: FastAPI + SQLite in Docker, mobile-friendly PWA frontend, deadlines, priorities, categories, tags, notes, checklists, recurring tasks, single-password login, and JSON export/import. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
82 lines
2.6 KiB
Python
82 lines
2.6 KiB
Python
import os
|
|
import sqlite3
|
|
from pathlib import Path
|
|
|
|
DB_PATH = os.environ.get("DB_PATH", "/data/whattodo.db")
|
|
SCHEMA_VERSION = 1
|
|
|
|
DEFAULT_CATEGORIES = [
|
|
("Work", "#2563eb"),
|
|
("Life", "#16a34a"),
|
|
("Personal", "#9333ea"),
|
|
("YouTube", "#dc2626"),
|
|
]
|
|
|
|
SCHEMA = """
|
|
CREATE TABLE IF NOT EXISTS meta (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS categories (
|
|
id INTEGER PRIMARY KEY,
|
|
name TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
|
color TEXT NOT NULL DEFAULT '#6b7280',
|
|
position INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
CREATE TABLE IF NOT EXISTS tasks (
|
|
id INTEGER PRIMARY KEY,
|
|
title TEXT NOT NULL,
|
|
notes TEXT NOT NULL DEFAULT '',
|
|
category_id INTEGER REFERENCES categories(id) ON DELETE SET NULL,
|
|
priority INTEGER NOT NULL DEFAULT 0,
|
|
due_date TEXT,
|
|
tags TEXT NOT NULL DEFAULT '[]',
|
|
recurrence TEXT NOT NULL DEFAULT 'none',
|
|
recur_interval INTEGER NOT NULL DEFAULT 1,
|
|
done INTEGER NOT NULL DEFAULT 0,
|
|
done_at TEXT,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS subtasks (
|
|
id INTEGER PRIMARY KEY,
|
|
task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
title TEXT NOT NULL,
|
|
done INTEGER NOT NULL DEFAULT 0,
|
|
position INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_subtasks_task ON subtasks(task_id);
|
|
"""
|
|
|
|
|
|
def connect() -> sqlite3.Connection:
|
|
# FastAPI may open a request's connection and run its endpoint on different threads;
|
|
# each connection is still used by only one request at a time.
|
|
conn = sqlite3.connect(DB_PATH, check_same_thread=False)
|
|
conn.row_factory = sqlite3.Row
|
|
conn.execute("PRAGMA foreign_keys = ON")
|
|
return conn
|
|
|
|
|
|
def init() -> None:
|
|
Path(DB_PATH).parent.mkdir(parents=True, exist_ok=True)
|
|
conn = connect()
|
|
try:
|
|
conn.execute("PRAGMA journal_mode = WAL")
|
|
conn.executescript(SCHEMA)
|
|
row = conn.execute("SELECT value FROM meta WHERE key = 'schema_version'").fetchone()
|
|
if row is None:
|
|
# Fresh database: seed default categories.
|
|
with conn:
|
|
for i, (name, color) in enumerate(DEFAULT_CATEGORIES):
|
|
conn.execute(
|
|
"INSERT INTO categories (name, color, position) VALUES (?, ?, ?)",
|
|
(name, color, i),
|
|
)
|
|
conn.execute(
|
|
"INSERT INTO meta (key, value) VALUES ('schema_version', ?)",
|
|
(str(SCHEMA_VERSION),),
|
|
)
|
|
finally:
|
|
conn.close()
|