eraz3r commited on
Commit
162afa2
Β·
verified Β·
1 Parent(s): 04b902f

Update core/database.py

Browse files
Files changed (1) hide show
  1. core/database.py +144 -69
core/database.py CHANGED
@@ -1,6 +1,7 @@
1
  """
2
  core/database.py
3
  SQLite persistence for Second Brain.
 
4
  """
5
 
6
  import sqlite3
@@ -12,6 +13,8 @@ from typing import Optional
12
  DB_PATH = "second_brain.db"
13
 
14
 
 
 
15
  def get_db() -> sqlite3.Connection:
16
  conn = sqlite3.connect(DB_PATH)
17
  conn.row_factory = sqlite3.Row
@@ -20,6 +23,7 @@ def get_db() -> sqlite3.Connection:
20
 
21
 
22
  def init_db():
 
23
  conn = get_db()
24
  c = conn.cursor()
25
 
@@ -75,15 +79,7 @@ def init_db():
75
  )
76
  """)
77
 
78
- # Migrate existing DBs β€” add deadline_date if missing
79
- try:
80
- c.execute("ALTER TABLE tasks ADD COLUMN deadline_date TEXT DEFAULT ''")
81
- except Exception:
82
- pass # column already exists
83
-
84
- # Migrate existing DBs β€” clear default date so old tasks aren't shown as today
85
- # (only affects new installs; existing data keeps its dates)
86
-
87
  c.execute("""
88
  CREATE TABLE IF NOT EXISTS user_context (
89
  user_id INTEGER PRIMARY KEY,
@@ -93,6 +89,12 @@ def init_db():
93
  )
