second-brain / core /database.py
eraz3r's picture
Update core/database.py
162afa2 verified
Raw
History Blame
14.1 kB
"""
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()