diff --git a/dashboard/habits.html b/dashboard/habits.html
index 8b41432..40db2ef 100644
--- a/dashboard/habits.html
+++ b/dashboard/habits.html
@@ -198,10 +198,37 @@
.habit-card-lives {
display: flex;
justify-content: center;
- gap: var(--space-1);
+ align-items: center;
+ gap: var(--space-2);
font-size: var(--text-lg);
}
+ .habit-card-lives-hearts {
+ display: flex;
+ gap: var(--space-1);
+ }
+
+ .habit-card-skip-btn {
+ background: none;
+ border: none;
+ color: var(--text-muted);
+ font-size: var(--text-xs);
+ cursor: pointer;
+ padding: var(--space-1) var(--space-2);
+ border-radius: var(--radius-sm);
+ transition: all var(--transition-base);
+ }
+
+ .habit-card-skip-btn:hover:not(:disabled) {
+ background: var(--bg-hover);
+ color: var(--text-primary);
+ }
+
+ .habit-card-skip-btn:disabled {
+ cursor: not-allowed;
+ opacity: 0.3;
+ }
+
.habit-card-completion {
font-size: var(--text-sm);
color: var(--text-muted);
@@ -811,6 +838,8 @@
throw new Error(`HTTP ${response.status}`);
}
habits = await response.json();
+ // Cache habits for delete confirmation
+ localStorage.setItem('habitsCache', JSON.stringify(habits));
renderHabits();
} catch (error) {
console.error('Failed to load habits:', error);
@@ -900,7 +929,17 @@
${lastCheckInfo}
- ${livesHtml}
+
+
${livesHtml}
+
+
${completionRate}% (30d)
@@ -1335,10 +1374,66 @@
// Delete habit (placeholder)
async function deleteHabit(habitId) {
- if (!confirm('Are you sure you want to delete this habit?')) {
+ // Find habit to get its name for confirmation
+ const habits = JSON.parse(localStorage.getItem('habitsCache') || '[]');
+ const habit = habits.find(h => h.id === habitId);
+ const habitName = habit ? habit.name : 'this habit';
+
+ if (!confirm(`Delete ${habitName}? This cannot be undone.`)) {
return;
}
- alert('Delete functionality - coming in next story!');
+
+ try {
+ const response = await fetch(`/echo/api/habits/${habitId}`, {
+ method: 'DELETE'
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}`);
+ }
+
+ // Show success toast
+ showToast('Habit deleted', 'success');
+
+ // Refresh habits list (will remove card from DOM)
+ await loadHabits();
+ } catch (error) {
+ console.error('Failed to delete habit:', error);
+ showToast(`Failed to delete habit: ${error.message}`, 'error');
+ }
+ }
+
+ // Skip a habit day using a life
+ async function skipHabitDay(habitId, habitName) {
+ if (!confirm('Use 1 life to skip today?')) {
+ return;
+ }
+
+ try {
+ const response = await fetch(`/echo/api/habits/${habitId}/skip`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.error || `HTTP ${response.status}`);
+ }
+
+ const updatedHabit = await response.json();
+
+ // Show success toast with remaining lives
+ const remainingLives = updatedHabit.lives;
+ showToast(`Day skipped. ${remainingLives} ${remainingLives === 1 ? 'life' : 'lives'} remaining.`, 'success');
+
+ // Refresh habits list
+ await loadHabits();
+ } catch (error) {
+ console.error('Failed to skip habit day:', error);
+ showToast(`Failed to skip: ${error.message}`, 'error');
+ }
}
// Check-in state
diff --git a/dashboard/tests/test_habits_frontend.py b/dashboard/tests/test_habits_frontend.py
index fb77e78..a67cd19 100644
--- a/dashboard/tests/test_habits_frontend.py
+++ b/dashboard/tests/test_habits_frontend.py
@@ -5,6 +5,7 @@ Story US-007: Frontend - Habit card component
Story US-008: Frontend - Create habit modal with all options
Story US-009: Frontend - Edit habit modal
Story US-010: Frontend - Check-in interaction (click and long-press)
+Story US-011: Frontend - Skip, lives display, and delete confirmation
"""
import sys
@@ -996,6 +997,156 @@ def test_typecheck_us010():
print("✓ Test 55: Typecheck passes (all check-in functions and variables defined)")
+# ========== US-011 Tests: Skip, lives display, and delete confirmation ==========
+
+def test_skip_button_visible():
+ """Test 56: Skip button visible next to lives hearts on each card"""
+ html_path = Path(__file__).parent.parent / 'habits.html'
+ content = html_path.read_text()
+
+ # Check skip button CSS class exists
+ assert '.habit-card-skip-btn' in content, "Skip button CSS class should exist"
+
+ # Check skip button is in renderHabitCard
+ assert 'Skip day' in content, "Skip button text should be 'Skip day'"
+ assert 'onclick="skipHabitDay' in content, "Skip button should call skipHabitDay function"
+
+ # Check lives display structure has both hearts and button
+ assert 'habit-card-lives-hearts' in content, "Lives hearts should be wrapped in separate div"
+
+ print("✓ Test 56: Skip button visible next to lives hearts on each card")
+
+def test_skip_confirmation_dialog():
+ """Test 57: Clicking skip shows confirmation dialog"""
+ html_path = Path(__file__).parent.parent / 'habits.html'
+ content = html_path.read_text()
+
+ # Check skipHabitDay function exists
+ assert 'async function skipHabitDay' in content, "skipHabitDay function should exist"
+
+ # Check confirmation dialog
+ assert 'Use 1 life to skip today?' in content, "Skip confirmation dialog should ask 'Use 1 life to skip today?'"
+ assert 'confirm(' in content, "Should use confirm dialog"
+
+ print("✓ Test 57: Clicking skip shows confirmation dialog")
+
+def test_skip_sends_post_and_refreshes():
+ """Test 58: Confirming skip sends POST /echo/api/habits/{id}/skip and refreshes card"""
+ html_path = Path(__file__).parent.parent / 'habits.html'
+ content = html_path.read_text()
+
+ # Check API endpoint
+ assert "/echo/api/habits/${habitId}/skip" in content, "Should POST to /echo/api/habits/{id}/skip"
+ assert "method: 'POST'" in content, "Should use POST method"
+
+ # Check refresh
+ assert 'await loadHabits()' in content, "Should refresh habits list after skip"
+
+ print("✓ Test 58: Confirming skip sends POST /echo/api/habits/{id}/skip and refreshes card")
+
+def test_skip_button_disabled_when_no_lives():
+ """Test 59: Skip button disabled when lives is 0 with tooltip 'No lives left'"""
+ html_path = Path(__file__).parent.parent / 'habits.html'
+ content = html_path.read_text()
+
+ # Check disabled condition
+ assert "habit.lives === 0 ? 'disabled' : ''" in content, "Skip button should be disabled when lives is 0"
+
+ # Check tooltip
+ assert 'No lives left' in content, "Tooltip should say 'No lives left' when disabled"
+
+ print("✓ Test 59: Skip button disabled when lives is 0 with tooltip 'No lives left'")
+
+def test_skip_toast_message():
+ """Test 60: Toast shows 'Day skipped. X lives remaining.' after skip"""
+ html_path = Path(__file__).parent.parent / 'habits.html'
+ content = html_path.read_text()
+
+ # Check toast message format
+ assert 'Day skipped' in content, "Toast should include 'Day skipped'"
+ assert 'remaining' in content, "Toast should show remaining lives"
+
+ # Check life/lives plural handling
+ assert "remainingLives === 1 ? 'life' : 'lives'" in content, "Should handle singular/plural for lives"
+
+ print("✓ Test 60: Toast shows 'Day skipped. X lives remaining.' after skip")
+
+def test_delete_confirmation_with_habit_name():
+ """Test 61: Clicking trash icon shows delete confirmation dialog with habit name"""
+ html_path = Path(__file__).parent.parent / 'habits.html'
+ content = html_path.read_text()
+
+ # Check delete function exists
+ assert 'async function deleteHabit' in content, "deleteHabit function should exist"
+
+ # Check confirmation uses habit name
+ assert 'Delete ${habitName}?' in content or 'Delete ' in content, "Delete confirmation should include habit name"
+ assert 'This cannot be undone' in content, "Delete confirmation should warn 'This cannot be undone'"
+
+ print("✓ Test 61: Clicking trash icon shows delete confirmation dialog with habit name")
+
+def test_delete_sends_delete_request():
+ """Test 62: Confirming delete sends DELETE /echo/api/habits/{id} and removes card"""
+ html_path = Path(__file__).parent.parent / 'habits.html'
+ content = html_path.read_text()
+
+ # Check DELETE request
+ assert "method: 'DELETE'" in content, "Should use DELETE method"
+ assert "/echo/api/habits/${habitId}" in content or "/echo/api/habits/" in content, "Should DELETE to /echo/api/habits/{id}"
+
+ # Check refresh (removes card from DOM via re-render)
+ assert 'await loadHabits()' in content, "Should refresh habits list after delete"
+
+ print("✓ Test 62: Confirming delete sends DELETE /echo/api/habits/{id} and removes card")
+
+def test_delete_toast_message():
+ """Test 63: Toast shows 'Habit deleted' after deletion"""
+ html_path = Path(__file__).parent.parent / 'habits.html'
+ content = html_path.read_text()
+
+ # Check success toast
+ assert 'Habit deleted' in content, "Toast should say 'Habit deleted'"
+
+ print("✓ Test 63: Toast shows 'Habit deleted' after deletion")
+
+def test_skip_delete_no_console_errors():
+ """Test 64: No obvious console error sources in skip and delete code"""
+ html_path = Path(__file__).parent.parent / 'habits.html'
+ content = html_path.read_text()
+
+ # Check error handling in skipHabitDay
+ skip_func = content[content.find('async function skipHabitDay'):content.find('async function skipHabitDay') + 1500]
+ assert 'try' in skip_func and 'catch' in skip_func, "skipHabitDay should have try/catch"
+ assert 'console.error' in skip_func, "skipHabitDay should log errors"
+
+ # Check error handling in deleteHabit
+ delete_func = content[content.find('async function deleteHabit'):content.find('async function deleteHabit') + 1500]
+ assert 'try' in delete_func and 'catch' in delete_func, "deleteHabit should have try/catch"
+ assert 'console.error' in delete_func, "deleteHabit should log errors"
+
+ print("✓ Test 64: No obvious console error sources in skip and delete code")
+
+def test_typecheck_us011():
+ """Test 65: Typecheck passes (all skip and delete functions defined)"""
+ html_path = Path(__file__).parent.parent / 'habits.html'
+ content = html_path.read_text()
+
+ # Check function definitions
+ functions = [
+ 'skipHabitDay',
+ 'deleteHabit',
+ ]
+
+ for func in functions:
+ assert f"function {func}" in content, \
+ f"{func} should be defined"
+
+ # Check that habits are cached for delete
+ assert 'localStorage.setItem' in content, "Should cache habits in localStorage"
+ assert 'habitsCache' in content, "Should use 'habitsCache' key"
+
+ print("✓ Test 65: Typecheck passes (all skip and delete functions defined)")
+
def run_all_tests():
"""Run all tests in sequence"""
tests = [
@@ -1059,9 +1210,20 @@ def run_all_tests():
test_checkin_prevent_context_menu,
test_checkin_event_listeners_attached,
test_typecheck_us010,
+ # US-011 tests
+ test_skip_button_visible,
+ test_skip_confirmation_dialog,
+ test_skip_sends_post_and_refreshes,
+ test_skip_button_disabled_when_no_lives,
+ test_skip_toast_message,
+ test_delete_confirmation_with_habit_name,
+ test_delete_sends_delete_request,
+ test_delete_toast_message,
+ test_skip_delete_no_console_errors,
+ test_typecheck_us011,
]
- print(f"\nRunning {len(tests)} frontend tests for US-006, US-007, US-008, US-009, and US-010...\n")
+ print(f"\nRunning {len(tests)} frontend tests for US-006, US-007, US-008, US-009, US-010, and US-011...\n")
failed = []
for test in tests: