eraz3r commited on
Commit
c1fa4d2
Β·
verified Β·
1 Parent(s): 7d63e80

Update core/database.py

Browse files
Files changed (1) hide show
  1. core/database.py +72 -128
core/database.py CHANGED
@@ -1,7 +1,6 @@
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,8 +12,6 @@ from typing import Optional
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,7 +20,6 @@ def get_db() -> sqlite3.Connection:
23
 
24
 
25
  def init_db():
26
- """Create all tables on first run."""
27
  conn = get_db()
28
  c = conn.cursor()
29
 
@@ -67,7 +63,8 @@ def init_db():
67
  importance TEXT DEFAULT 'Important',
68
  state_of_mind TEXT DEFAULT 'Easy',
69
  time_estimate INTEGER DEFAULT 30,
70
- scheduled_date TEXT DEFAULT (date('now')),
 
71
  is_completed INTEGER DEFAULT 0,
72
  actual_duration INTEGER,
73
  is_habit INTEGER DEFAULT 0,
@@ -78,7 +75,15 @@ def init_db():
78
  )
79
  """)
80
 
81
- # AI-learned context stored as a JSON blob per user
 
 
 
 
 
 
 
 
82
  c.execute("""
83
  CREATE TABLE IF NOT EXISTS user_context (
84
  user_id INTEGER PRIMARY KEY,
@@ -95,7 +100,6 @@ def init_db():
95
  # ── Auth ──────────────────────────────────────────────────────────────────────
96
 
97
  def register_user(username: str, password: str) -> tuple:
98
- """Returns (user_id, message). user_id is None on failure."""
99
  username = username.strip().lower()
100
  if not username or not password:
101
  return None, "Username and password cannot be empty."
@@ -104,10 +108,7 @@ def register_user(username: str, password: str) -> tuple:
104
  conn = get_db()
105
  try:
106
  pw_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
107
- conn.execute(
108
- "INSERT INTO users (username, password_hash) VALUES (?, ?)",
109
- (username, pw_hash)
110
- )
111
  conn.commit()
112
  row = conn.execute("SELECT id FROM users WHERE username = ?", (username,)).fetchone()
113
  return row["id"], "Account created!"
@@ -118,14 +119,11 @@ def register_user(username: str, password: str) -> tuple:
118
 
119
 
120
  def login_user(username: str, password: str) -> tuple:
121
- """Returns (user_id, message). user_id is None on failure."""
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(
127
- "SELECT id, password_hash FROM users WHERE username = ?", (username,)
128
- ).fetchone()
129
  conn.close()
130
  if not row:
131
  return None, "Username not found."
@@ -144,68 +142,41 @@ def get_username(user_id: int) -> str:
144
  # ── Life Areas ────────────────────────────────────────────────────────────────
145
 
146
  DEFAULT_AREAS = [
147
- ("Work", "#4F8EF7"),
148
- ("Health", "#4CAF87"),
149
- ("Finance", "#F7A84F"),
150
- ("Learning", "#A855F7"),
151
- ("Personal", "#EC4899"),
152
- ("Family", "#F59E0B"),
153
  ]
154
 
155
-
156
  def create_default_life_areas(user_id: int):
157
  conn = get_db()
158
  for name, color in DEFAULT_AREAS:
159
- conn.execute(
160
- "INSERT INTO life_areas (user_id, name, color) VALUES (?, ?, ?)",
161
- (user_id, name, color)
162
- )
163
  conn.commit()
164
  conn.close()
165
 
166
-
167
  def get_life_areas(user_id: int) -> list:
168
  conn = get_db()
169
- rows = conn.execute(
170
- "SELECT id, name, color FROM life_areas WHERE user_id = ? ORDER BY id",
171
- (user_id,)
172
- ).fetchall()
173
  conn.close()
174
  return [dict(r) for r in rows]
175
 
176
-
177
  def get_life_area_names(user_id: int) -> list:
178
  return [a["name"] for a in get_life_areas(user_id)]
179
 
180
-
181
  def add_life_area(user_id: int, name: str, color: str = "#6366f1") -> tuple:
182
  name = name.strip()
183
- if not name:
184
- return False, "Name cannot be empty."
185
  conn = get_db()
186
- exists = conn.execute(
187
- "SELECT id FROM life_areas WHERE user_id = ? AND LOWER(name) = LOWER(?)",
188
- (user_id, name)
189
- ).fetchone()
190
  if exists:
191
- conn.close()
192
- return False, f'"{name}" already exists.'
193
- conn.execute(
194
- "INSERT INTO life_areas (user_id, name, color) VALUES (?, ?, ?)",
195
- (user_id, name, color)
196
- )
197
- conn.commit()
198
- conn.close()
199
  return True, f'"{name}" added.'
200
 
201
-
202
  def delete_life_area(user_id: int, name: str) -> tuple:
203
  conn = get_db()
204
- conn.execute(
205
- "DELETE FROM life_areas WHERE user_id = ? AND name = ?", (user_id, name)
206
- )
207
- conn.commit()
208
- conn.close()
209
  return True, f'"{name}" removed.'
210
 
211
 
@@ -217,20 +188,12 @@ def save_goals(user_id: int, goals_text: str):
217
  for line in goals_text.strip().splitlines():
218
  line = line.strip("β€’- ").strip()
219
  if line:
220
- conn.execute(
221
- "INSERT INTO user_goals (user_id, goal_text) VALUES (?, ?)",
222
- (user_id, line)
223
- )
224
- conn.commit()
225
- conn.close()
226
-
227
 
228
  def get_goals(user_id: int) -> list:
229
  conn = get_db()
230
- rows = conn.execute(
231
- "SELECT goal_text FROM user_goals WHERE user_id = ? ORDER BY id",
232
- (user_id,)
233
- ).fetchall()
234
  conn.close()
235
  return [r["goal_text"] for r in rows]
236
 
@@ -238,12 +201,16 @@ def get_goals(user_id: int) -> list:
238
  # ── Tasks ─────────────────────────────────────────────────────────────────────
239
 
240
  def save_task(user_id: int, task: dict, scheduled_date: str = None) -> int:
 
 
 
 
241
  conn = get_db()
242
  cursor = conn.execute("""
243
  INSERT INTO tasks
244
  (user_id, title, life_area, urgency, importance, state_of_mind,
245
- time_estimate, scheduled_date, raw_input, is_habit, habit_interval)
246
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
247
  """, (
248
  user_id,
249
  task.get("title", "Untitled"),
@@ -252,31 +219,41 @@ def save_task(user_id: int, task: dict, scheduled_date: str = None) -> int:
252
  task.get("importance", "Important"),
253
  task.get("state_of_mind", "Easy"),
254
  int(task.get("time_estimate") or 30),
255
- scheduled_date or str(date.today()),
 
256
  task.get("raw_input", ""),
257
  1 if task.get("is_habit") else 0,
258
  task.get("habit_interval", ""),
259
  ))
260
  task_id = cursor.lastrowid
261
- conn.commit()
262
- conn.close()
263
  return task_id
264
 
265
 
 
 
 
 
 
 
 
 
 
 
266
  def get_tasks(user_id: int, filter_area: str = "All", only_today: bool = False,
267
- include_completed: bool = True) -> list:
268
  conn = get_db()
269
  q = "SELECT * FROM tasks WHERE user_id = ?"
270
  params = [user_id]
271
  if filter_area and filter_area != "All":
272
- q += " AND life_area = ?"
273
- params.append(filter_area)
274
  if only_today:
275
- q += " AND scheduled_date = ?"
276
- params.append(str(date.today()))
 
277
  if not include_completed:
278
  q += " AND is_completed = 0"
279
- q += " ORDER BY is_completed ASC, created_at DESC"
280
  rows = conn.execute(q, params).fetchall()
281
  conn.close()
282
  return [dict(r) for r in rows]
@@ -284,92 +261,62 @@ def get_tasks(user_id: int, filter_area: str = "All", only_today: bool = False,
284
 
285
  def toggle_task_complete(task_id: int, user_id: int, actual_duration: int = None):
286
  conn = get_db()
287
- task = conn.execute(
288
- "SELECT is_completed FROM tasks WHERE id = ? AND user_id = ?", (task_id, user_id)
289
- ).fetchone()
290
  if task:
291
  new_status = 1 - task["is_completed"]
292
  if actual_duration and new_status == 1:
293
- conn.execute(
294
- "UPDATE tasks SET is_completed = ?, actual_duration = ? WHERE id = ? AND user_id = ?",
295
- (new_status, actual_duration, task_id, user_id)
296
- )
297
  else:
298
- conn.execute(
299
- "UPDATE tasks SET is_completed = ? WHERE id = ? AND user_id = ?",
300
- (new_status, task_id, user_id)
301
- )
302
- conn.commit()
303
- conn.close()
304
 
305
 
306
  def delete_task(task_id: int, user_id: int):
307
  conn = get_db()
308
  conn.execute("DELETE FROM tasks WHERE id = ? AND user_id = ?", (task_id, user_id))
309
- conn.commit()
310
- conn.close()
311
 
312
 
313
  def get_today_stats(user_id: int) -> dict:
314
  tasks = get_tasks(user_id, only_today=True)
315
- total = len(tasks)
316
- done = sum(1 for t in tasks if t["is_completed"])
317
  return {"total": total, "done": done, "remaining": total - done}
318
 
319
 
320
  # ── Habit recurrence ──────────────────────────────────────────────────────────
321
 
322
  def spawn_due_habits(user_id: int):
323
- """
324
- Check all habit tasks. If a habit's scheduled_date < today and
325
- it's not already scheduled for today, create a fresh copy for today.
326
- Called on login / tab load.
327
- """
328
  today = str(date.today())
329
- conn = get_db()
330
- habits = conn.execute(
331
- "SELECT * FROM tasks WHERE user_id = ? AND is_habit = 1",
332
- (user_id,)
333
- ).fetchall()
334
-
335
  for h in habits:
336
- # Check if already exists today
337
  existing = conn.execute(
338
  "SELECT id FROM tasks WHERE user_id = ? AND title = ? AND is_habit = 1 AND scheduled_date = ?",
339
  (user_id, h["title"], today)
340
  ).fetchone()
341
- if existing:
342
- continue
343
- # Only spawn if original was scheduled before today
344
- if h["scheduled_date"] and h["scheduled_date"] >= today:
345
- continue
346
  conn.execute("""
347
  INSERT INTO tasks (user_id, title, life_area, urgency, importance,
348
  state_of_mind, time_estimate, scheduled_date, is_habit,
349
  habit_interval, raw_input)
350
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
351
- """, (
352
- user_id, h["title"], h["life_area"], "Habit",
353
- h["importance"], h["state_of_mind"], h["time_estimate"],
354
- today, h["habit_interval"], h["raw_input"]
355
- ))
356
- conn.commit()
357
- conn.close()
358
 
359
 
360
  # ── AI Context ────────────────────────────────────────────────────────────────
361
 
362
  def load_user_context(user_id: int) -> Optional[dict]:
363
  conn = get_db()
364
- row = conn.execute(
365
- "SELECT context FROM user_context WHERE user_id = ?", (user_id,)
366
- ).fetchone()
367
  conn.close()
368
  if row:
369
- try:
370
- return json.loads(row["context"])
371
- except Exception:
372
- return None
373
  return None
374
 
375
 
@@ -378,9 +325,6 @@ def save_user_context(user_id: int, context: dict):
378
  conn.execute("""
379
  INSERT INTO user_context (user_id, context, updated_at)
380
  VALUES (?, ?, datetime('now'))
381
- ON CONFLICT(user_id) DO UPDATE SET
382
- context = excluded.context,
383
- updated_at = excluded.updated_at
384
  """, (user_id, json.dumps(context)))
385
- conn.commit()
386
- conn.close()
 
1
  """
2
  core/database.py
3
  SQLite persistence for Second Brain.
 
4
  """
5
 
6
  import sqlite3
 
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
 
21
 
22
  def init_db():
 
23
  conn = get_db()
24
  c = conn.cursor()
25
 
 
63
  importance TEXT DEFAULT 'Important',
64
  state_of_mind TEXT DEFAULT 'Easy',
65
  time_estimate INTEGER DEFAULT 30,
66
+ scheduled_date TEXT DEFAULT '',
67
+ deadline_date TEXT DEFAULT '',
68
  is_completed INTEGER DEFAULT 0,
69
  actual_duration INTEGER,
70
  is_habit INTEGER DEFAULT 0,
 
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,
 
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
  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
 
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
  # ── 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
  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
 
 
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
211
  (user_id, title, life_area, urgency, importance, state_of_mind,
212
+ time_estimate, scheduled_date, deadline_date, raw_input, is_habit, habit_interval)
213
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
214
  """, (
215
  user_id,
216
  task.get("title", "Untitled"),
 
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
 
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
  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()