Spaces:
Runtime error
Runtime error
| """ | |
| core/database.py | |
| SQLite persistence for Second Brain. | |
| Tables: users, life_areas, goals, tasks, user_context (AI memory) | |
| """ | |
| import sqlite3 | |
| import json | |
| import bcrypt | |
| from datetime import datetime, date | |
| from typing import Optional | |
| DB_PATH = "second_brain.db" | |
| # ββ Connection ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def get_db() -> sqlite3.Connection: | |
| conn = sqlite3.connect(DB_PATH) | |
| conn.row_factory = sqlite3.Row | |
| conn.execute("PRAGMA foreign_keys = ON") | |
| return conn | |
| def init_db(): | |
| """Create all tables on first run.""" | |
| conn = get_db() | |
| c = conn.cursor() | |
| c.execute(""" | |
| CREATE TABLE IF NOT EXISTS users ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| username TEXT UNIQUE NOT NULL, | |
| password_hash TEXT NOT NULL, | |
| created_at TEXT DEFAULT (datetime('now')) | |
| ) | |
| """) | |
| c.execute(""" | |
| CREATE TABLE IF NOT EXISTS life_areas ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| user_id INTEGER NOT NULL, | |
| name TEXT NOT NULL, | |
| color TEXT DEFAULT '#6366f1', | |
| created_at TEXT DEFAULT (datetime('now')), | |
| FOREIGN KEY (user_id) REFERENCES users(id) | |
| ) | |
| """) | |
| c.execute(""" | |
| CREATE TABLE IF NOT EXISTS user_goals ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| user_id INTEGER NOT NULL, | |
| goal_text TEXT NOT NULL, | |
| created_at TEXT DEFAULT (datetime('now')), | |
| FOREIGN KEY (user_id) REFERENCES users(id) | |
| ) | |
| """) | |
| c.execute(""" | |
| CREATE TABLE IF NOT EXISTS tasks ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| user_id INTEGER NOT NULL, | |
| title TEXT NOT NULL, | |
| life_area TEXT DEFAULT '', | |
| urgency TEXT DEFAULT 'Not Urgent', | |
| importance TEXT DEFAULT 'Important', | |
| state_of_mind TEXT DEFAULT 'Easy', | |
| time_estimate INTEGER DEFAULT 30, | |
| scheduled_date TEXT DEFAULT '', | |
| deadline_date TEXT DEFAULT '', | |
| is_completed INTEGER DEFAULT 0, | |
| actual_duration INTEGER, | |
| is_habit INTEGER DEFAULT 0, | |
| habit_interval TEXT DEFAULT '', | |
| raw_input TEXT DEFAULT '', | |
| created_at TEXT DEFAULT (datetime('now')), | |
| FOREIGN KEY (user_id) REFERENCES users(id) | |
| ) | |
| """) | |
| # AI-learned context stored as a JSON blob per user | |
| c.execute(""" | |
| CREATE TABLE IF NOT EXISTS user_context ( | |
| user_id INTEGER PRIMARY KEY, | |
| context TEXT NOT NULL, | |
| updated_at TEXT DEFAULT (datetime('now')), | |
| FOREIGN KEY (user_id) REFERENCES users(id) | |
| ) | |
| """) | |
| # Migrate: add new columns to existing databases without breaking them | |
| for _col in ["deadline_date", "scheduled_date"]: | |
| try: | |
| conn.execute(f"ALTER TABLE tasks ADD COLUMN {_col} TEXT DEFAULT ''") | |
| except Exception: | |
| pass # column already exists | |
| conn.commit() | |
| conn.close() | |
| # ββ Auth ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def register_user(username: str, password: str) -> tuple: | |
| """Returns (user_id, message). user_id is None on failure.""" | |
| username = username.strip().lower() | |
| if not username or not password: | |
| return None, "Username and password cannot be empty." | |
| if len(password) < 6: | |
| return None, "Password must be at least 6 characters." | |
| conn = get_db() | |
| try: | |
| pw_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() | |
| conn.execute( | |
| "INSERT INTO users (username, password_hash) VALUES (?, ?)", | |
| (username, pw_hash) | |
| ) | |
| conn.commit() | |
| row = conn.execute("SELECT id FROM users WHERE username = ?", (username,)).fetchone() | |
| return row["id"], "Account created!" | |
| except sqlite3.IntegrityError: | |
| return None, "Username already taken." | |
| finally: | |
| conn.close() | |
| def login_user(username: str, password: str) -> tuple: | |
| """Returns (user_id, message). user_id is None on failure.""" | |
| username = username.strip().lower() | |
| if not username or not password: | |
| return None, "Please enter your credentials." | |
| conn = get_db() | |
| row = conn.execute( | |
| "SELECT id, password_hash FROM users WHERE username = ?", (username,) | |
| ).fetchone() | |
| conn.close() | |
| if not row: | |
| return None, "Username not found." | |
| if not bcrypt.checkpw(password.encode(), row["password_hash"].encode()): | |
| return None, "Incorrect password." | |
| return row["id"], f"Welcome back, {username}!" | |
| def get_username(user_id: int) -> str: | |
| conn = get_db() | |
| row = conn.execute("SELECT username FROM users WHERE id = ?", (user_id,)).fetchone() | |
| conn.close() | |
| return row["username"].capitalize() if row else "User" | |
| # ββ Life Areas ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| DEFAULT_AREAS = [ | |
| ("Work", "#4F8EF7"), | |
| ("Health", "#4CAF87"), | |
| ("Finance", "#F7A84F"), | |
| ("Learning", "#A855F7"), | |
| ("Personal", "#EC4899"), | |
| ("Family", "#F59E0B"), | |
| ] | |
| def create_default_life_areas(user_id: int): | |
| conn = get_db() | |
| for name, color in DEFAULT_AREAS: | |
| conn.execute( | |
| "INSERT INTO life_areas (user_id, name, color) VALUES (?, ?, ?)", | |
| (user_id, name, color) | |
| ) | |
| conn.commit() | |
| conn.close() | |
| def get_life_areas(user_id: int) -> list: | |
| conn = get_db() | |
| rows = conn.execute( | |
| "SELECT id, name, color FROM life_areas WHERE user_id = ? ORDER BY id", | |
| (user_id,) | |
| ).fetchall() | |
| conn.close() | |
| return [dict(r) for r in rows] | |
| def get_life_area_names(user_id: int) -> list: | |
| return [a["name"] for a in get_life_areas(user_id)] | |
| def add_life_area(user_id: int, name: str, color: str = "#6366f1") -> tuple: | |
| name = name.strip() | |
| if not name: | |
| return False, "Name cannot be empty." | |
| conn = get_db() | |
| exists = conn.execute( | |
| "SELECT id FROM life_areas WHERE user_id = ? AND LOWER(name) = LOWER(?)", | |
| (user_id, name) | |
| ).fetchone() | |
| if exists: | |
| conn.close() | |
| return False, f'"{name}" already exists.' | |
| conn.execute( | |
| "INSERT INTO life_areas (user_id, name, color) VALUES (?, ?, ?)", | |
| (user_id, name, color) | |
| ) | |
| conn.commit() | |
| conn.close() | |
| return True, f'"{name}" added.' | |
| def delete_life_area(user_id: int, name: str) -> tuple: | |
| conn = get_db() | |
| conn.execute( | |
| "DELETE FROM life_areas WHERE user_id = ? AND name = ?", (user_id, name) | |
| ) | |
| conn.commit() | |
| conn.close() | |
| return True, f'"{name}" removed.' | |
| # ββ Goals βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def save_goals(user_id: int, goals_text: str): | |
| conn = get_db() | |
| conn.execute("DELETE FROM user_goals WHERE user_id = ?", (user_id,)) | |
| for line in goals_text.strip().splitlines(): | |
| line = line.strip("β’- ").strip() | |
| if line: | |
| conn.execute( | |
| "INSERT INTO user_goals (user_id, goal_text) VALUES (?, ?)", | |
| (user_id, line) | |
| ) | |
| conn.commit() | |
| conn.close() | |
| def get_goals(user_id: int) -> list: | |
| conn = get_db() | |
| rows = conn.execute( | |
| "SELECT goal_text FROM user_goals WHERE user_id = ? ORDER BY id", | |
| (user_id,) | |
| ).fetchall() | |
| conn.close() | |
| return [r["goal_text"] for r in rows] | |
| # ββ Tasks βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def save_task(user_id: int, task: dict, scheduled_date: str = "") -> int: | |
| conn = get_db() | |
| cursor = conn.execute(""" | |
| INSERT INTO tasks | |
| (user_id, title, life_area, urgency, importance, state_of_mind, | |
| time_estimate, scheduled_date, deadline_date, raw_input, is_habit, habit_interval) | |
| VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) | |
| """, ( | |
| user_id, | |
| task.get("title", "Untitled"), | |
| task.get("life_area", ""), | |
| task.get("urgency", "Not Urgent"), | |
| task.get("importance", "Important"), | |
| task.get("state_of_mind", "Easy"), | |
| int(task.get("time_estimate") or 30), | |
| scheduled_date or "", | |
| task.get("deadline_date", "") or "", | |
| task.get("raw_input", ""), | |
| 1 if task.get("is_habit") else 0, | |
| task.get("habit_interval", ""), | |
| )) | |
| task_id = cursor.lastrowid | |
| conn.commit() | |
| conn.close() | |
| return task_id | |
| def get_tasks(user_id: int, filter_area: str = "All", only_today: bool = False, | |
| include_completed: bool = True) -> list: | |
| conn = get_db() | |
| q = "SELECT * FROM tasks WHERE user_id = ?" | |
| params = [user_id] | |
| if filter_area and filter_area != "All": | |
| q += " AND life_area = ?" | |
| params.append(filter_area) | |
| if only_today: | |
| q += " AND scheduled_date = ?" | |
| params.append(str(date.today())) | |
| if not include_completed: | |
| q += " AND is_completed = 0" | |
| q += " ORDER BY is_completed ASC, created_at DESC" | |
| rows = conn.execute(q, params).fetchall() | |
| conn.close() | |
| return [dict(r) for r in rows] | |
| def toggle_task_complete(task_id: int, user_id: int, actual_duration: int = None): | |
| conn = get_db() | |
| task = conn.execute( | |
| "SELECT is_completed FROM tasks WHERE id = ? AND user_id = ?", (task_id, user_id) | |
| ).fetchone() | |
| if task: | |
| new_status = 1 - task["is_completed"] | |
| if actual_duration and new_status == 1: | |
| conn.execute( | |
| "UPDATE tasks SET is_completed = ?, actual_duration = ? WHERE id = ? AND user_id = ?", | |
| (new_status, actual_duration, task_id, user_id) | |
| ) | |
| else: | |
| conn.execute( | |
| "UPDATE tasks SET is_completed = ? WHERE id = ? AND user_id = ?", | |
| (new_status, task_id, user_id) | |
| ) | |
| conn.commit() | |
| conn.close() | |
| def delete_task(task_id: int, user_id: int): | |
| conn = get_db() | |
| conn.execute("DELETE FROM tasks WHERE id = ? AND user_id = ?", (task_id, user_id)) | |
| conn.commit() | |
| conn.close() | |
| def assign_task_date(task_id: int, user_id: int, scheduled_date: str): | |
| """Assign a scheduled date to an existing task (called by the AI planner).""" | |
| conn = get_db() | |
| conn.execute( | |
| "UPDATE tasks SET scheduled_date = ? WHERE id = ? AND user_id = ?", | |
| (scheduled_date, task_id, user_id) | |
| ) | |
| conn.commit() | |
| conn.close() | |
| def get_today_stats(user_id: int) -> dict: | |
| tasks = get_tasks(user_id, only_today=True) | |
| total = len(tasks) | |
| done = sum(1 for t in tasks if t["is_completed"]) | |
| return {"total": total, "done": done, "remaining": total - done} | |
| # ββ Habit recurrence ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def spawn_due_habits(user_id: int): | |
| """ | |
| Check all habit tasks. If a habit's scheduled_date < today and | |
| it's not already scheduled for today, create a fresh copy for today. | |
| Called on login / tab load. | |
| """ | |
| today = str(date.today()) | |
| conn = get_db() | |
| habits = conn.execute( | |
| "SELECT * FROM tasks WHERE user_id = ? AND is_habit = 1", | |
| (user_id,) | |
| ).fetchall() | |
| for h in habits: | |
| # Check if already exists today | |
| existing = conn.execute( | |
| "SELECT id FROM tasks WHERE user_id = ? AND title = ? AND is_habit = 1 AND scheduled_date = ?", | |
| (user_id, h["title"], today) | |
| ).fetchone() | |
| if existing: | |
| continue | |
| # Only spawn if original was scheduled before today | |
| if h["scheduled_date"] and h["scheduled_date"] >= today: | |
| continue | |
| conn.execute(""" | |
| INSERT INTO tasks (user_id, title, life_area, urgency, importance, | |
| state_of_mind, time_estimate, scheduled_date, is_habit, | |
| habit_interval, raw_input) | |
| VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?) | |
| """, ( | |
| user_id, h["title"], h["life_area"], "Habit", | |
| h["importance"], h["state_of_mind"], h["time_estimate"], | |
| today, h["habit_interval"], h["raw_input"] | |
| )) | |
| conn.commit() | |
| conn.close() | |
| # ββ AI Context ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def load_user_context(user_id: int) -> Optional[dict]: | |
| conn = get_db() | |
| row = conn.execute( | |
| "SELECT context FROM user_context WHERE user_id = ?", (user_id,) | |
| ).fetchone() | |
| conn.close() | |
| if row: | |
| try: | |
| return json.loads(row["context"]) | |
| except Exception: | |
| return None | |
| return None | |
| def save_user_context(user_id: int, context: dict): | |
| conn = get_db() | |
| conn.execute(""" | |
| INSERT INTO user_context (user_id, context, updated_at) | |
| VALUES (?, ?, datetime('now')) | |
| ON CONFLICT(user_id) DO UPDATE SET | |
| context = excluded.context, | |
| updated_at = excluded.updated_at | |
| """, (user_id, json.dumps(context))) | |
| conn.commit() | |
| conn.close() |