avaliev commited on
Commit
c6ad652
·
verified ·
1 Parent(s): 3fc4713

Upload 4 files

Browse files

Update to in-memory demo storage for HF spaces

Files changed (4) hide show
  1. app.py +8 -59
  2. mcp_tools.py +3 -6
  3. shared.py +17 -0
  4. storage.py +171 -58
app.py CHANGED
@@ -5,9 +5,8 @@ Configurable via environment variables for HuggingFace Spaces or local use.
5
  import gradio as gr
6
  import os
7
  from dotenv import load_dotenv
8
- from storage import TaskManager
9
  from monitor import FileMonitor
10
- from metrics import MetricsTracker
11
  from voice import voice_generator
12
  from linear_client import LinearClient
13
  from core.pomodoro import PomodoroTimer
@@ -18,23 +17,6 @@ from ui.layout import create_app
18
  # Load environment variables
19
  load_dotenv()
20
 
21
- # Debug: Print Configuration
22
- print("-" * 40)
23
- print("🔧 FOCUSFLOW CONFIGURATION")
24
- print(f" LAUNCH_MODE: {os.getenv('LAUNCH_MODE', 'demo')}")
25
- print(f" AI_PROVIDER: {os.getenv('AI_PROVIDER', 'openai')}")
26
- print(f" MONITOR_INTERVAL: {os.getenv('MONITOR_INTERVAL', '30')}")
27
- print(f" ENABLE_MCP: {os.getenv('ENABLE_MCP', 'true')}")
28
- print(f" HF_SPACE: {'Yes' if os.getenv('SPACE_ID') or os.getenv('huggingface_space_id') else 'No'}")
29
-
30
- # Masked Keys for safety
31
- for key in ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GEMINI_API_KEY", "LINEAR_API_KEY",
32
- "DEMO_OPENAI_API_KEY", "DEMO_ANTHROPIC_API_KEY", "DEMO_GEMINI_API_KEY"]:
33
- val = os.getenv(key)
34
- status = f"Set ({val[:4]}...)" if val else "Not Set"
35
- print(f" {key:<22}: {status}")
36
- print("-" * 40)
37
-
38
  # Import MCP tools to register them with Gradio
39
  try:
40
  import mcp_tools
@@ -49,9 +31,8 @@ AI_PROVIDER = os.getenv("AI_PROVIDER", "openai").lower() # 'openai', 'anthropic
49
  MONITOR_INTERVAL = int(os.getenv("MONITOR_INTERVAL", "30")) # seconds
50
 
51
  # Initialize Core Components
52
- task_manager = TaskManager()
53
  file_monitor = FileMonitor()
54
- metrics_tracker = MetricsTracker()
55
  linear_client = LinearClient()
56
 
57
  # Initialize Logic Modules
@@ -63,32 +44,6 @@ pomodoro_timer = PomodoroTimer()
63
  # Initialize UI Handlers
64
  ui_handlers = UIHandlers(task_manager, file_monitor, metrics_tracker, focus_monitor, linear_client)
65
 
66
- # Register cleanup handler
67
- import atexit
68
- import signal
69
- import sys
70
-
71
- def cleanup():
72
- """Cleanup resources on shutdown."""
73
- print("🧹 Cleaning up resources...")
74
- if file_monitor.is_running():
75
- print(" Stopping file monitor...")
76
- file_monitor.stop()
77
- try:
78
- print(" Closing Gradio app...")
79
- app.close()
80
- except Exception as e:
81
- print(f" Error closing app: {e}")
82
-
83
- atexit.register(cleanup)
84
-
85
- def handle_sigterm(*args):
86
- """Handle SIGTERM signal from K8s/Docker."""
87
- print("🛑 Received SIGTERM, initiating shutdown...")
88
- sys.exit(0)
89
-
90
- signal.signal(signal.SIGTERM, handle_sigterm)
91
-
92
  # Create App
93
  app = create_app(ui_handlers, pomodoro_timer, LAUNCH_MODE, AI_PROVIDER, MONITOR_INTERVAL)
94
 
@@ -96,15 +51,9 @@ if __name__ == "__main__":
96
  # Enable MCP server if available
97
  mcp_enabled = os.getenv("ENABLE_MCP", "true").lower() == "true"
98
 
99
- try:
100
- if MCP_AVAILABLE and mcp_enabled:
101
- print("🔗 MCP Server enabled! Connect via Claude Desktop or other MCP clients.")
102
- app.launch(server_name="0.0.0.0", server_port=7860, share=False, mcp_server=True)
103
- else:
104
- print("📱 Running without MCP integration")
105
- app.launch(server_name="0.0.0.0", server_port=7860, share=False)
106
- except KeyboardInterrupt:
107
- print("👋 FocusFlow stopped by user")
108
- except Exception as e:
109
- print(f"❌ Unexpected error: {e}")
110
-
 
5
  import gradio as gr
6
  import os
7
  from dotenv import load_dotenv
8
+ from shared import task_manager, metrics_tracker
9
  from monitor import FileMonitor
 
10
  from voice import voice_generator
11
  from linear_client import LinearClient
12
  from core.pomodoro import PomodoroTimer
 
17
  # Load environment variables
18
  load_dotenv()
19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  # Import MCP tools to register them with Gradio
21
  try:
22
  import mcp_tools
 
31
  MONITOR_INTERVAL = int(os.getenv("MONITOR_INTERVAL", "30")) # seconds
32
 
33
  # Initialize Core Components
34
+ # task_manager and metrics_tracker are imported from shared.py
35
  file_monitor = FileMonitor()
 
36
  linear_client = LinearClient()
37
 
38
  # Initialize Logic Modules
 
44
  # Initialize UI Handlers
45
  ui_handlers = UIHandlers(task_manager, file_monitor, metrics_tracker, focus_monitor, linear_client)
46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  # Create App
48
  app = create_app(ui_handlers, pomodoro_timer, LAUNCH_MODE, AI_PROVIDER, MONITOR_INTERVAL)
49
 
 
51
  # Enable MCP server if available
52
  mcp_enabled = os.getenv("ENABLE_MCP", "true").lower() == "true"
53
 
54
+ if MCP_AVAILABLE and mcp_enabled:
55
+ print("🔗 MCP Server enabled! Connect via Claude Desktop or other MCP clients.")
56
+ app.launch(server_name="0.0.0.0", server_port=5000, share=False, mcp_server=True)
57
+ else:
58
+ print("📱 Running without MCP integration")
59
+ app.launch(server_name="0.0.0.0", server_port=5000, share=False)
 
 
 
 
 
 
mcp_tools.py CHANGED
@@ -4,13 +4,10 @@ Enables LLM assistants like Claude Desktop to interact with FocusFlow.
4
  """
5
  import gradio as gr
6
  from typing import Dict, List, Optional
7
- from storage import TaskManager
8
- from metrics import MetricsTracker
9
 
10
- # Initialize shared instances for MCP tools
11
- # Note: These are separate from app.py's instances but use the same database
12
- task_manager = TaskManager()
13
- metrics_tracker = MetricsTracker()
14
 
15
 
16
  @gr.mcp.tool()
 
4
  """
5
  import gradio as gr
6
  from typing import Dict, List, Optional
7
+ from shared import task_manager, metrics_tracker
 
8
 
9
+ # Shared instances are imported from shared.py
10
+ # This ensures MCP tools interact with the same data as the main app
 
 
11
 
12
 
13
  @gr.mcp.tool()
shared.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Shared module to hold singleton instances.
3
+ Ensures app.py and mcp_tools.py share the same state.
4
+ """
5
+ from storage import TaskManager
6
+ from metrics import MetricsTracker
7
+ import os
8
+
9
+ # Configuration
10
+ LAUNCH_MODE = os.getenv("LAUNCH_MODE", "demo").lower()
11
+
12
+ # Initialize singletons
13
+ # For demo mode, we use in-memory storage to avoid filesystem issues in HF Spaces
14
+ use_memory = (LAUNCH_MODE == "demo")
15
+
16
+ task_manager = TaskManager(use_memory=use_memory)
17
+ metrics_tracker = MetricsTracker()
storage.py CHANGED
@@ -10,187 +10,300 @@ import os
10
 
11
  class TaskManager:
12
  """Manages tasks with SQLite persistence."""
13
-
14
  # Strict status enum
15
  VALID_STATUSES = {"Todo", "In Progress", "Done"}
16
-
17
- def __init__(self, db_path: str = "focusflow.db"):
18
- """Initialize the task manager with SQLite database."""
 
 
 
 
 
 
19
  self.db_path = db_path
20
- self._init_db()
21
-
 
 
 
 
 
 
 
22
  def _init_db(self):
23
  """Create the tasks table if it doesn't exist."""
24
- conn = sqlite3.connect(self.db_path)
25
- cursor = conn.cursor()
26
- cursor.execute("""
27
- CREATE TABLE IF NOT EXISTS tasks (
28
- id INTEGER PRIMARY KEY AUTOINCREMENT,
29
- title TEXT NOT NULL,
30
- description TEXT,
31
- status TEXT DEFAULT 'Todo',
32
- estimated_duration TEXT,
33
- position INTEGER,
34
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
35
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
36
- )
37
- """)
38
- conn.commit()
39
- conn.close()
40
-
41
- def add_task(self, title: str, description: str = "",
 
 
 
 
 
 
42
  estimated_duration: str = "", status: str = "Todo") -> int:
43
  """Add a new task and return its ID."""
44
  # Validate status
45
  if status not in self.VALID_STATUSES:
46
  status = "Todo"
47
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  conn = sqlite3.connect(self.db_path)
49
  cursor = conn.cursor()
50
-
51
  # Get max position
52
  cursor.execute("SELECT MAX(position) FROM tasks")
53
- max_pos = cursor.fetchone()[0]
54
- position = (max_pos or 0) + 1
55
-
 
56
  cursor.execute("""
57
  INSERT INTO tasks (title, description, status, estimated_duration, position)
58
  VALUES (?, ?, ?, ?, ?)
59
  """, (title, description, status, estimated_duration, position))
60
-
61
  task_id = cursor.lastrowid or 0
62
  conn.commit()
63
  conn.close()
64
  return task_id
65
-
66
  def get_all_tasks(self) -> List[Dict]:
67
  """Get all tasks ordered by position."""
 
 
 
68
  conn = sqlite3.connect(self.db_path)
69
  conn.row_factory = sqlite3.Row
70
  cursor = conn.cursor()
71
-
72
  cursor.execute("""
73
  SELECT id, title, description, status, estimated_duration, position
74
  FROM tasks ORDER BY position
75
  """)
76
-
77
  tasks = [dict(row) for row in cursor.fetchall()]
78
  conn.close()
79
  return tasks
80
-
81
  def get_task(self, task_id: int) -> Optional[Dict]:
82
  """Get a specific task by ID."""
 
 
 
 
 
 
83
  conn = sqlite3.connect(self.db_path)
84
  conn.row_factory = sqlite3.Row
85
  cursor = conn.cursor()
86
-
87
  cursor.execute("""
88
  SELECT id, title, description, status, estimated_duration, position
89
  FROM tasks WHERE id = ?
90
  """, (task_id,))
91
-
92
  row = cursor.fetchone()
93
  conn.close()
94
  return dict(row) if row else None
95
-
96
  def update_task(self, task_id: int, **kwargs):
97
  """Update a task's fields with validation."""
98
  # Validate status if provided
99
  if 'status' in kwargs and kwargs['status'] not in self.VALID_STATUSES:
100
  raise ValueError(f"Invalid status. Must be one of: {', '.join(self.VALID_STATUSES)}")
101
-
 
 
 
 
 
 
 
 
 
 
 
 
102
  conn = sqlite3.connect(self.db_path)
103
  cursor = conn.cursor()
104
-
105
- allowed_fields = ['title', 'description', 'status', 'estimated_duration', 'position']
106
  updates = []
107
  values = []
108
-
109
  for key, value in kwargs.items():
110
  if key in allowed_fields:
111
  updates.append(f"{key} = ?")
112
  values.append(value)
113
-
114
  if updates:
115
  values.append(task_id)
116
  query = f"UPDATE tasks SET {', '.join(updates)}, updated_at = CURRENT_TIMESTAMP WHERE id = ?"
117
  cursor.execute(query, values)
118
  conn.commit()
119
-
120
  conn.close()
121
-
122
  def delete_task(self, task_id: int):
123
  """Delete a task by ID."""
 
 
 
 
124
  conn = sqlite3.connect(self.db_path)
125
  cursor = conn.cursor()
126
  cursor.execute("DELETE FROM tasks WHERE id = ?", (task_id,))
127
  conn.commit()
128
  conn.close()
129
-
130
  def reorder_tasks(self, task_ids: List[int]):
131
  """Reorder tasks based on new order."""
 
 
 
 
 
 
 
 
 
 
132
  conn = sqlite3.connect(self.db_path)
133
  cursor = conn.cursor()
134
-
135
  for position, task_id in enumerate(task_ids, start=1):
136
  cursor.execute("UPDATE tasks SET position = ? WHERE id = ?", (position, task_id))
137
-
138
  conn.commit()
139
  conn.close()
140
-
141
  def get_active_task(self) -> Optional[Dict]:
142
  """Get the task marked as 'In Progress'."""
 
 
 
 
 
 
 
 
143
  conn = sqlite3.connect(self.db_path)
144
  conn.row_factory = sqlite3.Row
145
  cursor = conn.cursor()
146
-
147
  cursor.execute("""
148
  SELECT id, title, description, status, estimated_duration
149
  FROM tasks WHERE status = 'In Progress'
150
  ORDER BY position LIMIT 1
151
  """)
152
-
153
  row = cursor.fetchone()
154
  conn.close()
155
  return dict(row) if row else None
156
-
157
  def set_active_task(self, task_id: int) -> bool:
158
  """Set a task as 'In Progress' and ensure only one task has this status.
159
  Returns True if successful, False otherwise."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  conn = sqlite3.connect(self.db_path)
161
  cursor = conn.cursor()
162
-
163
  # Check if task exists and is not already Done
164
  cursor.execute("SELECT status FROM tasks WHERE id = ?", (task_id,))
165
  result = cursor.fetchone()
166
-
167
  if not result:
168
  conn.close()
169
  return False
170
-
171
  current_status = result[0]
172
  if current_status == "Done":
173
  conn.close()
174
  return False
175
-
176
  # Enforce single "In Progress" rule: set all current "In Progress" tasks back to "Todo"
177
  cursor.execute("""
178
  UPDATE tasks SET status = 'Todo', updated_at = CURRENT_TIMESTAMP
179
  WHERE status = 'In Progress'
180
  """)
181
-
182
  # Set the selected task as 'In Progress'
183
  cursor.execute("""
184
  UPDATE tasks SET status = 'In Progress', updated_at = CURRENT_TIMESTAMP
185
  WHERE id = ?
186
  """, (task_id,))
187
-
188
  conn.commit()
189
  conn.close()
190
  return True
191
-
192
  def clear_all_tasks(self):
193
  """Clear all tasks from the database."""
 
 
 
 
194
  conn = sqlite3.connect(self.db_path)
195
  cursor = conn.cursor()
196
  cursor.execute("DELETE FROM tasks")
 
10
 
11
  class TaskManager:
12
  """Manages tasks with SQLite persistence."""
13
+
14
  # Strict status enum
15
  VALID_STATUSES = {"Todo", "In Progress", "Done"}
16
+
17
+ def __init__(self, db_path: str = "focusflow.db", use_memory: bool = False):
18
+ """
19
+ Initialize the task manager.
20
+
21
+ Args:
22
+ db_path: Path to SQLite database file
23
+ use_memory: If True, use in-memory list instead of SQLite (for Demo/HF Spaces)
24
+ """
25
  self.db_path = db_path
26
+ self.use_memory = use_memory
27
+ self.memory_tasks = [] # List of dicts for in-memory storage
28
+ self.memory_counter = 0 # Auto-increment ID for in-memory
29
+
30
+ if not self.use_memory:
31
+ self._init_db()
32
+ else:
33
+ print("ℹ️ TaskManager initialized in IN-MEMORY mode (non-persistent)")
34
+
35
  def _init_db(self):
36
  """Create the tasks table if it doesn't exist."""
37
+ try:
38
+ conn = sqlite3.connect(self.db_path)
39
+ cursor = conn.cursor()
40
+ cursor.execute("""
41
+ CREATE TABLE IF NOT EXISTS tasks (
42
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
43
+ title TEXT NOT NULL,
44
+ description TEXT,
45
+ status TEXT DEFAULT 'Todo',
46
+ estimated_duration TEXT,
47
+ position INTEGER,
48
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
49
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
50
+ )
51
+ """)
52
+ conn.commit()
53
+ conn.close()
54
+ except Exception as e:
55
+ print(f"⚠️ Database initialization failed: {e}. Falling back to in-memory mode.")
56
+ self.use_memory = True
57
+ self.memory_tasks = []
58
+ self.memory_counter = 0
59
+
60
+ def add_task(self, title: str, description: str = "",
61
  estimated_duration: str = "", status: str = "Todo") -> int:
62
  """Add a new task and return its ID."""
63
  # Validate status
64
  if status not in self.VALID_STATUSES:
65
  status = "Todo"
