feat: US-004 - Backend API - Check-in endpoint with streak logic

This commit is contained in:
Echo
2026-02-10 16:06:34 +00:00
parent 648185abe6
commit 71bcc5f6f6
2 changed files with 398 additions and 2 deletions

View File

@@ -56,6 +56,8 @@ class TaskBoardHandler(SimpleHTTPRequestHandler):
self.handle_pdf_post()
elif self.path == '/api/habits':
self.handle_habits_post()
elif self.path.startswith('/api/habits/') and self.path.endswith('/check'):
self.handle_habits_check()
elif self.path == '/api/workspace/run':
self.handle_workspace_run()
elif self.path == '/api/workspace/stop':
@@ -1648,6 +1650,124 @@ class TaskBoardHandler(SimpleHTTPRequestHandler):
except Exception as e:
self.send_json({'error': str(e)}, 500)
def handle_habits_check(self):
"""Check in on a habit (complete it for today)."""
try:
# Extract habit ID from path (/api/habits/{id}/check)
path_parts = self.path.split('/')
if len(path_parts) < 5:
self.send_json({'error': 'Invalid path'}, 400)
return
habit_id = path_parts[3]
# Read optional body (note, rating, mood)
body_data = {}
content_length = self.headers.get('Content-Length')
if content_length:
post_data = self.rfile.read(int(content_length)).decode('utf-8')
if post_data.strip():
try:
body_data = json.loads(post_data)
except json.JSONDecodeError:
self.send_json({'error': 'Invalid JSON'}, 400)
return
# Read existing habits
if not HABITS_FILE.exists():
self.send_json({'error': 'Habit not found'}, 404)
return
with open(HABITS_FILE, 'r', encoding='utf-8') as f:
habits_data = json.load(f)
# Find habit
habit = None
for h in habits_data.get('habits', []):
if h['id'] == habit_id:
habit = h
break
if not habit:
self.send_json({'error': 'Habit not found'}, 404)
return
# Verify habit is relevant for today
if not habits_helpers.should_check_today(habit):
self.send_json({'error': 'Habit is not relevant for today based on its frequency'}, 400)
return
# Verify not already checked today
today = datetime.now().date().isoformat()
completions = habit.get('completions', [])
for completion in completions:
if completion.get('date') == today:
self.send_json({'error': 'Habit already checked in today'}, 409)
return
# Create completion entry
completion_entry = {
'date': today,
'type': 'check' # Distinguish from 'skip' for life restore logic
}
# Add optional fields
if 'note' in body_data:
completion_entry['note'] = body_data['note']
if 'rating' in body_data:
rating = body_data['rating']
if not isinstance(rating, int) or rating < 1 or rating > 5:
self.send_json({'error': 'rating must be an integer between 1 and 5'}, 400)
return
completion_entry['rating'] = rating
if 'mood' in body_data:
mood = body_data['mood']
if mood not in ['happy', 'neutral', 'sad']:
self.send_json({'error': 'mood must be one of: happy, neutral, sad'}, 400)
return
completion_entry['mood'] = mood
# Add completion to habit
habit['completions'].append(completion_entry)
# Recalculate streak
current_streak = habits_helpers.calculate_streak(habit)
habit['streak']['current'] = current_streak
# Update best streak if current is higher
if current_streak > habit['streak']['best']:
habit['streak']['best'] = current_streak
# Update lastCheckIn
habit['streak']['lastCheckIn'] = today
# Check for life restore: if last 7 completions are all check-ins (no skips) and lives < 3
if habit.get('lives', 3) < 3:
recent_completions = sorted(
habit['completions'],
key=lambda x: x.get('date', ''),
reverse=True
)[:7]
# Check if we have 7 completions and all are check-ins (not skips)
if len(recent_completions) == 7:
all_checks = all(c.get('type') == 'check' for c in recent_completions)
if all_checks:
habit['lives'] = min(habit['lives'] + 1, 3)
# Update timestamp
habit['updatedAt'] = datetime.now().isoformat()
habits_data['lastUpdated'] = habit['updatedAt']
# Save to file
with open(HABITS_FILE, 'w', encoding='utf-8') as f:
json.dump(habits_data, f, indent=2)
# Return updated habit
self.send_json(habit, 200)
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')