94
  """)
95
 
 
 
 
 
 
 
96
  conn.commit()
97
  conn.close()
98
 
@@ -100,6 +102,7 @@ def init_db():
100
  # ── Auth ──────────────────────────────────────────────────────────────────────
101
 
102
  def register_user(username: str, password: str) -> tuple:
 
103
  username = username.strip().lower()
104
  if not username or not password:
105
  return None, "Username and password cannot be empty."
@@ -108,7 +111,10 @@ def register_user(username: str, password: str) -> tuple:
108
  conn = get_db()
109
  try:
110
  pw_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
111
- conn.execute("INSERT INTO users (username, password_hash) VALUES (?, ?)", (username, pw_hash))
 
 
 
112
  conn.commit()
113
  row = conn.execute("SELECT id FROM users WHERE username = ?", (username,)).fetchone()
114
  return row["id"], "Account created!"
@@ -119,11 +125,14 @@ def register_user(username: str, password: str) -> tuple:
119
 
120
 
121
  def login_user(username: str, password: str) -> tuple:
 
122
  username = username.strip().lower()
123
  if not username or not password:
124
  return None, "Please enter your credentials."
125
  conn = get_db()
126
- row = conn.execute("SELECT id, password_hash FROM users WHERE username = ?", (username,)).fetchone()
 
 
127
  conn.close()
128
  if not row:
129
  return None, "Username not found."
@@ -142,41 +151,68 @@ def get_username(user_id: int) -> str:
142
  # ── Life Areas ────────────────────────────────────────────────────────────────
143
 
144
  DEFAULT_AREAS = [
145
- ("Work", "#4F8EF7"), ("Health", "#4CAF87"), ("Finance", "#F7A84F"),
146
- ("Learning", "#A855F7"), ("Personal", "#EC4899"), ("Family", "#F59E0B"),
 
 
 
 
147
  ]
148
 
 
149
  def create_default_life_areas(user_id: int):
150
  conn = get_db()
151
  for name, color in DEFAULT_AREAS:
152
- conn.execute("INSERT INTO life_areas (user_id, name, color) VALUES (?, ?, ?)", (user_id, name, color))
 
 
 
153
  conn.commit()
154
  conn.close()
155
 
 
156
  def get_life_areas(user_id: int) -> list:
157
  conn = get_db()
158
- rows = conn.execute("SELECT id, name, color FROM life_areas WHERE user_id = ? ORDER BY id", (user_id,)).fetchall()
 
 
 
159
  conn.close()
160
  return [dict(r) for r in rows]
161
 
 
162
  def get_life_area_names(user_id: int) -> list:
163
  return [a["name"] for a in get_life_areas(user_id)]
164
 
 
165
  def add_life_area(user_id: int, name: str, color: str = "#6366f1") -> tuple:
166
  name = name.strip()
167
- if not name: return False, "Name cannot be empty."
 
168
  conn = get_db()
169
- exists = conn.execute("SELECT id FROM life_areas WHERE user_id = ? AND LOWER(name) = LOWER(?)", (user_id, name)).fetchone()
 
 
 
170
  if exists:
171
- conn.close(); return False, f'"{name}" already exists.'
172
- conn.execute("INSERT INTO life_areas (user_id, name, color) VALUES (?, ?, ?)", (user_id, name, color))
173
- conn.commit(); conn.close()
 
 
 
 
 
174
  return True, f'"{name}" added.'
175
 
 
176
  def delete_life_area(user_id: int, name: str) -> tuple:
177
  conn = get_db()
178
- conn.execute("DELETE FROM life_areas WHERE user_id = ? AND name = ?", (user_id, name))
179
- conn.commit(); conn.close()
 
 
 
180
  return True, f'"{name}" removed.'
181
 
182
 
@@ -188,23 +224,27 @@ def save_goals(user_id: int, goals_text: str):
188
  for line in goals_text.strip().splitlines():
189
  line = line.strip("β€’- ").strip()
190
  if line:
191
- conn.execute("INSERT INTO user_goals (user_id, goal_text) VALUES (?, ?)", (user_id, line))
192
- conn.commit(); conn.close()
 
 
 
 
 
193
 
194
  def get_goals(user_id: int) -> list:
195
  conn = get_db()
196
- rows = conn.execute("SELECT goal_text FROM user_goals WHERE user_id = ? ORDER BY id", (user_id,)).fetchall()
 
 
 
197
  conn.close()
198
  return [r["goal_text"] for r in rows]
199
 
200
 
201
  # ── Tasks ─────────────────────────────────────────────────────────────────────
202
 
203
- def save_task(user_id: int, task: dict, scheduled_date: str = None) -> int:
204
- """
205
- Save a task. scheduled_date=None means unscheduled (awaiting AI assignment).
206
- Pass scheduled_date="" explicitly to also leave unscheduled.
207
- """
208
  conn = get_db()
209
  cursor = conn.execute("""
210
  INSERT INTO tasks
@@ -219,41 +259,32 @@ def save_task(user_id: int, task: dict, scheduled_date: str = None) -> int:
219
  task.get("importance", "Important"),
220
  task.get("state_of_mind", "Easy"),
221
  int(task.get("time_estimate") or 30),
222
- scheduled_date if scheduled_date is not None else "", # "" = unscheduled
223
- task.get("deadline_date", ""),
224
  task.get("raw_input", ""),
225
  1 if task.get("is_habit") else 0,
226
  task.get("habit_interval", ""),
227
  ))
228
  task_id = cursor.lastrowid
229
- conn.commit(); conn.close()
 
230
  return task_id
231
 
232
 
233
- def assign_task_date(task_id: int, user_id: int, scheduled_date: str):
234
- """Update a task's scheduled_date (called by the AI scheduler)."""
235
- conn = get_db()
236
- conn.execute(
237
- "UPDATE tasks SET scheduled_date = ? WHERE id = ? AND user_id = ?",
238
- (scheduled_date, task_id, user_id)
239
- )
240
- conn.commit(); conn.close()
241
-
242
-
243
  def get_tasks(user_id: int, filter_area: str = "All", only_today: bool = False,
244
- include_completed: bool = True, only_unscheduled: bool = False) -> list:
245
  conn = get_db()
246
  q = "SELECT * FROM tasks WHERE user_id = ?"
247
  params = [user_id]
248
  if filter_area and filter_area != "All":
249
- q += " AND life_area = ?"; params.append(filter_area)
 
250
  if only_today:
