feat: US-002 - Backend API - GET and POST habits
This commit is contained in:
126
dashboard/api.py
126
dashboard/api.py
@@ -11,16 +11,22 @@ import sys
|
||||
import re
|
||||
import os
|
||||
import signal
|
||||
import uuid
|
||||
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
# Import habits helpers
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
import habits_helpers
|
||||
|
||||
BASE_DIR = Path(__file__).parent.parent
|
||||
TOOLS_DIR = BASE_DIR / 'tools'
|
||||
NOTES_DIR = BASE_DIR / 'kb' / 'youtube'
|
||||
KANBAN_DIR = BASE_DIR / 'dashboard'
|
||||
WORKSPACE_DIR = Path('/home/moltbot/workspace')
|
||||
HABITS_FILE = KANBAN_DIR / 'habits.json'
|
||||
|
||||
# Load .env file if present
|
||||
_env_file = Path(__file__).parent / '.env'
|
||||
@@ -48,6 +54,8 @@ class TaskBoardHandler(SimpleHTTPRequestHandler):
|
||||
self.handle_git_commit()
|
||||
elif self.path == '/api/pdf':
|
||||
self.handle_pdf_post()
|
||||
elif self.path == '/api/habits':
|
||||
self.handle_habits_post()
|
||||
elif self.path == '/api/workspace/run':
|
||||
self.handle_workspace_run()
|
||||
elif self.path == '/api/workspace/stop':
|
||||
@@ -251,6 +259,8 @@ class TaskBoardHandler(SimpleHTTPRequestHandler):
|
||||
self.handle_cron_status()
|
||||
elif self.path == '/api/activity' or self.path.startswith('/api/activity?'):
|
||||
self.handle_activity()
|
||||
elif self.path == '/api/habits':
|
||||
self.handle_habits_get()
|
||||
elif self.path.startswith('/api/files'):
|
||||
self.handle_files_get()
|
||||
elif self.path.startswith('/api/diff'):
|
||||
@@ -1381,6 +1391,122 @@ class TaskBoardHandler(SimpleHTTPRequestHandler):
|
||||
except Exception as e:
|
||||
self.send_json({'error': str(e)}, 500)
|
||||
|
||||
def handle_habits_get(self):
|
||||
"""Get all habits with enriched stats."""
|
||||
try:
|
||||
# Read habits file
|
||||
if not HABITS_FILE.exists():
|
||||
self.send_json([])
|
||||
return
|
||||
|
||||
with open(HABITS_FILE, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
habits = data.get('habits', [])
|
||||
|
||||
# Enrich each habit with calculated stats
|
||||
enriched_habits = []
|
||||
for habit in habits:
|
||||
# Calculate stats using helpers
|
||||
current_streak = habits_helpers.calculate_streak(habit)
|
||||
best_streak = habit.get('streak', {}).get('best', 0)
|
||||
completion_rate = habits_helpers.get_completion_rate(habit, days=30)
|
||||
weekly_summary = habits_helpers.get_weekly_summary(habit)
|
||||
|
||||
# Add stats to habit
|
||||
enriched = habit.copy()
|
||||
enriched['current_streak'] = current_streak
|
||||
enriched['best_streak'] = best_streak
|
||||
enriched['completion_rate_30d'] = completion_rate
|
||||
enriched['weekly_summary'] = weekly_summary
|
||||
|
||||
enriched_habits.append(enriched)
|
||||
|
||||
# Sort by priority ascending (lower number = higher priority)
|
||||
enriched_habits.sort(key=lambda h: h.get('priority', 999))
|
||||
|
||||
self.send_json(enriched_habits)
|
||||
except Exception as e:
|
||||
self.send_json({'error': str(e)}, 500)
|
||||
|
||||
def handle_habits_post(self):
|
||||
"""Create a new habit."""
|
||||
try:
|
||||
# Read request body
|
||||
content_length = int(self.headers['Content-Length'])
|
||||
post_data = self.rfile.read(content_length).decode('utf-8')
|
||||
data = json.loads(post_data)
|
||||
|
||||
# Validate required fields
|
||||
name = data.get('name', '').strip()
|
||||
if not name:
|
||||
self.send_json({'error': 'name is required'}, 400)
|
||||
return
|
||||
|
||||
if len(name) > 100:
|
||||
self.send_json({'error': 'name must be max 100 characters'}, 400)
|
||||
return
|
||||
|
||||
# Validate color (hex format)
|
||||
color = data.get('color', '#3b82f6')
|
||||
if color and not re.match(r'^#[0-9A-Fa-f]{6}$', color):
|
||||
self.send_json({'error': 'color must be valid hex format (#RRGGBB)'}, 400)
|
||||
return
|
||||
|
||||
# Validate frequency type
|
||||
frequency_type = data.get('frequency', {}).get('type', 'daily')
|
||||
valid_types = ['daily', 'specific_days', 'x_per_week', 'weekly', 'monthly', 'custom']
|
||||
if frequency_type not in valid_types:
|
||||
self.send_json({'error': f'frequency.type must be one of: {", ".join(valid_types)}'}, 400)
|
||||
return
|
||||
|
||||
# Create new habit
|
||||
habit_id = str(uuid.uuid4())
|
||||
now = datetime.now().isoformat()
|
||||
|
||||
new_habit = {
|
||||
'id': habit_id,
|
||||
'name': name,
|
||||
'category': data.get('category', 'other'),
|
||||
'color': color,
|
||||
'icon': data.get('icon', 'check-circle'),
|
||||
'priority': data.get('priority', 5),
|
||||
'notes': data.get('notes', ''),
|
||||
'reminderTime': data.get('reminderTime', ''),
|
||||
'frequency': data.get('frequency', {'type': 'daily'}),
|
||||
'streak': {
|
||||
'current': 0,
|
||||
'best': 0,
|
||||
'lastCheckIn': None
|
||||
},
|
||||
'lives': 3,
|
||||
'completions': [],
|
||||
'createdAt': now,
|
||||
'updatedAt': now
|
||||
}
|
||||
|
||||
# Read existing habits
|
||||
if HABITS_FILE.exists():
|
||||
with open(HABITS_FILE, 'r', encoding='utf-8') as f:
|
||||
habits_data = json.load(f)
|
||||
else:
|
||||
habits_data = {'lastUpdated': '', 'habits': []}
|
||||
|
||||
# Add new habit
|
||||
habits_data['habits'].append(new_habit)
|
||||
habits_data['lastUpdated'] = now
|
||||
|
||||
# Save to file
|
||||
with open(HABITS_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(habits_data, f, indent=2)
|
||||
|
||||
# Return created habit with 201 status
|
||||
self.send_json(new_habit, 201)
|
||||
except json.JSONDecodeError:
|
||||
self.send_json({'error': 'Invalid JSON'}, 400)
|
||||
except Exception as e:
|
||||
self.send_json({'error': str(e)}, 500)
|
||||
|
||||
def send_json(self, data, code=200):
|
||||
self.send_response(code)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
|
||||
Reference in New Issue
Block a user