66
+
67
+ if self.use_memory:
68
+ self.memory_counter += 1
69
+
70
+ # Calculate position
71
+ max_pos = 0
72
+ if self.memory_tasks:
73
+ max_pos = max(t.get('position', 0) for t in self.memory_tasks)
74
+ position = max_pos + 1
75
+
76
+ new_task = {
77
+ "id": self.memory_counter,
78
+ "title": title,
79
+ "description": description,
80
+ "status": status,
81
+ "estimated_duration": estimated_duration,
82
+ "position": position,
83
+ "created_at": datetime.now().isoformat(),
84
+ "updated_at": datetime.now().isoformat()
85
+ }
86
+ self.memory_tasks.append(new_task)
87
+ return self.memory_counter
88
+
89
  conn = sqlite3.connect(self.db_path)
90
  cursor = conn.cursor()
91
+
92
  # Get max position
93
  cursor.execute("SELECT MAX(position) FROM tasks")
94
+ result = cursor.fetchone()
95
+ max_pos = result[0] if result and result[0] is not None else 0
96
+ position = max_pos + 1
97
+
98
  cursor.execute("""
99
  INSERT INTO tasks (title, description, status, estimated_duration, position)
100
  VALUES (?, ?, ?, ?, ?)
101
  """, (title, description, status, estimated_duration, position))
102
+
103
  task_id = cursor.lastrowid or 0
104
  conn.commit()
105
  conn.close()
106
  return task_id
107
+
108
  def get_all_tasks(self) -> List[Dict]:
109
  """Get all tasks ordered by position."""
110
+ if self.use_memory:
111
+ return sorted(self.memory_tasks, key=lambda x: x.get('position', 0))
112
+
113
  conn = sqlite3.connect(self.db_path)
114
  conn.row_factory = sqlite3.Row
115
  cursor = conn.cursor()
116
+
117
  cursor.execute("""
118
  SELECT id, title, description, status, estimated_duration, position
119
  FROM tasks ORDER BY position
120
  """)
121
+
122
  tasks = [dict(row) for row in cursor.fetchall()]
123
  conn.close()
124
  return tasks
125
+
126
  def get_task(self, task_id: int) -> Optional[Dict]:
127
  """Get a specific task by ID."""
128
+ if self.use_memory:
129
+ for task in self.memory_tasks:
130
+ if task['id'] == task_id:
131
+ return task.copy()
132
+ return None
133
+
134
  conn = sqlite3.connect(self.db_path)
135
  conn.row_factory = sqlite3.Row
136
  cursor = conn.cursor()
137
+
138
  cursor.execute("""
139
  SELECT id, title, description, status, estimated_duration, position
140
  FROM tasks WHERE id = ?
141
  """, (task_id,))
142
+
143
  row = cursor.fetchone()
144
  conn.close()
145
  return dict(row) if row else None
146
+
147
  def update_task(self, task_id: int, **kwargs):
148
  """Update a task's fields with validation."""
149
  # Validate status if provided
150
  if 'status' in kwargs and kwargs['status'] not in self.VALID_STATUSES:
151
  raise ValueError(f"Invalid status. Must be one of: {', '.join(self.VALID_STATUSES)}")
152
+
153
+ allowed_fields = ['title', 'description', 'status', 'estimated_duration', 'position']
154
+
155
+ if self.use_memory:
156
+ for task in self.memory_tasks:
157
+ if task['id'] == task_id:
158
+ for key, value in kwargs.items():
159
+ if key in allowed_fields:
160
+ task[key] = value
161
+ task['updated_at'] = datetime.now().isoformat()
162
+ return
163
+ return
164
+
165
  conn = sqlite3.connect(self.db_path)
166
  cursor = conn.cursor()
167
+
 
168
  updates = []
169
  values = []
170
+
171
  for key, value in kwargs.items():
172
  if key in allowed_fields:
173
  updates.append(f"{key} = ?")
174
  values.append(value)
175
+
176
  if updates:
177
  values.append(task_id)
178
  query = f"UPDATE tasks SET {', '.join(updates)}, updated_at = CURRENT_TIMESTAMP WHERE id = ?"
179
  cursor.execute(query, values)
180
  conn.commit()
181
+
182
  conn.close()
183
+
184
  def delete_task(self, task_id: int):
185
  """Delete a task by ID."""
186
+ if self.use_memory:
187
+ self.memory_tasks = [t for t in self.memory_tasks if t['id'] != task_id]
188
+ return
189
+
190
  conn = sqlite3.connect(self.db_path)