251
- q += " AND scheduled_date = ?"; params.append(str(date.today()))
252
- if only_unscheduled:
253
- q += " AND (scheduled_date = '' OR scheduled_date IS NULL)"
254
  if not include_completed:
255
  q += " AND is_completed = 0"
256
- q += " ORDER BY is_completed ASC, CASE WHEN deadline_date = '' THEN '9999' ELSE deadline_date END ASC, created_at DESC"
257
  rows = conn.execute(q, params).fetchall()
258
  conn.close()
259
  return [dict(r) for r in rows]
@@ -261,62 +292,103 @@ def get_tasks(user_id: int, filter_area: str = "All", only_today: bool = False,
261
 
262
  def toggle_task_complete(task_id: int, user_id: int, actual_duration: int = None):
263
  conn = get_db()
264
- task = conn.execute("SELECT is_completed FROM tasks WHERE id = ? AND user_id = ?", (task_id, user_id)).fetchone()
 
 
265
  if task:
266
  new_status = 1 - task["is_completed"]
267
  if actual_duration and new_status == 1:
268
- conn.execute("UPDATE tasks SET is_completed = ?, actual_duration = ? WHERE id = ? AND user_id = ?",
269
- (new_status, actual_duration, task_id, user_id))
 
 
270
  else:
271
- conn.execute("UPDATE tasks SET is_completed = ? WHERE id = ? AND user_id = ?",
272
- (new_status, task_id, user_id))
273
- conn.commit(); conn.close()
 
 
 
274
 
275
 
276
  def delete_task(task_id: int, user_id: int):
277
  conn = get_db()
278
  conn.execute("DELETE FROM tasks WHERE id = ? AND user_id = ?", (task_id, user_id))
279
- conn.commit(); conn.close()
 
 
 
 
 
 
 
 
 
 
 
 
280
 
281
 
282
  def get_today_stats(user_id: int) -> dict:
283
  tasks = get_tasks(user_id, only_today=True)
284
- total = len(tasks); done = sum(1 for t in tasks if t["is_completed"])
 
285
  return {"total": total, "done": done, "remaining": total - done}
286
 
287
 
288
  # ── Habit recurrence ──────────────────────────────────────────────────────────
289
 
290
  def spawn_due_habits(user_id: int):
 
 
 
 
 
291
  today = str(date.today())
292
- conn = get_db()
293
- habits = conn.execute("SELECT * FROM tasks WHERE user_id = ? AND is_habit = 1", (user_id,)).fetchall()
 
 
 
 
294
  for h in habits:
 
295
  existing = conn.execute(
296
  "SELECT id FROM tasks WHERE user_id = ? AND title = ? AND is_habit = 1 AND scheduled_date = ?",
297
  (user_id, h["title"], today)
298
  ).fetchone()
299
- if existing: continue
300
- if h["scheduled_date"] and h["scheduled_date"] >= today: continue
 
 
 
301
  conn.execute("""
302
  INSERT INTO tasks (user_id, title, life_area, urgency, importance,
303
  state_of_mind, time_estimate, scheduled_date, is_habit,
304
  habit_interval, raw_input)
305
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
306
- """, (user_id, h["title"], h["life_area"], "Habit", h["importance"],
307
- h["state_of_mind"], h["time_estimate"], today, h["habit_interval"], h["raw_input"]))
308
- conn.commit(); conn.close()
 
 
 
 
309
 
310
 
311
  # ── AI Context ────────────────────────────────────────────────────────────────
312
 
313
  def load_user_context(user_id: int) -> Optional[dict]:
314
  conn = get_db()
315
- row = conn.execute("SELECT context FROM user_context WHERE user_id = ?", (user_id,)).fetchone()
 
 
316
  conn.close()
317
  if row:
318
- try: return json.loads(row["context"])
319
- except Exception: return None
 
 
320
  return None
321
 
322
 
@@ -325,6 +397,9 @@ def save_user_context(user_id: int, context: dict):
325
  conn.execute("""
326
  INSERT INTO user_context (user_id, context, updated_at)
327
  VALUES (?, ?, datetime('now'))
328
- ON CONFLICT(user_id) DO UPDATE SET context = excluded.context, updated_at = excluded.updated_at
 
 
329
  """, (user_id, json.dumps(context)))
330
- conn.commit(); conn.close()
 
 
1
  """
2
  core/database.py
3
  SQLite persistence for Second Brain.
4
+ Tables: users, life_areas, goals, tasks, user_context (AI memory)
5
  """
6
 
7
  import sqlite3
 
13
  DB_PATH = "second_brain.db"
14
 
15
 
16
+ # ── Connection ────────────────────────────────────────────────────────────────
17
+
18
  def get_db() -> sqlite3.Connection:
19
  conn = sqlite3.connect(DB_PATH)
20
  conn.row_factory = sqlite3.Row
 
23
 
24
 
25
  def init_db():
26
+ """Create all tables on first run."""
27
  conn = get_db()
28
  c = conn.cursor()
29
 
 
79
  )
80
  """)
81
 
82
+ # AI-learned context stored as a JSON blob per user
 
 
 
 
 
 
 
 
83
  c.execute("""
84
  CREATE TABLE IF NOT EXISTS user_context (
85
  user_id INTEGER PRIMARY KEY,
 
89
  )
90
  """)
91
 
92
+ # Migrate: add new columns to existing databases without breaking them
93
+ for _col in ["deadline_date", "scheduled_date"]:
94
+ try:
95
+ conn.execute(f"ALTER TABLE tasks ADD COLUMN {_col} TEXT DEFAULT ''")
96
+ except Exception:
97
+ pass # column already exists
98
  conn.commit()
99
  conn.close()
100
 
 
102
  # ── Auth ──────────────────────────────────────────────────────────────────────
103
 
104
  def register_user(username: str, password: str) -> tuple:
105
+ """Returns (user_id, message). user_id is None on failure."""
106
  username = username.strip().lower()
107
  if not username or not password:
108
  return None, "Username and password cannot be empty."
 
111
  conn = get_db()
112
  try:
113
  pw_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
114
+ conn.execute(
115
+ "INSERT INTO users (username, password_hash) VALUES (?, ?)",
116
+ (username, pw_hash)
117
+ )
118
  conn.commit()
119
  row = conn.execute("SELECT id FROM users WHERE username = ?", (username,)).fetchone()
120
  return row["id"], "Account created!"
 
125
 
126
 
127
  def login_user(username: str, password: str) -> tuple:
128
+ """Returns (user_id, message). user_id is None on failure."""
129
  username = username.strip().lower()
130
  if not username or not password:
131
  return None, "Please enter your credentials."
132
  conn = get_db()
133
+ row = conn.execute(
134
+ "SELECT id, password_hash FROM users WHERE username = ?", (username,)
135
+ ).fetchone()
136
  conn.close()
137
  if not row:
138
  return None, "Username not found."
 
151
  # ── Life Areas ────────────────────────────────────────────────────────────────
152
 
153
  DEFAULT_AREAS = [
154
+ ("Work", "#4F8EF7"),
155
+ ("Health", "#4CAF87"),
156
+ ("Finance", "#F7A84F"),
157
+ ("Learning", "#A855F7"),
158
+ ("Personal", "#EC4899"),
159
+ ("Family", "#F59E0B"),
160
  ]
161
 
162
+
163
  def create_default_life_areas(user_id: int):
164
  conn = get_db()
165
  for name, color in DEFAULT_AREAS:
166
+ conn.execute(
167
+ "INSERT INTO life_areas (user_id, name, color) VALUES (?, ?, ?)",
168
+ (user_id, name, color)
169
+ )
170
  conn.commit()
171
  conn.close()
172
 
173
+
174
  def get_life_areas(user_id: int) -> list:
175
  conn = get_db()
176
+ rows = conn.execute(
177
+ "SELECT id, name, color FROM life_areas WHERE user_id = ? ORDER BY id",
178
+ (user_id,)
179
+ ).fetchall()
180
  conn.close()
181
  return [dict(r) for r in rows]
182
 
183
+
184
  def get_life_area_names(user_id: int) -> list:
185
  return [a["name"] for a in get_life_areas(user_id)]
186
 
187
+
188
  def add_life_area(user_id: int, name: str, color: str = "#6366f1") -> tuple:
189
  name = name.strip()
190
+ if not name:
191
+ return False, "Name cannot be empty."
192
  conn = get_db()
193
+ exists = conn.execute(
194
+ "SELECT id FROM life_areas WHERE user_id = ? AND LOWER(name) = LOWER(?)",
195
+ (user_id, name)
196
+ ).fetchone()
197
  if exists:
198
+ conn.close()
199
+ return False, f'"{name}" already exists.'
200
+ conn.execute(
201
+ "INSERT INTO life_areas (user_id, name, color) VALUES (?, ?, ?)",
202
+ (user_id, name, color)
203
+ )
204
+ conn.commit()
205
+ conn.close()
206
  return True, f'"{name}" added.'
207
 
208
+
209
  def delete_life_area(user_id: int, name: str) -> tuple:
210
  conn = get_db()
211
+ conn.execute(
212
+ "DELETE FROM life_areas WHERE user_id = ? AND name = ?", (user_id, name)
213
+ )
214
+ conn.commit()
215
+ conn.close()
216
  return True, f'"{name}" removed.'
217
 
218
 
 
224
  for line in goals_text.strip().splitlines():
225
  line = line.strip("β€’- ").strip()
226
  if line:
227
+ conn.execute(
228
+ "INSERT INTO user_goals (user_id, goal_text) VALUES (?, ?)",
229
+ (user_id, line)
230
+ )
231
+ conn.commit()
232
+ conn.close()
233
+
234
 
235
  def get_goals(user_id: int) -> list:
236
  conn = get_db()
237
+ rows = conn.execute(
238
+ "SELECT goal_text FROM user_goals WHERE user_id = ? ORDER BY id",
239
+ (user_id,)
240
+ ).fetchall()
241
  conn.close()
242
  return [r["goal_text"] for r in rows]
243
 
244
 
245
  # ── Tasks ─────────────────────────────────────────────────────────────────────
246
 
247
+ def save_task(user_id: int, task: dict, scheduled_date: str = "") -> int:
 
 
 
 
248
  conn = get_db()
249
  cursor = conn.execute("""
250
  INSERT INTO tasks
 
259
  task.get("importance", "Important"),
260
  task.get("state_of_mind", "Easy"),
261
  int(task.get("time_estimate") or 30),
262
+ scheduled_date or "",
263
+ task.get("deadline_date", "") or "",
264
  task.get("raw_input", ""),
265
  1 if task.get("is_habit") else 0,
266
  task.get("habit_interval", ""),
267
  ))
268
  task_id = cursor.lastrowid
269
+ conn.commit()
270
+ conn.close()
271
  return task_id
272
 
273
 
 
 
 
 
 
 
 
 
 
 
274
  def get_tasks(user_id: int, filter_area: str = "All", only_today: bool = False,
275
+ include_completed: bool = True) -> list:
276
  conn = get_db()
277
  q = "SELECT * FROM tasks WHERE user_id = ?"
278
  params = [user_id]
279
  if filter_area and filter_area != "All":
280
+ q += " AND life_area = ?"
281
+ params.append(filter_area)
282
  if only_today:
283
+ q += " AND scheduled_date = ?"
284
+ params.append(str(date.today()))
 
285
  if not include_completed:
286
  q += " AND is_completed = 0"
287
+ q += " ORDER BY is_completed ASC, created_at DESC"
288
  rows = conn.execute(q, params).fetchall()
289
  conn.close()
290
  return [dict(r) for r in rows]
 
292
 
293
  def toggle_task_complete(task_id: int, user_id: int, actual_duration: int = None):
294
  conn = get_db()
295
+ task = conn.execute(
296
+ "SELECT is_completed FROM tasks WHERE id = ? AND user_id = ?", (task_id, user_id)
297
+ ).fetchone()
298
  if task:
299
  new_status = 1 - task["is_completed"]
300
  if actual_duration and new_status == 1:
301
+ conn.execute(
302
+ "UPDATE tasks SET is_completed = ?, actual_duration = ? WHERE id = ? AND user_id = ?",
303
+ (new_status, actual_duration, task_id, user_id)
304
+ )
305
  else:
306
+ conn.execute(
307
+ "UPDATE tasks SET is_completed = ? WHERE id = ? AND user_id = ?",
308
+ (new_status, task_id, user_id)
309
+ )
310
+ conn.commit()
311
+ conn.close()
312
 
313
 
314
  def delete_task(task_id: int, user_id: int):
315
  conn = get_db()
316
  conn.execute("DELETE FROM tasks WHERE id = ? AND user_id = ?", (task_id, user_id))
317
+ conn.commit()
318
+ conn.close()
319
+
320
+
321
+ def assign_task_date(task_id: int, user_id: int, scheduled_date: str):
322
+ """Assign a scheduled date to an existing task (called by the AI planner)."""
323
+ conn = get_db()
324
+ conn.execute(
325
+ "UPDATE tasks SET scheduled_date = ? WHERE id = ? AND user_id = ?",
326
+ (scheduled_date, task_id, user_id)
327
+ )
328
+ conn.commit()
329
+ conn.close()
330
 
331
 
332
  def get_today_stats(user_id: int) -> dict:
333
  tasks = get_tasks(user_id, only_today=True)
334
+ total = len(tasks)
335
+ done = sum(1 for t in tasks if t["is_completed"])
336
  return {"total": total, "done": done, "remaining": total - done}
337
 
338
 
339
  # ── Habit recurrence ──────────────────────────────────────────────────────────
340
 
341
  def spawn_due_habits(user_id: int):
342
+ """
343
+ Check all habit tasks. If a habit's scheduled_date < today and
344
+ it's not already scheduled for today, create a fresh copy for today.
345
+ Called on login / tab load.
346
+ """
347
  today = str(date.today())
348
+ conn = get_db()
349
+ habits = conn.execute(
350
+ "SELECT * FROM tasks WHERE user_id = ? AND is_habit = 1",
351
+ (user_id,)
352
+ ).fetchall()
353
+
354
  for h in habits:
355
+ # Check if already exists today
356
  existing = conn.execute(
357
  "SELECT id FROM tasks WHERE user_id = ? AND title = ? AND is_habit = 1 AND scheduled_date = ?",
358
  (user_id, h["title"], today)
359
  ).fetchone()
360
+ if existing:
361
+ continue
362
+ # Only spawn if original was scheduled before today
363
+ if h["scheduled_date"] and h["scheduled_date"] >= today:
364
+ continue
365
  conn.execute("""
366
  INSERT INTO tasks (user_id, title, life_area, urgency, importance,
367
  state_of_mind, time_estimate, scheduled_date, is_habit,
368
  habit_interval, raw_input)
369
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
370
+ """, (
371
+ user_id, h["title"], h["life_area"], "Habit",
372
+ h["importance"], h["state_of_mind"], h["time_estimate"],
373
+ today, h["habit_interval"], h["raw_input"]
374
+ ))
375
+ conn.commit()
376
+ conn.close()
377
 
378
 
379
  # ── AI Context ────────────────────────────────────────────────────────────────
380
 
381
  def load_user_context(user_id: int) -> Optional[dict]:
382
  conn = get_db()
383
+ row = conn.execute(
384
+ "SELECT context FROM user_context WHERE user_id = ?", (user_id,)
385
+ ).fetchone()
386
  conn.close()
387
  if row:
388
+ try:
389
+ return json.loads(row["context"])
390
+ except Exception:
391
+ return None
392
  return None
393
 
394
 
 
397
  conn.execute("""
398
  INSERT INTO user_context (user_id, context, updated_at)
399
  VALUES (?, ?, datetime('now'))
400
+ ON CONFLICT(user_id) DO UPDATE SET
401
+ context = excluded.context,
402
+ updated_at = excluded.updated_at
403
  """, (user_id, json.dumps(context)))
404
+ conn.commit()
405
+ conn.close()