feat: US-002 - Backend API - GET and POST habits

This commit is contained in:
Echo
2026-02-10 15:50:45 +00:00
parent 8f326b1846
commit f9de7a2c26
2 changed files with 425 additions and 0 deletions

View File

@@ -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')