191
  cursor = conn.cursor()
192
  cursor.execute("DELETE FROM tasks WHERE id = ?", (task_id,))
193
  conn.commit()
194
  conn.close()
195
+
196
  def reorder_tasks(self, task_ids: List[int]):
197
  """Reorder tasks based on new order."""
198
+ if self.use_memory:
199
+ # Create a map for O(1) lookup
200
+ task_map = {t['id']: t for t in self.memory_tasks}
201
+
202
+ # Update positions based on the input list order
203
+ for position, task_id in enumerate(task_ids, start=1):
204
+ if task_id in task_map:
205
+ task_map[task_id]['position'] = position
206
+ return
207
+
208
  conn = sqlite3.connect(self.db_path)
209
  cursor = conn.cursor()
210
+
211
  for position, task_id in enumerate(task_ids, start=1):
212
  cursor.execute("UPDATE tasks SET position = ? WHERE id = ?", (position, task_id))
213
+
214
  conn.commit()
215
  conn.close()
216
+
217
  def get_active_task(self) -> Optional[Dict]:
218
  """Get the task marked as 'In Progress'."""
219
+ if self.use_memory:
220
+ # Filter for In Progress tasks and sort by position
221
+ active_tasks = [t for t in self.memory_tasks if t['status'] == 'In Progress']
222
+ if not active_tasks:
223
+ return None
224
+ # Return the first one (lowest position)
225
+ return sorted(active_tasks, key=lambda x: x.get('position', 0))[0].copy()
226
+
227
  conn = sqlite3.connect(self.db_path)
228
  conn.row_factory = sqlite3.Row
229
  cursor = conn.cursor()
230
+
231
  cursor.execute("""
232
  SELECT id, title, description, status, estimated_duration
233
  FROM tasks WHERE status = 'In Progress'
234
  ORDER BY position LIMIT 1
235
  """)
236
+
237
  row = cursor.fetchone()
238
  conn.close()
239
  return dict(row) if row else None
240
+
241
  def set_active_task(self, task_id: int) -> bool:
242
  """Set a task as 'In Progress' and ensure only one task has this status.
243
  Returns True if successful, False otherwise."""
244
+ if self.use_memory:
245
+ # Check if task exists
246
+ target_task = None
247
+ for t in self.memory_tasks:
248
+ if t['id'] == task_id:
249
+ target_task = t
250
+ break
251
+
252
+ if not target_task:
253
+ return False
254
+
255
+ if target_task['status'] == 'Done':
256
+ return False
257
+
258
+ # Reset other In Progress tasks
259
+ for t in self.memory_tasks:
260
+ if t['status'] == 'In Progress':
261
+ t['status'] = 'Todo'
262
+ t['updated_at'] = datetime.now().isoformat()
263
+
264
+ # Set target task
265
+ target_task['status'] = 'In Progress'
266
+ target_task['updated_at'] = datetime.now().isoformat()
267
+ return True
268
+
269
  conn = sqlite3.connect(self.db_path)
270
  cursor = conn.cursor()
271
+
272
  # Check if task exists and is not already Done
273
  cursor.execute("SELECT status FROM tasks WHERE id = ?", (task_id,))
274
  result = cursor.fetchone()
275
+
276
  if not result:
277
  conn.close()
278
  return False
279
+
280
  current_status = result[0]
281
  if current_status == "Done":
282
  conn.close()
283
  return False
284
+
285
  # Enforce single "In Progress" rule: set all current "In Progress" tasks back to "Todo"
286
  cursor.execute("""
287
  UPDATE tasks SET status = 'Todo', updated_at = CURRENT_TIMESTAMP
288
  WHERE status = 'In Progress'
289
  """)
290
+
291
  # Set the selected task as 'In Progress'
292
  cursor.execute("""
293
  UPDATE tasks SET status = 'In Progress', updated_at = CURRENT_TIMESTAMP
294
  WHERE id = ?
295
  """, (task_id,))
296
+
297
  conn.commit()
298
  conn.close()
299
  return True
300
+
301
  def clear_all_tasks(self):
302
  """Clear all tasks from the database."""
303
+ if self.use_memory:
304
+ self.memory_tasks = []
305
+ return
306
+
307
  conn = sqlite3.connect(self.db_path)
308
  cursor = conn.cursor()
309
  cursor.execute("DELETE FROM tasks")