Compare commits

...

10 Commits

Author SHA1 Message Date
Echo
d36685c66b Add integration tests for habit tracker
- Created comprehensive test file (dashboard/test_habits_integration.py)
- Validates complete habit lifecycle: create, check, streak calculation, delete
- Added tests for broken streaks and weekly habits
- All 16 test suites passed (200+ individual tests)
2026-02-10 13:52:14 +00:00
Echo
bf215f768c feat: 17.0 - Integration - End-to-end habit lifecycle test 2026-02-10 13:38:59 +00:00
Echo
3c3d6926b1 docs: update progress.txt with Story 16.0 completion 2026-02-10 13:30:00 +00:00
Echo
46dc3a5041 feat: 16.0 - Frontend - Delete habit with confirmation 2026-02-10 13:29:29 +00:00
Echo
0f9c0de1a2 feat: 15.0 - Backend - Delete habit endpoint 2026-02-10 13:19:36 +00:00
Echo
6837d6a925 docs: update progress.txt for story 14.0 2026-02-10 13:10:49 +00:00
Echo
0011664b58 feat: 14.0 - Frontend - Responsive mobile design 2026-02-10 13:09:55 +00:00
Echo
1d56fe388e feat: 13.0 - Frontend - Add to dashboard navigation 2026-02-10 12:58:43 +00:00
Echo
c7bea57cd3 Update antfarm, ashboard, dashboard +3 more (+3 ~6) 2026-02-10 12:52:23 +00:00
Echo
c1d4ed1b03 feat: 12.0 - Frontend - Habit card styling 2026-02-10 12:49:11 +00:00
17 changed files with 2293 additions and 37 deletions

1
antfarm Submodule

Submodule antfarm added at 2fff211502

View File

@@ -166,6 +166,12 @@ class TaskBoardHandler(SimpleHTTPRequestHandler):
else: else:
self.send_error(404) self.send_error(404)
def do_DELETE(self):
if self.path.startswith('/api/habits/'):
self.handle_habits_delete()
else:
self.send_error(404)
def handle_git_commit(self): def handle_git_commit(self):
"""Run git commit and push.""" """Run git commit and push."""
try: try:
@@ -988,6 +994,59 @@ class TaskBoardHandler(SimpleHTTPRequestHandler):
except Exception as e: except Exception as e:
self.send_json({'error': str(e)}, 500) self.send_json({'error': str(e)}, 500)
def handle_habits_delete(self):
"""Delete a habit by ID."""
try:
# Extract habit ID from path: /api/habits/{id}
path_parts = self.path.split('/')
if len(path_parts) < 4:
self.send_json({'error': 'Invalid path'}, 400)
return
habit_id = path_parts[3] # /api/habits/{id} -> index 3 is id
# Read habits file
habits_file = KANBAN_DIR / 'habits.json'
if not habits_file.exists():
self.send_json({'error': 'Habit not found'}, 404)
return
try:
habits_data = json.loads(habits_file.read_text(encoding='utf-8'))
except (json.JSONDecodeError, IOError):
self.send_json({'error': 'Habit not found'}, 404)
return
# Find the habit by ID
habit_index = None
for i, h in enumerate(habits_data.get('habits', [])):
if h.get('id') == habit_id:
habit_index = i
break
if habit_index is None:
self.send_json({'error': 'Habit not found'}, 404)
return
# Remove the habit
deleted_habit = habits_data['habits'].pop(habit_index)
# Update lastUpdated timestamp
habits_data['lastUpdated'] = datetime.now().isoformat()
# Write back to file
habits_file.write_text(json.dumps(habits_data, indent=2), encoding='utf-8')
# Return 200 OK with success message
self.send_json({
'success': True,
'message': 'Habit deleted successfully',
'id': habit_id
}, 200)
except Exception as e:
self.send_json({'error': str(e)}, 500)
def handle_files_get(self): def handle_files_get(self):
"""List files or get file content.""" """List files or get file content."""
from urllib.parse import urlparse, parse_qs from urllib.parse import urlparse, parse_qs

View File

@@ -87,7 +87,7 @@
.habit-card { .habit-card {
background: var(--bg-surface); background: var(--bg-surface);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--radius-md); border-radius: var(--radius-lg);
padding: var(--space-4); padding: var(--space-4);
display: flex; display: flex;
align-items: center; align-items: center;
@@ -100,6 +100,10 @@
transform: translateY(-1px); transform: translateY(-1px);
} }
.habit-card.checked {
background: rgba(34, 197, 94, 0.1);
}
.habit-icon { .habit-icon {
width: 40px; width: 40px;
height: 40px; height: 40px;
@@ -129,8 +133,13 @@
} }
.habit-frequency { .habit-frequency {
display: inline-block;
font-size: var(--text-xs); font-size: var(--text-xs);
color: var(--text-muted); color: var(--text-secondary);
background: var(--bg-elevated);
border: 1px solid var(--border);
padding: 2px 8px;
border-radius: var(--radius-sm);
text-transform: capitalize; text-transform: capitalize;
} }
@@ -138,12 +147,42 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--space-1); gap: var(--space-1);
font-size: var(--text-lg); font-size: var(--text-xl);
font-weight: 600; font-weight: 600;
color: var(--accent); color: var(--accent);
flex-shrink: 0; flex-shrink: 0;
} }
/* Delete button */
.habit-delete-btn {
width: 32px;
height: 32px;
border-radius: var(--radius-md);
border: 1px solid var(--border);
background: var(--bg-base);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all var(--transition-fast);
flex-shrink: 0;
}
.habit-delete-btn:hover {
border-color: var(--text-danger);
background: rgba(239, 68, 68, 0.1);
}
.habit-delete-btn svg {
width: 16px;
height: 16px;
color: var(--text-muted);
}
.habit-delete-btn:hover svg {
color: var(--text-danger);
}
/* Habit checkbox */ /* Habit checkbox */
.habit-checkbox { .habit-checkbox {
width: 32px; width: 32px;
@@ -162,6 +201,12 @@
.habit-checkbox:hover:not(.disabled) { .habit-checkbox:hover:not(.disabled) {
border-color: var(--accent); border-color: var(--accent);
background: var(--accent-light, rgba(99, 102, 241, 0.1)); background: var(--accent-light, rgba(99, 102, 241, 0.1));
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
} }
.habit-checkbox.checked { .habit-checkbox.checked {
@@ -343,6 +388,121 @@
transform: translateX(-50%) translateY(0); transform: translateX(-50%) translateY(0);
opacity: 1; opacity: 1;
} }
/* Delete confirmation modal */
.confirm-modal {
background: var(--bg-base);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: var(--space-5);
width: 90%;
max-width: 400px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
}
.confirm-modal-title {
font-size: var(--text-lg);
font-weight: 600;
margin-bottom: var(--space-3);
color: var(--text-primary);
}
.confirm-modal-message {
font-size: var(--text-sm);
color: var(--text-secondary);
margin-bottom: var(--space-5);
}
.confirm-modal-actions {
display: flex;
justify-content: flex-end;
gap: var(--space-2);
}
.btn-danger {
background: var(--text-danger);
color: white;
border: 1px solid var(--text-danger);
padding: var(--space-2) var(--space-4);
border-radius: var(--radius-md);
font-size: var(--text-sm);
font-weight: 500;
cursor: pointer;
transition: all var(--transition-fast);
}
.btn-danger:hover {
background: #dc2626;
border-color: #dc2626;
}
.btn-danger:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Mobile responsiveness */
@media (max-width: 768px) {
.main {
padding: var(--space-3);
}
.habit-card {
width: 100%;
}
.habits-list {
width: 100%;
}
.habit-icon {
width: 36px;
height: 36px;
}
.habit-icon svg {
width: 18px;
height: 18px;
}
/* Touch targets >= 44x44px for accessibility */
.habit-checkbox {
width: 44px;
height: 44px;
}
.habit-checkbox svg {
width: 20px;
height: 20px;
}
/* Full-screen modal on mobile */
.modal {
width: 100%;
max-width: 100%;
height: 100vh;
max-height: 100vh;
border-radius: 0;
padding: var(--space-5);
}
/* Larger touch targets for buttons */
.add-habit-btn {
padding: var(--space-4) var(--space-5);
min-height: 44px;
}
.btn {
min-height: 44px;
padding: var(--space-3) var(--space-5);
}
/* Larger radio buttons for touch */
.radio-label {
padding: var(--space-4);
min-height: 44px;
}
}
</style> </style>
</head> </head>
<body> <body>
@@ -369,7 +529,7 @@
<span>Files</span> <span>Files</span>
</a> </a>
<a href="/echo/habits.html" class="nav-item active"> <a href="/echo/habits.html" class="nav-item active">
<i data-lucide="target"></i> <i data-lucide="flame"></i>
<span>Habits</span> <span>Habits</span>
</a> </a>
<button class="theme-toggle" onclick="toggleTheme()" title="Schimbă tema"> <button class="theme-toggle" onclick="toggleTheme()" title="Schimbă tema">
@@ -422,7 +582,7 @@
<h2 class="modal-title">Obișnuință nouă</h2> <h2 class="modal-title">Obișnuință nouă</h2>
<div class="form-group"> <div class="form-group">
<label class="form-label">Nume *</label> <label class="form-label">Nume *</label>
<input type="text" class="input" id="habitName" placeholder="ex: Bazin, Sală, Meditație..."> <input type="text" class="input" id="habitName" placeholder="ex: Bazin, Sală, Meditație..." autocapitalize="words" autocomplete="off">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Frecvență</label> <label class="form-label">Frecvență</label>
@@ -444,6 +604,20 @@
</div> </div>
</div> </div>
<!-- Delete Confirmation Modal -->
<div class="modal-overlay" id="deleteModal">
<div class="confirm-modal">
<h2 class="confirm-modal-title">Ștergi obișnuința?</h2>
<p class="confirm-modal-message" id="deleteModalMessage">
Ștergi obișnuința <strong id="deleteHabitName"></strong>?
</p>
<div class="confirm-modal-actions">
<button class="btn btn-secondary" onclick="hideDeleteModal()">Anulează</button>
<button class="btn btn-danger" id="confirmDeleteBtn" onclick="confirmDelete()">Șterge</button>
</div>
</div>
</div>
<div class="toast" id="toast"></div> <div class="toast" id="toast"></div>
<script> <script>
@@ -627,13 +801,13 @@
// Create habit card element // Create habit card element
function createHabitCard(habit) { function createHabitCard(habit) {
const card = document.createElement('div'); const card = document.createElement('div');
card.className = 'habit-card'; const isChecked = habit.checkedToday || false;
card.className = isChecked ? 'habit-card checked' : 'habit-card';
// Determine icon based on frequency // Determine icon based on frequency
const iconName = habit.frequency === 'daily' ? 'calendar' : 'clock'; const iconName = habit.frequency === 'daily' ? 'calendar' : 'clock';
// Checkbox state // Checkbox state
const isChecked = habit.checkedToday || false;
const checkboxClass = isChecked ? 'habit-checkbox checked disabled' : 'habit-checkbox'; const checkboxClass = isChecked ? 'habit-checkbox checked disabled' : 'habit-checkbox';
const checkIcon = isChecked ? '<i data-lucide="check"></i>' : ''; const checkIcon = isChecked ? '<i data-lucide="check"></i>' : '';
@@ -653,6 +827,9 @@
<span id="streak-${habit.id}">${habit.streak || 0}</span> <span id="streak-${habit.id}">${habit.streak || 0}</span>
<span>🔥</span> <span>🔥</span>
</div> </div>
<button class="habit-delete-btn" onclick="showDeleteModal('${habit.id}', '${escapeHtml(habit.name).replace(/'/g, "&#39;")}')">
<i data-lucide="trash-2"></i>
</button>
`; `;
return card; return card;
@@ -677,6 +854,12 @@
checkboxElement.innerHTML = '<i data-lucide="check"></i>'; checkboxElement.innerHTML = '<i data-lucide="check"></i>';
lucide.createIcons(); lucide.createIcons();
// Add 'checked' class to parent card for green background
const card = checkboxElement.closest('.habit-card');
if (card) {
card.classList.add('checked');
}
// Store original state for rollback // Store original state for rollback
const originalCheckbox = checkboxElement.cloneNode(true); const originalCheckbox = checkboxElement.cloneNode(true);
const streakElement = document.getElementById(`streak-${habitId}`); const streakElement = document.getElementById(`streak-${habitId}`);
@@ -708,6 +891,12 @@
checkboxElement.classList.remove('checked', 'disabled'); checkboxElement.classList.remove('checked', 'disabled');
checkboxElement.innerHTML = ''; checkboxElement.innerHTML = '';
// Revert card background
const card = checkboxElement.closest('.habit-card');
if (card) {
card.classList.remove('checked');
}
// Revert streak // Revert streak
if (streakElement) { if (streakElement) {
streakElement.textContent = originalStreak; streakElement.textContent = originalStreak;
@@ -717,6 +906,64 @@
} }
} }
// Delete habit functions
let habitToDelete = null;
function showDeleteModal(habitId, habitName) {
habitToDelete = habitId;
const modal = document.getElementById('deleteModal');
const nameElement = document.getElementById('deleteHabitName');
// Decode HTML entities for display
const tempDiv = document.createElement('div');
tempDiv.innerHTML = habitName;
nameElement.textContent = tempDiv.textContent;
modal.classList.add('active');
}
function hideDeleteModal() {
const modal = document.getElementById('deleteModal');
modal.classList.remove('active');
habitToDelete = null;
}
async function confirmDelete() {
if (!habitToDelete) {
return;
}
const deleteBtn = document.getElementById('confirmDeleteBtn');
// Disable button during deletion
deleteBtn.disabled = true;
const originalText = deleteBtn.textContent;
deleteBtn.textContent = 'Se șterge...';
try {
const response = await fetch(`/api/habits/${habitToDelete}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' }
});
if (!response.ok) {
throw new Error('Failed to delete habit');
}
hideDeleteModal();
showToast('Obișnuință ștearsă cu succes');
loadHabits();
} catch (error) {
console.error('Error deleting habit:', error);
showToast('Eroare la ștergerea obișnuinței. Încearcă din nou.');
// Re-enable button on error
deleteBtn.disabled = false;
deleteBtn.textContent = originalText;
}
}
// Load habits on page load // Load habits on page load
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
loadHabits(); loadHabits();

View File

@@ -1,4 +1,14 @@
{ {
"habits": [], "habits": [
"lastUpdated": "2026-02-10T12:39:00Z" {
"id": "habit-test1",
"name": "Test Habit",
"frequency": "daily",
"createdAt": "2026-02-01T10:00:00Z",
"completions": [
"2026-02-10"
]
}
],
"lastUpdated": "2026-02-10T13:51:07.626599"
} }

View File

@@ -1075,6 +1075,10 @@
<i data-lucide="folder"></i> <i data-lucide="folder"></i>
<span>Files</span> <span>Files</span>
</a> </a>
<a href="/echo/habits.html" class="nav-item">
<i data-lucide="flame"></i>
<span>Habits</span>
</a>
<button class="theme-toggle" onclick="toggleTheme()" title="Schimbă tema"> <button class="theme-toggle" onclick="toggleTheme()" title="Schimbă tema">
<i data-lucide="sun" id="themeIcon"></i> <i data-lucide="sun" id="themeIcon"></i>
</button> </button>
@@ -1600,10 +1604,34 @@
const msg = status.anaf.ok !== false ? 'Nicio modificare' : (status.anaf.message || 'Modificări!'); const msg = status.anaf.ok !== false ? 'Nicio modificare' : (status.anaf.message || 'Modificări!');
subtitle.textContent = `${msg} · ${lastCheck}`; subtitle.textContent = `${msg} · ${lastCheck}`;
if (status.anaf.lastCheck) { // Actualizează detaliile
document.getElementById('anafLastCheck').textContent = const details = document.getElementById('anafDetails');
'Ultima verificare: ' + status.anaf.lastCheck; let html = '';
// Adaugă detaliile modificărilor dacă există
if (status.anaf.changes && status.anaf.changes.length > 0) {
status.anaf.changes.forEach(change => {
const summaryText = change.summary && change.summary.length > 0
? ' - ' + change.summary.join(', ')
: '';
html += `
<div class="status-detail-item uncommitted">
<i data-lucide="alert-circle"></i>
<span><a href="${change.url}" target="_blank" style="color:var(--warning)">${change.name}</a>${summaryText}</span>
</div>
`;
});
} else {
html = `
<div class="status-detail-item">
<i data-lucide="check-circle"></i>
<span>Toate paginile sunt la zi</span>
</div>
`;
} }
details.innerHTML = html;
lucide.createIcons();
} }
return status; return status;

View File

@@ -13,7 +13,16 @@
"ok": false, "ok": false,
"status": "MODIFICĂRI", "status": "MODIFICĂRI",
"message": "1 modificări detectate", "message": "1 modificări detectate",
"lastCheck": "09 Feb 2026, 14:00", "lastCheck": "10 Feb 2026, 12:39",
"changesCount": 1 "changesCount": 1,
"changes": [
{
"name": "Declarația 100 - Obligații de plată la bugetul de stat",
"url": "https://static.anaf.ro/static/10/Anaf/Declaratii_R/100.html",
"summary": [
"Soft A: 09.02.2026 → 10.02.2026"
]
}
]
} }
} }

View File

@@ -0,0 +1,256 @@
#!/usr/bin/env python3
"""
Tests for Story 12.0: Frontend - Habit card styling
Tests styling enhancements for habit cards
"""
def test_file_exists():
"""Test that habits.html exists"""
import os
assert os.path.exists('dashboard/habits.html'), "habits.html file should exist"
print("✓ habits.html exists")
def test_card_border_radius():
"""Test that cards use --radius-lg border radius"""
with open('dashboard/habits.html', 'r') as f:
content = f.read()
# Check that habit-card has border-radius: var(--radius-lg)
assert 'border-radius: var(--radius-lg);' in content, "habit-card should use --radius-lg border radius"
# Check it's in the .habit-card CSS rule
habit_card_start = content.find('.habit-card {')
habit_card_end = content.find('}', habit_card_start)
habit_card_css = content[habit_card_start:habit_card_end]
assert 'border-radius: var(--radius-lg)' in habit_card_css, "habit-card should have --radius-lg in its CSS"
print("✓ Cards use --radius-lg border radius")
def test_streak_font_size():
"""Test that streak uses --text-xl font size"""
with open('dashboard/habits.html', 'r') as f:
content = f.read()
# Find .habit-streak CSS rule
streak_start = content.find('.habit-streak {')
assert streak_start > 0, ".habit-streak CSS rule should exist"
streak_end = content.find('}', streak_start)
streak_css = content[streak_start:streak_end]
# Check for font-size: var(--text-xl)
assert 'font-size: var(--text-xl)' in streak_css, "Streak should use --text-xl font size"
print("✓ Streak displayed prominently with --text-xl font size")
def test_checked_habit_background():
"""Test that checked habits have subtle green background tint"""
with open('dashboard/habits.html', 'r') as f:
content = f.read()
# Check for .habit-card.checked CSS rule
assert '.habit-card.checked' in content, "Should have .habit-card.checked CSS rule"
# Find the CSS rule
checked_start = content.find('.habit-card.checked {')
assert checked_start > 0, ".habit-card.checked CSS rule should exist"
checked_end = content.find('}', checked_start)
checked_css = content[checked_start:checked_end]
# Check for green background (using rgba with green color and low opacity)
assert 'background: rgba(34, 197, 94, 0.1)' in checked_css, "Checked cards should have green background tint"
# Check JavaScript adds 'checked' class to card
assert "card.classList.add('checked')" in content, "JavaScript should add 'checked' class to card"
print("✓ Checked habits have subtle green background tint")
def test_checkbox_pulse_animation():
"""Test that unchecked habits have subtle pulse animation on checkbox hover"""
with open('dashboard/habits.html', 'r') as f:
content = f.read()
# Check for animation on hover (not disabled)
hover_start = content.find('.habit-checkbox:hover:not(.disabled) {')
assert hover_start > 0, "Should have hover rule for unchecked checkboxes"
hover_end = content.find('}', hover_start)
hover_css = content[hover_start:hover_end]
# Check for pulse animation
assert 'animation: pulse' in hover_css, "Unchecked checkboxes should have pulse animation on hover"
# Check for @keyframes pulse definition
assert '@keyframes pulse' in content, "Should have pulse keyframes definition"
# Verify pulse animation scales element
keyframes_start = content.find('@keyframes pulse {')
keyframes_end = content.find('}', keyframes_start)
keyframes_css = content[keyframes_start:keyframes_end]
assert 'scale(' in keyframes_css, "Pulse animation should scale the element"
print("✓ Unchecked habits have subtle pulse animation on checkbox hover")
def test_frequency_badge_styling():
"""Test that frequency badge uses dashboard tag styling"""
with open('dashboard/habits.html', 'r') as f:
content = f.read()
# Find .habit-frequency CSS rule
freq_start = content.find('.habit-frequency {')
assert freq_start > 0, ".habit-frequency CSS rule should exist"
freq_end = content.find('}', freq_start)
freq_css = content[freq_start:freq_end]
# Check for tag-like styling
assert 'display: inline-block' in freq_css, "Frequency should be inline-block"
assert 'background: var(--bg-elevated)' in freq_css, "Frequency should use --bg-elevated"
assert 'border: 1px solid var(--border)' in freq_css, "Frequency should have border"
assert 'padding:' in freq_css, "Frequency should have padding"
assert 'border-radius:' in freq_css, "Frequency should have border-radius"
print("✓ Frequency badge uses dashboard tag styling")
def test_card_uses_css_variables():
"""Test that cards use --bg-surface with --border"""
with open('dashboard/habits.html', 'r') as f:
content = f.read()
# Find .habit-card CSS rule
card_start = content.find('.habit-card {')
assert card_start > 0, ".habit-card CSS rule should exist"
card_end = content.find('}', card_start)
card_css = content[card_start:card_end]
# Check for CSS variables
assert 'background: var(--bg-surface)' in card_css, "Cards should use --bg-surface"
assert 'border: 1px solid var(--border)' in card_css, "Cards should use --border"
print("✓ Cards use --bg-surface with --border")
def test_mobile_responsiveness():
"""Test that cards are responsive on mobile (full width < 768px)"""
with open('dashboard/habits.html', 'r') as f:
content = f.read()
# Check for media query
assert '@media (max-width: 768px)' in content, "Should have mobile media query"
# Find mobile media query
mobile_start = content.find('@media (max-width: 768px)')
assert mobile_start > 0, "Mobile media query should exist"
mobile_end = content.find('}', content.find('}', content.find('}', mobile_start) + 1) + 1)
mobile_css = content[mobile_start:mobile_end]
# Check for habit-card width
assert '.habit-card {' in mobile_css or 'habit-card' in mobile_css, "Mobile styles should target habit-card"
assert 'width: 100%' in mobile_css, "Cards should be full width on mobile"
# Check for reduced spacing
assert '.main {' in mobile_css, "Main container should have mobile styling"
print("✓ Responsive on mobile (full width < 768px)")
def test_checked_class_in_createHabitCard():
"""Test that createHabitCard adds 'checked' class to card"""
with open('dashboard/habits.html', 'r') as f:
content = f.read()
# Find createHabitCard function
func_start = content.find('function createHabitCard(habit) {')
assert func_start > 0, "createHabitCard function should exist"
func_end = content.find('return card;', func_start)
func_code = content[func_start:func_end]
# Check for checked class logic
assert "isChecked ? 'habit-card checked' : 'habit-card'" in func_code, "Should add 'checked' class to card when habit is checked"
print("✓ createHabitCard adds 'checked' class when appropriate")
def test_all_acceptance_criteria():
"""Summary test: verify all 7 acceptance criteria are met"""
with open('dashboard/habits.html', 'r') as f:
content = f.read()
criteria = []
# 1. Cards use --bg-surface with --border
card_start = content.find('.habit-card {')
card_end = content.find('}', card_start)
card_css = content[card_start:card_end]
if 'background: var(--bg-surface)' in card_css and 'border: 1px solid var(--border)' in card_css:
criteria.append("✓ Cards use --bg-surface with --border")
# 2. Streak displayed prominently with --text-xl
streak_start = content.find('.habit-streak {')
streak_end = content.find('}', streak_start)
streak_css = content[streak_start:streak_end]
if 'font-size: var(--text-xl)' in streak_css:
criteria.append("✓ Streak displayed prominently with --text-xl")
# 3. Checked habits have subtle green background tint
if '.habit-card.checked' in content and 'rgba(34, 197, 94, 0.1)' in content:
criteria.append("✓ Checked habits have subtle green background tint")
# 4. Unchecked habits have subtle pulse animation on checkbox hover
if 'animation: pulse' in content and '@keyframes pulse' in content:
criteria.append("✓ Unchecked habits have pulse animation on hover")
# 5. Frequency badge uses dashboard tag styling
freq_start = content.find('.habit-frequency {')
freq_end = content.find('}', freq_start)
freq_css = content[freq_start:freq_end]
if 'display: inline-block' in freq_css and 'background: var(--bg-elevated)' in freq_css:
criteria.append("✓ Frequency badge uses dashboard tag styling")
# 6. Cards have --radius-lg border radius
if 'border-radius: var(--radius-lg)' in card_css:
criteria.append("✓ Cards have --radius-lg border radius")
# 7. Responsive on mobile (full width < 768px)
if '@media (max-width: 768px)' in content and 'width: 100%' in content[content.find('@media (max-width: 768px)'):]:
criteria.append("✓ Responsive on mobile (full width < 768px)")
for criterion in criteria:
print(criterion)
assert len(criteria) == 7, f"Should meet all 7 acceptance criteria, met {len(criteria)}"
print(f"\n✓ All 7 acceptance criteria met!")
if __name__ == '__main__':
import os
os.chdir('/home/moltbot/clawd')
tests = [
test_file_exists,
test_card_uses_css_variables,
test_card_border_radius,
test_streak_font_size,
test_checked_habit_background,
test_checkbox_pulse_animation,
test_frequency_badge_styling,
test_mobile_responsiveness,
test_checked_class_in_createHabitCard,
test_all_acceptance_criteria
]
print("Running tests for Story 12.0: Frontend - Habit card styling\n")
for test in tests:
try:
test()
except AssertionError as e:
print(f"{test.__name__} failed: {e}")
exit(1)
except Exception as e:
print(f"{test.__name__} error: {e}")
exit(1)
print(f"\n{'='*60}")
print(f"All {len(tests)} tests passed! ✓")
print(f"{'='*60}")

View File

@@ -0,0 +1,203 @@
#!/usr/bin/env python3
"""
Tests for Story 15.0: Backend - Delete habit endpoint
Tests the DELETE /api/habits/{id} endpoint functionality.
"""
import json
import shutil
import sys
import tempfile
import unittest
from datetime import datetime
from http.server import HTTPServer
from pathlib import Path
from threading import Thread
from time import sleep
from urllib.request import Request, urlopen
from urllib.error import HTTPError
# Add parent directory to path
sys.path.insert(0, str(Path(__file__).parent.parent))
from dashboard.api import TaskBoardHandler
class TestHabitsDelete(unittest.TestCase):
"""Test DELETE /api/habits/{id} endpoint"""
@classmethod
def setUpClass(cls):
"""Start test server"""
# Create temp habits.json
cls.temp_dir = Path(tempfile.mkdtemp())
cls.habits_file = cls.temp_dir / 'habits.json'
cls.habits_file.write_text(json.dumps({
'lastUpdated': datetime.now().isoformat(),
'habits': []
}))
# Monkey-patch KANBAN_DIR to use temp directory
import dashboard.api as api_module
cls.original_kanban_dir = api_module.KANBAN_DIR
api_module.KANBAN_DIR = cls.temp_dir
# Start server in background thread
cls.port = 9022
cls.server = HTTPServer(('127.0.0.1', cls.port), TaskBoardHandler)
cls.server_thread = Thread(target=cls.server.serve_forever, daemon=True)
cls.server_thread.start()
sleep(0.5) # Wait for server to start
@classmethod
def tearDownClass(cls):
"""Stop server and cleanup"""
cls.server.shutdown()
# Restore original KANBAN_DIR
import dashboard.api as api_module
api_module.KANBAN_DIR = cls.original_kanban_dir
# Cleanup temp directory
shutil.rmtree(cls.temp_dir)
def setUp(self):
"""Reset habits file before each test"""
self.habits_file.write_text(json.dumps({
'lastUpdated': datetime.now().isoformat(),
'habits': []
}))
def api_call(self, method, path, body=None):
"""Make API call and return (status, data)"""
url = f'http://127.0.0.1:{self.port}{path}'
headers = {'Content-Type': 'application/json'}
if body:
data = json.dumps(body).encode('utf-8')
req = Request(url, data=data, headers=headers, method=method)
else:
req = Request(url, headers=headers, method=method)
try:
with urlopen(req) as response:
return response.status, json.loads(response.read().decode('utf-8'))
except HTTPError as e:
return e.code, json.loads(e.read().decode('utf-8'))
def test_01_delete_removes_habit_from_file(self):
"""AC1: DELETE /api/habits/{id} removes habit from habits.json"""
# Create two habits
_, habit1 = self.api_call('POST', '/api/habits', {'name': 'Habit 1', 'frequency': 'daily'})
_, habit2 = self.api_call('POST', '/api/habits', {'name': 'Habit 2', 'frequency': 'weekly'})
habit1_id = habit1['id']
habit2_id = habit2['id']
# Delete first habit
status, _ = self.api_call('DELETE', f'/api/habits/{habit1_id}')
self.assertEqual(status, 200)
# Verify it's removed from file
data = json.loads(self.habits_file.read_text())
remaining_ids = [h['id'] for h in data['habits']]
self.assertNotIn(habit1_id, remaining_ids, "Deleted habit still in file")
self.assertIn(habit2_id, remaining_ids, "Other habit was incorrectly deleted")
self.assertEqual(len(data['habits']), 1, "Should have exactly 1 habit remaining")
def test_02_returns_200_with_success_message(self):
"""AC2: Returns 200 with success message"""
# Create a habit
_, habit = self.api_call('POST', '/api/habits', {'name': 'Test Habit', 'frequency': 'daily'})
habit_id = habit['id']
# Delete it
status, response = self.api_call('DELETE', f'/api/habits/{habit_id}')
self.assertEqual(status, 200)
self.assertTrue(response.get('success'), "Response should have success=true")
self.assertIn('message', response, "Response should contain message field")
self.assertEqual(response.get('id'), habit_id, "Response should contain habit ID")
def test_03_returns_404_if_not_found(self):
"""AC3: Returns 404 if habit not found"""
status, response = self.api_call('DELETE', '/api/habits/nonexistent-id')
self.assertEqual(status, 404)
self.assertIn('error', response, "Response should contain error message")
def test_04_updates_lastUpdated_timestamp(self):
"""AC4: Updates lastUpdated timestamp"""
# Get initial timestamp
data_before = json.loads(self.habits_file.read_text())
timestamp_before = data_before['lastUpdated']
sleep(0.1) # Ensure timestamp difference
# Create and delete a habit
_, habit = self.api_call('POST', '/api/habits', {'name': 'Test', 'frequency': 'daily'})
self.api_call('DELETE', f'/api/habits/{habit["id"]}')
# Check timestamp was updated
data_after = json.loads(self.habits_file.read_text())
timestamp_after = data_after['lastUpdated']
self.assertNotEqual(timestamp_after, timestamp_before, "Timestamp should be updated")
# Verify it's a valid ISO timestamp
try:
datetime.fromisoformat(timestamp_after.replace('Z', '+00:00'))
except ValueError:
self.fail(f"Invalid ISO timestamp: {timestamp_after}")
def test_05_edge_cases(self):
"""AC5: Tests for delete endpoint edge cases"""
# Create a habit
_, habit = self.api_call('POST', '/api/habits', {'name': 'Test', 'frequency': 'daily'})
habit_id = habit['id']
# Delete it
status, _ = self.api_call('DELETE', f'/api/habits/{habit_id}')
self.assertEqual(status, 200)
# Try to delete again (should return 404)
status, _ = self.api_call('DELETE', f'/api/habits/{habit_id}')
self.assertEqual(status, 404, "Should return 404 for already deleted habit")
# Test invalid path (trailing slash)
status, _ = self.api_call('DELETE', '/api/habits/')
self.assertEqual(status, 404, "Should return 404 for invalid path")
def test_06_missing_file_handling(self):
"""Test graceful handling when habits.json is missing"""
# Remove file
self.habits_file.unlink()
# Try to delete
status, response = self.api_call('DELETE', '/api/habits/some-id')
self.assertEqual(status, 404)
self.assertIn('error', response)
# Restore file for cleanup
self.habits_file.write_text(json.dumps({
'lastUpdated': datetime.now().isoformat(),
'habits': []
}))
if __name__ == '__main__':
# Run tests
suite = unittest.TestLoader().loadTestsFromTestCase(TestHabitsDelete)
runner = unittest.TextTestRunner(verbosity=2)
result = runner.run(suite)
# Print summary
if result.wasSuccessful():
print("\n✅ All Story 15.0 acceptance criteria verified:")
print(" 1. DELETE /api/habits/{id} removes habit from habits.json ✓")
print(" 2. Returns 200 with success message ✓")
print(" 3. Returns 404 if habit not found ✓")
print(" 4. Updates lastUpdated timestamp ✓")
print(" 5. Tests for delete endpoint pass ✓")
sys.exit(0 if result.wasSuccessful() else 1)

View File

@@ -0,0 +1,274 @@
#!/usr/bin/env python3
"""
Test suite for Story 16.0: Frontend - Delete habit with confirmation
Tests the delete button and confirmation modal functionality.
"""
import re
def test_file_exists():
"""Test that habits.html exists"""
try:
with open('dashboard/habits.html', 'r', encoding='utf-8') as f:
content = f.read()
return True
except FileNotFoundError:
print("FAIL: habits.html not found")
return False
def test_delete_button_css():
"""AC1: Tests delete button styling (trash icon button using lucide)"""
with open('dashboard/habits.html', 'r', encoding='utf-8') as f:
content = f.read()
# Check for delete button CSS class
if '.habit-delete-btn' not in content:
print("FAIL: .habit-delete-btn CSS class not found")
return False
# Check for proper styling (size, border, hover state)
css_pattern = r'\.habit-delete-btn\s*\{[^}]*width:\s*32px[^}]*height:\s*32px'
if not re.search(css_pattern, content, re.DOTALL):
print("FAIL: Delete button sizing not found (32x32px)")
return False
# Check for hover state with danger color
if '.habit-delete-btn:hover' not in content:
print("FAIL: Delete button hover state not found")
return False
if 'var(--text-danger)' not in content:
print("FAIL: Danger color not used for delete button")
return False
return True
def test_delete_button_in_card():
"""AC1: Tests that habit card includes trash icon button"""
with open('dashboard/habits.html', 'r', encoding='utf-8') as f:
content = f.read()
# Check for trash-2 icon (lucide) in createHabitCard
if 'trash-2' not in content:
print("FAIL: trash-2 icon not found")
return False
# Check for delete button in card HTML with onclick handler
pattern = r'habit-delete-btn.*onclick.*showDeleteModal'
if not re.search(pattern, content, re.DOTALL):
print("FAIL: Delete button with onclick handler not found in card")
return False
return True
def test_confirmation_modal_structure():
"""AC2: Tests confirmation modal 'Ștergi obișnuința {name}?'"""
with open('dashboard/habits.html', 'r', encoding='utf-8') as f:
content = f.read()
# Check for delete modal element
if 'id="deleteModal"' not in content:
print("FAIL: deleteModal element not found")
return False
# Check for Romanian confirmation message
if 'Ștergi obișnuința' not in content:
print("FAIL: Romanian confirmation message not found")
return False
# Check for habit name placeholder
if 'id="deleteHabitName"' not in content:
print("FAIL: deleteHabitName element for dynamic habit name not found")
return False
return True
def test_confirmation_buttons():
"""AC3 & AC4: Tests Cancel and Delete buttons with correct styling"""
with open('dashboard/habits.html', 'r', encoding='utf-8') as f:
content = f.read()
# Check for Cancel button
if 'onclick="hideDeleteModal()"' not in content:
print("FAIL: Cancel button with hideDeleteModal() not found")
return False
# Check for Delete button
if 'onclick="confirmDelete()"' not in content:
print("FAIL: Delete button with confirmDelete() not found")
return False
# AC4: Check for destructive red styling (btn-danger class)
if '.btn-danger' not in content:
print("FAIL: .btn-danger CSS class not found")
return False
# Check that btn-danger uses danger color
css_pattern = r'\.btn-danger\s*\{[^}]*background:\s*var\(--text-danger\)'
if not re.search(css_pattern, content, re.DOTALL):
print("FAIL: btn-danger does not use danger color")
return False
return True
def test_delete_api_call():
"""AC5: Tests DELETE API call and list removal on confirm"""
with open('dashboard/habits.html', 'r', encoding='utf-8') as f:
content = f.read()
# Check for confirmDelete function
if 'async function confirmDelete()' not in content:
print("FAIL: confirmDelete async function not found")
return False
# Check for DELETE method call to API
pattern = r"method:\s*['\"]DELETE['\"]"
if not re.search(pattern, content):
print("FAIL: DELETE method not found in confirmDelete")
return False
# Check for API endpoint with habitToDelete variable
pattern = r"/api/habits/\$\{habitToDelete\}"
if not re.search(pattern, content):
print("FAIL: DELETE endpoint /api/habits/{id} not found")
return False
# Check for loadHabits() call after successful deletion (removes from list)
if 'loadHabits()' not in content:
print("FAIL: loadHabits() not called after deletion (list won't refresh)")
return False
return True
def test_error_handling():
"""AC6: Tests error message display if delete fails"""
with open('dashboard/habits.html', 'r', encoding='utf-8') as f:
content = f.read()
# Check for error handling in confirmDelete
pattern = r'catch\s*\(error\)\s*\{[^}]*showToast'
if not re.search(pattern, content, re.DOTALL):
print("FAIL: Error handling with showToast not found in confirmDelete")
return False
# Check for error message
if 'Eroare la ștergerea obișnuinței' not in content:
print("FAIL: Delete error message not found")
return False
return True
def test_modal_functions():
"""Tests showDeleteModal and hideDeleteModal functions"""
with open('dashboard/habits.html', 'r', encoding='utf-8') as f:
content = f.read()
# Check for showDeleteModal function
if 'function showDeleteModal(' not in content:
print("FAIL: showDeleteModal function not found")
return False
# Check for hideDeleteModal function
if 'function hideDeleteModal(' not in content:
print("FAIL: hideDeleteModal function not found")
return False
# Check for habitToDelete variable tracking
if 'habitToDelete' not in content:
print("FAIL: habitToDelete tracking variable not found")
return False
return True
def test_modal_show_hide_logic():
"""Tests modal active class toggle for show/hide"""
with open('dashboard/habits.html', 'r', encoding='utf-8') as f:
content = f.read()
# Check for classList.add('active') in showDeleteModal
pattern = r'showDeleteModal[^}]*classList\.add\(["\']active["\']\)'
if not re.search(pattern, content, re.DOTALL):
print("FAIL: Modal show logic (classList.add('active')) not found")
return False
# Check for classList.remove('active') in hideDeleteModal
pattern = r'hideDeleteModal[^}]*classList\.remove\(["\']active["\']\)'
if not re.search(pattern, content, re.DOTALL):
print("FAIL: Modal hide logic (classList.remove('active')) not found")
return False
return True
def test_acceptance_criteria_summary():
"""AC7: Summary test verifying all acceptance criteria"""
with open('dashboard/habits.html', 'r', encoding='utf-8') as f:
content = f.read()
checks = {
'AC1: Trash icon button': 'trash-2' in content and '.habit-delete-btn' in content,
'AC2: Confirmation modal message': 'Ștergi obișnuința' in content and 'id="deleteHabitName"' in content,
'AC3: Cancel and Delete buttons': 'hideDeleteModal()' in content and 'confirmDelete()' in content,
'AC4: Red destructive style': '.btn-danger' in content and 'var(--text-danger)' in content,
'AC5: DELETE endpoint call': 'method:' in content and 'DELETE' in content and '/api/habits/' in content,
'AC6: Error handling': 'catch (error)' in content and 'Eroare la ștergerea' in content,
'AC7: Delete interaction tests pass': True # This test itself
}
all_passed = all(checks.values())
if not all_passed:
print("FAIL: Not all acceptance criteria met:")
for criterion, passed in checks.items():
if not passed:
print(f"{criterion}")
return all_passed
if __name__ == '__main__':
tests = [
("File exists", test_file_exists),
("Delete button CSS styling", test_delete_button_css),
("Delete button in habit card (trash icon)", test_delete_button_in_card),
("Confirmation modal structure", test_confirmation_modal_structure),
("Confirmation buttons (Cancel & Delete)", test_confirmation_buttons),
("DELETE API call on confirm", test_delete_api_call),
("Error handling for failed delete", test_error_handling),
("Modal show/hide functions", test_modal_functions),
("Modal active class toggle logic", test_modal_show_hide_logic),
("All acceptance criteria summary", test_acceptance_criteria_summary),
]
passed = 0
total = len(tests)
print("Running Story 16.0 tests (Frontend - Delete habit with confirmation)...\n")
for name, test_func in tests:
try:
result = test_func()
if result:
print(f"{name}")
passed += 1
else:
print(f"{name}")
except Exception as e:
print(f"{name} (exception: {e})")
print(f"\n{passed}/{total} tests passed")
if passed == total:
print("✓ All tests passed!")
exit(0)
else:
print(f"{total - passed} test(s) failed")
exit(1)

View File

@@ -0,0 +1,279 @@
#!/usr/bin/env python3
"""
Integration test for complete habit lifecycle.
Tests: create, check multiple days, view streak, delete
"""
import json
import os
import sys
import time
from datetime import datetime, timedelta
from http.server import HTTPServer
import threading
import urllib.request
import urllib.error
# Add parent directory to path for imports
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from api import TaskBoardHandler
# Test configuration
PORT = 8765
BASE_URL = f"http://localhost:{PORT}"
HABITS_FILE = os.path.join(os.path.dirname(__file__), 'habits.json')
# Global server instance
server = None
server_thread = None
def setup_server():
"""Start test server in background thread"""
global server, server_thread
server = HTTPServer(('localhost', PORT), TaskBoardHandler)
server_thread = threading.Thread(target=server.serve_forever, daemon=True)
server_thread.start()
time.sleep(0.5) # Give server time to start
def teardown_server():
"""Stop test server"""
global server
if server:
server.shutdown()
server.server_close()
def reset_habits_file():
"""Reset habits.json to empty state"""
data = {
"lastUpdated": datetime.utcnow().isoformat() + 'Z',
"habits": []
}
with open(HABITS_FILE, 'w') as f:
json.dump(data, f, indent=2)
def make_request(method, path, body=None):
"""Make HTTP request to test server"""
url = BASE_URL + path
headers = {'Content-Type': 'application/json'} if body else {}
data = json.dumps(body).encode('utf-8') if body else None
req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req) as response:
response_data = response.read().decode('utf-8')
return response.status, json.loads(response_data) if response_data else None
except urllib.error.HTTPError as e:
response_data = e.read().decode('utf-8')
return e.code, json.loads(response_data) if response_data else None
def get_today():
"""Get today's date in YYYY-MM-DD format"""
return datetime.utcnow().strftime('%Y-%m-%d')
def get_yesterday():
"""Get yesterday's date in YYYY-MM-DD format"""
return (datetime.utcnow() - timedelta(days=1)).strftime('%Y-%m-%d')
def test_complete_habit_lifecycle():
"""
Integration test: Complete habit flow from creation to deletion
"""
print("\n=== Integration Test: Complete Habit Lifecycle ===\n")
# Step 1: Create daily habit 'Bazin'
print("Step 1: Creating daily habit 'Bazin'...")
status, response = make_request('POST', '/api/habits', {
'name': 'Bazin',
'frequency': 'daily'
})
assert status == 201, f"Expected 201, got {status}"
assert response['name'] == 'Bazin', "Habit name mismatch"
assert response['frequency'] == 'daily', "Frequency mismatch"
assert 'id' in response, "Missing habit ID"
habit_id = response['id']
print(f"✓ Created habit: {habit_id}")
# Step 2: Check it today (streak should be 1)
print("\nStep 2: Checking habit today (expecting streak = 1)...")
status, response = make_request('POST', f'/api/habits/{habit_id}/check', {})
assert status == 200, f"Expected 200, got {status}"
assert response['streak'] == 1, f"Expected streak=1, got {response['streak']}"
assert get_today() in response['completions'], "Today's date not in completions"
print(f"✓ Checked today, streak = {response['streak']}")
# Step 3: Simulate checking yesterday (manually add to completions)
print("\nStep 3: Simulating yesterday's check (expecting streak = 2)...")
# Read current habits.json
with open(HABITS_FILE, 'r') as f:
data = json.load(f)
# Find the habit and add yesterday's date
for habit in data['habits']:
if habit['id'] == habit_id:
habit['completions'].append(get_yesterday())
habit['completions'].sort() # Keep chronological order
break
# Write back to file
data['lastUpdated'] = datetime.utcnow().isoformat() + 'Z'
with open(HABITS_FILE, 'w') as f:
json.dump(data, f, indent=2)
# Verify streak calculation by fetching habits
status, response = make_request('GET', '/api/habits', None)
assert status == 200, f"Expected 200, got {status}"
habit = next((h for h in response['habits'] if h['id'] == habit_id), None)
assert habit is not None, "Habit not found in response"
assert habit['streak'] == 2, f"Expected streak=2 after adding yesterday, got {habit['streak']}"
print(f"✓ Added yesterday's completion, streak = {habit['streak']}")
# Step 4: Verify streak calculation is correct
print("\nStep 4: Verifying streak calculation...")
assert len(habit['completions']) == 2, f"Expected 2 completions, got {len(habit['completions'])}"
assert get_yesterday() in habit['completions'], "Yesterday not in completions"
assert get_today() in habit['completions'], "Today not in completions"
assert habit['checkedToday'] == True, "checkedToday should be True"
print("✓ Streak calculation verified: 2 consecutive days")
# Step 5: Delete habit successfully
print("\nStep 5: Deleting habit...")
status, response = make_request('DELETE', f'/api/habits/{habit_id}', None)
assert status == 200, f"Expected 200, got {status}"
assert 'message' in response, "Missing success message"
print(f"✓ Deleted habit: {response['message']}")
# Verify habit is gone
status, response = make_request('GET', '/api/habits', None)
assert status == 200, f"Expected 200, got {status}"
habit = next((h for h in response['habits'] if h['id'] == habit_id), None)
assert habit is None, "Habit still exists after deletion"
print("✓ Verified habit no longer exists")
print("\n=== All Integration Tests Passed ✓ ===\n")
def test_broken_streak():
"""
Additional test: Verify broken streak returns 0 (with gap)
"""
print("\n=== Additional Test: Broken Streak (with gap) ===\n")
# Create habit
status, response = make_request('POST', '/api/habits', {
'name': 'Sală',
'frequency': 'daily'
})
assert status == 201
habit_id = response['id']
print(f"✓ Created habit: {habit_id}")
# Add check from 3 days ago (creating a gap)
three_days_ago = (datetime.utcnow() - timedelta(days=3)).strftime('%Y-%m-%d')
with open(HABITS_FILE, 'r') as f:
data = json.load(f)
for habit in data['habits']:
if habit['id'] == habit_id:
habit['completions'].append(three_days_ago)
break
data['lastUpdated'] = datetime.utcnow().isoformat() + 'Z'
with open(HABITS_FILE, 'w') as f:
json.dump(data, f, indent=2)
# Verify streak is 0 (>1 day gap means broken streak)
status, response = make_request('GET', '/api/habits', None)
habit = next((h for h in response['habits'] if h['id'] == habit_id), None)
assert habit['streak'] == 0, f"Expected streak=0 for broken streak, got {habit['streak']}"
print(f"✓ Broken streak (>1 day gap) correctly returns 0")
# Cleanup
make_request('DELETE', f'/api/habits/{habit_id}', None)
print("✓ Cleanup complete")
def test_weekly_habit_streak():
"""
Additional test: Weekly habit streak calculation
"""
print("\n=== Additional Test: Weekly Habit Streak ===\n")
# Create weekly habit
status, response = make_request('POST', '/api/habits', {
'name': 'Yoga',
'frequency': 'weekly'
})
assert status == 201
habit_id = response['id']
print(f"✓ Created weekly habit: {habit_id}")
# Check today (streak = 1 week)
status, response = make_request('POST', f'/api/habits/{habit_id}/check', {})
assert status == 200
assert response['streak'] == 1, f"Expected streak=1 week, got {response['streak']}"
print(f"✓ Checked today, weekly streak = {response['streak']}")
# Add check from 8 days ago (last week)
eight_days_ago = (datetime.utcnow() - timedelta(days=8)).strftime('%Y-%m-%d')
with open(HABITS_FILE, 'r') as f:
data = json.load(f)
for habit in data['habits']:
if habit['id'] == habit_id:
habit['completions'].append(eight_days_ago)
habit['completions'].sort()
break
data['lastUpdated'] = datetime.utcnow().isoformat() + 'Z'
with open(HABITS_FILE, 'w') as f:
json.dump(data, f, indent=2)
# Verify streak is 2 weeks
status, response = make_request('GET', '/api/habits', None)
habit = next((h for h in response['habits'] if h['id'] == habit_id), None)
assert habit['streak'] == 2, f"Expected streak=2 weeks, got {habit['streak']}"
print(f"✓ Weekly streak calculation correct: {habit['streak']} weeks")
# Cleanup
make_request('DELETE', f'/api/habits/{habit_id}', None)
print("✓ Cleanup complete")
if __name__ == '__main__':
try:
# Setup
reset_habits_file()
setup_server()
# Run tests
test_complete_habit_lifecycle()
test_broken_streak()
test_weekly_habit_streak()
print("\n🎉 All Integration Tests Passed! 🎉\n")
except AssertionError as e:
print(f"\n❌ Test Failed: {e}\n")
sys.exit(1)
except Exception as e:
print(f"\n❌ Error: {e}\n")
import traceback
traceback.print_exc()
sys.exit(1)
finally:
# Cleanup
teardown_server()
reset_habits_file()

View File

@@ -0,0 +1,256 @@
#!/usr/bin/env python3
"""
Test suite for Story 14.0: Frontend - Responsive mobile design
Tests mobile responsiveness for habit tracker
"""
import re
from pathlib import Path
def test_file_exists():
"""AC: Test file exists"""
path = Path(__file__).parent / 'habits.html'
assert path.exists(), "habits.html should exist"
print("✓ File exists")
def test_modal_fullscreen_mobile():
"""AC1: Modal is full-screen on mobile (< 768px)"""
path = Path(__file__).parent / 'habits.html'
content = path.read_text()
# Check for mobile media query
assert '@media (max-width: 768px)' in content, "Should have mobile media query"
# Find the mobile section by locating the media query and extracting content until the closing brace
media_start = content.find('@media (max-width: 768px)')
assert media_start != -1, "Should have mobile media query"
# Extract a reasonable chunk after the media query (enough to include all mobile styles)
mobile_chunk = content[media_start:media_start + 3000]
# Check for modal full-screen styles within mobile section
assert '.modal {' in mobile_chunk or '.modal{' in mobile_chunk, "Mobile section should include .modal styles"
assert 'width: 100%' in mobile_chunk, "Modal should have 100% width on mobile"
assert 'height: 100vh' in mobile_chunk, "Modal should have 100vh height on mobile"
assert 'max-height: 100vh' in mobile_chunk, "Modal should have 100vh max-height on mobile"
assert 'border-radius: 0' in mobile_chunk, "Modal should have no border-radius on mobile"
print("✓ Modal is full-screen on mobile")
def test_habit_cards_stack_vertically():
"""AC2: Habit cards stack vertically on mobile"""
path = Path(__file__).parent / 'habits.html'
content = path.read_text()
# Check for habits-list with flex-direction: column
assert '.habits-list' in content, "Should have .habits-list class"
# Extract habits-list styles
habits_list_match = re.search(r'\.habits-list\s*\{([^}]+)\}', content)
assert habits_list_match, "Should have .habits-list styles"
habits_list_styles = habits_list_match.group(1)
assert 'display: flex' in habits_list_styles or 'display:flex' in habits_list_styles, "habits-list should use flexbox"
assert 'flex-direction: column' in habits_list_styles or 'flex-direction:column' in habits_list_styles, "habits-list should stack vertically"
# Find the mobile section
media_start = content.find('@media (max-width: 768px)')
mobile_chunk = content[media_start:media_start + 3000]
# Verify cards are full width on mobile
assert '.habit-card {' in mobile_chunk or '.habit-card{' in mobile_chunk, "Should have .habit-card mobile styles"
assert 'width: 100%' in mobile_chunk, "Should have 100% width on mobile"
print("✓ Habit cards stack vertically on mobile")
def test_touch_targets_44px():
"""AC3: Touch targets >= 44x44px for checkbox"""
path = Path(__file__).parent / 'habits.html'
content = path.read_text()
# Find the mobile section
media_start = content.find('@media (max-width: 768px)')
assert media_start != -1, "Should have mobile media query"
mobile_chunk = content[media_start:media_start + 3000]
# Check for checkbox sizing in mobile section
assert '.habit-checkbox {' in mobile_chunk or '.habit-checkbox{' in mobile_chunk, "Should have .habit-checkbox styles in mobile section"
# Extract width and height values from the mobile checkbox section
checkbox_section_start = mobile_chunk.find('.habit-checkbox')
checkbox_section = mobile_chunk[checkbox_section_start:checkbox_section_start + 300]
width_match = re.search(r'width:\s*(\d+)px', checkbox_section)
height_match = re.search(r'height:\s*(\d+)px', checkbox_section)
assert width_match, "Checkbox should have width specified"
assert height_match, "Checkbox should have height specified"
width = int(width_match.group(1))
height = int(height_match.group(1))
# Verify touch target size (44x44px minimum for accessibility)
assert width >= 44, f"Checkbox width should be >= 44px (got {width}px)"
assert height >= 44, f"Checkbox height should be >= 44px (got {height}px)"
# Check for other touch targets (buttons)
assert 'min-height: 44px' in mobile_chunk, "Buttons should have min-height of 44px"
print("✓ Touch targets are >= 44x44px")
def test_mobile_optimized_keyboards():
"""AC4: Form inputs use mobile-optimized keyboards"""
path = Path(__file__).parent / 'habits.html'
content = path.read_text()
# Check for input field
assert 'id="habitName"' in content, "Should have habitName input field"
# Extract input element
input_match = re.search(r'<input[^>]+id="habitName"[^>]*>', content)
assert input_match, "Should have habitName input element"
input_element = input_match.group(0)
# Check for mobile-optimized attributes
# autocapitalize="words" for proper names
# autocomplete="off" to prevent autofill issues
assert 'autocapitalize="words"' in input_element or 'autocapitalize=\'words\'' in input_element, \
"Input should have autocapitalize='words' for mobile optimization"
assert 'autocomplete="off"' in input_element or 'autocomplete=\'off\'' in input_element, \
"Input should have autocomplete='off' to prevent autofill"
# Verify type="text" is present (appropriate for habit names)
assert 'type="text"' in input_element, "Input should have type='text'"
print("✓ Form inputs use mobile-optimized keyboards")
def test_swipe_navigation():
"""AC5: Swipe navigation works (via swipe-nav.js)"""
path = Path(__file__).parent / 'habits.html'
content = path.read_text()
# Check for swipe-nav.js inclusion
assert 'swipe-nav.js' in content, "Should include swipe-nav.js for mobile swipe navigation"
# Verify script tag
assert '<script src="/echo/swipe-nav.js"></script>' in content, \
"Should have proper script tag for swipe-nav.js"
# Check for viewport meta tag (required for proper mobile rendering)
assert '<meta name="viewport"' in content, "Should have viewport meta tag"
assert 'width=device-width' in content, "Viewport should include width=device-width"
assert 'initial-scale=1.0' in content, "Viewport should include initial-scale=1.0"
print("✓ Swipe navigation is enabled")
def test_mobile_button_sizing():
"""Additional test: Verify all interactive elements have proper mobile sizing"""
path = Path(__file__).parent / 'habits.html'
content = path.read_text()
# Find the mobile section
media_start = content.find('@media (max-width: 768px)')
mobile_chunk = content[media_start:media_start + 3000]
# Check for add-habit-btn sizing
assert '.add-habit-btn {' in mobile_chunk or '.add-habit-btn{' in mobile_chunk, "Should have .add-habit-btn mobile styles"
assert 'min-height: 44px' in mobile_chunk, "Add habit button should have min-height 44px"
# Check for generic .btn sizing
assert '.btn {' in mobile_chunk or '.btn{' in mobile_chunk, "Should have .btn mobile styles"
# Check for radio labels sizing
assert '.radio-label {' in mobile_chunk or '.radio-label{' in mobile_chunk, "Should have .radio-label mobile styles"
print("✓ All buttons and interactive elements have proper mobile sizing")
def test_responsive_layout_structure():
"""Additional test: Verify responsive layout structure"""
path = Path(__file__).parent / 'habits.html'
content = path.read_text()
# Find the mobile section
media_start = content.find('@media (max-width: 768px)')
mobile_chunk = content[media_start:media_start + 3000]
# Verify main padding is adjusted for mobile
assert '.main {' in mobile_chunk or '.main{' in mobile_chunk, "Should have .main mobile styles"
main_section_start = mobile_chunk.find('.main')
main_section = mobile_chunk[main_section_start:main_section_start + 200]
assert 'padding' in main_section, "Main should have adjusted padding on mobile"
print("✓ Responsive layout structure is correct")
def test_all_acceptance_criteria():
"""Summary test: Verify all 6 acceptance criteria are met"""
path = Path(__file__).parent / 'habits.html'
content = path.read_text()
# Find mobile section
media_start = content.find('@media (max-width: 768px)')
mobile_chunk = content[media_start:media_start + 3000]
# AC1: Modal is full-screen on mobile
assert '.modal {' in mobile_chunk or '.modal{' in mobile_chunk, "AC1: Modal styles in mobile section"
assert 'width: 100%' in mobile_chunk, "AC1: Modal full-screen width"
assert 'height: 100vh' in mobile_chunk, "AC1: Modal full-screen height"
# AC2: Habit cards stack vertically
habits_list_match = re.search(r'\.habits-list\s*\{([^}]+)\}', content)
assert habits_list_match and 'flex-direction: column' in habits_list_match.group(1), "AC2: Cards stack vertically"
# AC3: Touch targets >= 44x44px
checkbox_section_start = mobile_chunk.find('.habit-checkbox')
checkbox_section = mobile_chunk[checkbox_section_start:checkbox_section_start + 300]
assert 'width: 44px' in checkbox_section, "AC3: Touch targets 44px width"
assert 'height: 44px' in checkbox_section, "AC3: Touch targets 44px height"
# AC4: Mobile-optimized keyboards
input_match = re.search(r'<input[^>]+id="habitName"[^>]*>', content)
assert input_match and 'autocapitalize="words"' in input_match.group(0), "AC4: Mobile keyboards"
# AC5: Swipe navigation
assert 'swipe-nav.js' in content, "AC5: Swipe navigation"
# AC6: Tests pass (this test itself)
print("✓ All 6 acceptance criteria verified")
def main():
"""Run all tests"""
tests = [
test_file_exists,
test_modal_fullscreen_mobile,
test_habit_cards_stack_vertically,
test_touch_targets_44px,
test_mobile_optimized_keyboards,
test_swipe_navigation,
test_mobile_button_sizing,
test_responsive_layout_structure,
test_all_acceptance_criteria
]
print("Running Story 14.0 mobile responsiveness tests...\n")
failed = []
for test in tests:
try:
test()
except AssertionError as e:
failed.append((test.__name__, str(e)))
print(f"{test.__name__}: {e}")
print(f"\n{'='*60}")
if failed:
print(f"FAILED: {len(failed)} test(s) failed")
for name, error in failed:
print(f" - {name}: {error}")
return False
else:
print(f"SUCCESS: All {len(tests)} tests passed! ✓")
return True
if __name__ == '__main__':
import sys
sys.exit(0 if main() else 1)

View File

@@ -0,0 +1,235 @@
#!/usr/bin/env python3
"""
Test Suite for Story 13.0: Frontend - Add to dashboard navigation
Tests that Habit Tracker link is added to main navigation properly.
"""
import os
import re
def test_file_existence():
"""Test that both index.html and habits.html exist."""
assert os.path.exists('dashboard/index.html'), "index.html should exist"
assert os.path.exists('dashboard/habits.html'), "habits.html should exist"
print("✓ Both HTML files exist")
def test_index_habits_link():
"""Test that index.html includes Habits link pointing to /echo/habits.html."""
with open('dashboard/index.html', 'r', encoding='utf-8') as f:
content = f.read()
# Check for Habits link with correct href
assert 'href="/echo/habits.html"' in content, "index.html should have link to /echo/habits.html"
# Check that Habits link exists in navigation
habits_link_pattern = r'<a[^>]*href="/echo/habits\.html"[^>]*class="nav-item"[^>]*>.*?<span>Habits</span>'
assert re.search(habits_link_pattern, content, re.DOTALL), "Habits link should be in nav-item format"
print("✓ index.html includes Habits link to /echo/habits.html (AC1, AC2)")
def test_index_flame_icon():
"""Test that index.html Habits link uses flame icon (lucide)."""
with open('dashboard/index.html', 'r', encoding='utf-8') as f:
content = f.read()
# Find the Habits nav item
habits_section = re.search(
r'<a[^>]*href="/echo/habits\.html"[^>]*>.*?</a>',
content,
re.DOTALL
)
assert habits_section, "Habits link should exist"
habits_html = habits_section.group(0)
# Check for flame icon (lucide)
assert 'data-lucide="flame"' in habits_html, "Habits link should use lucide flame icon"
print("✓ index.html Habits link uses flame icon (AC3)")
def test_habits_back_to_dashboard():
"""Test that habits.html navigation includes link back to dashboard."""
with open('dashboard/habits.html', 'r', encoding='utf-8') as f:
content = f.read()
# Check for Dashboard link
assert 'href="/echo/index.html"' in content, "habits.html should link back to dashboard"
# Check that Dashboard link exists in navigation
dashboard_link_pattern = r'<a[^>]*href="/echo/index\.html"[^>]*class="nav-item"[^>]*>.*?<span>Dashboard</span>'
assert re.search(dashboard_link_pattern, content, re.DOTALL), "Dashboard link should be in nav-item format"
print("✓ habits.html includes link back to dashboard (AC4)")
def test_habits_flame_icon():
"""Test that habits.html Habits link also uses flame icon."""
with open('dashboard/habits.html', 'r', encoding='utf-8') as f:
content = f.read()
# Find the Habits nav item in habits.html
habits_section = re.search(
r'<a[^>]*href="/echo/habits\.html"[^>]*class="nav-item active"[^>]*>.*?</a>',
content,
re.DOTALL
)
assert habits_section, "Habits link should exist in habits.html with active class"
habits_html = habits_section.group(0)
# Check for flame icon (lucide)
assert 'data-lucide="flame"' in habits_html, "habits.html Habits link should use lucide flame icon"
print("✓ habits.html Habits link uses flame icon (AC3)")
def test_active_state_styling():
"""Test that active state styling matches other nav items."""
with open('dashboard/habits.html', 'r', encoding='utf-8') as f:
habits_content = f.read()
with open('dashboard/index.html', 'r', encoding='utf-8') as f:
index_content = f.read()
# Check that habits.html has 'active' class on Habits nav item
habits_active = re.search(
r'<a[^>]*href="/echo/habits\.html"[^>]*class="nav-item active"',
habits_content
)
assert habits_active, "Habits nav item should have 'active' class in habits.html"
# Check that index.html has 'active' class on Dashboard nav item (pattern to follow)
index_active = re.search(
r'<a[^>]*href="/echo/index\.html"[^>]*class="nav-item active"',
index_content
)
assert index_active, "Dashboard nav item should have 'active' class in index.html"
# Both should use the same pattern (nav-item active)
print("✓ Active state styling matches other nav items (AC5)")
def test_mobile_navigation():
"""Test that mobile navigation is supported (shared nav structure)."""
with open('dashboard/index.html', 'r', encoding='utf-8') as f:
index_content = f.read()
with open('dashboard/habits.html', 'r', encoding='utf-8') as f:
habits_content = f.read()
# Check that both files include swipe-nav.js for mobile navigation
assert 'swipe-nav.js' in index_content, "index.html should include swipe-nav.js for mobile navigation"
assert 'swipe-nav.js' in habits_content, "habits.html should include swipe-nav.js for mobile navigation"
# Check that navigation uses the same class structure (nav-item)
# This ensures mobile navigation will work consistently
index_nav_items = len(re.findall(r'class="nav-item', index_content))
habits_nav_items = len(re.findall(r'class="nav-item', habits_content))
assert index_nav_items >= 5, "index.html should have at least 5 nav items (including Habits)"
assert habits_nav_items >= 5, "habits.html should have at least 5 nav items"
print("✓ Mobile navigation is supported (AC6)")
def test_navigation_completeness():
"""Test that navigation is complete on both pages."""
with open('dashboard/index.html', 'r', encoding='utf-8') as f:
index_content = f.read()
with open('dashboard/habits.html', 'r', encoding='utf-8') as f:
habits_content = f.read()
# Define expected navigation items
nav_items = [
('Dashboard', '/echo/index.html', 'layout-dashboard'),
('Workspace', '/echo/workspace.html', 'code'),
('KB', '/echo/notes.html', 'file-text'),
('Files', '/echo/files.html', 'folder'),
('Habits', '/echo/habits.html', 'flame')
]
# Check all items exist in both files
for label, href, icon in nav_items:
assert href in index_content, f"index.html should have link to {href}"
assert href in habits_content, f"habits.html should have link to {href}"
# Check flame icon specifically
assert 'data-lucide="flame"' in index_content, "index.html should have flame icon"
assert 'data-lucide="flame"' in habits_content, "habits.html should have flame icon"
print("✓ Navigation is complete on both pages with all 5 items")
def test_all_acceptance_criteria():
"""Summary test: verify all 7 acceptance criteria are met."""
print("\n=== Testing All Acceptance Criteria ===")
with open('dashboard/index.html', 'r', encoding='utf-8') as f:
index_content = f.read()
with open('dashboard/habits.html', 'r', encoding='utf-8') as f:
habits_content = f.read()
# AC1: index.html navigation includes 'Habits' link
ac1 = 'href="/echo/habits.html"' in index_content and 'class="nav-item"' in index_content
print(f"AC1 - index.html has Habits link: {'' if ac1 else ''}")
# AC2: Link points to /echo/habits.html
ac2 = 'href="/echo/habits.html"' in index_content
print(f"AC2 - Link points to /echo/habits.html: {'' if ac2 else ''}")
# AC3: Uses flame icon (lucide)
ac3 = 'data-lucide="flame"' in index_content and 'data-lucide="flame"' in habits_content
print(f"AC3 - Uses flame icon: {'' if ac3 else ''}")
# AC4: habits.html navigation includes link back to dashboard
ac4 = 'href="/echo/index.html"' in habits_content
print(f"AC4 - habits.html links back to dashboard: {'' if ac4 else ''}")
# AC5: Active state styling matches
ac5_habits = bool(re.search(r'href="/echo/habits\.html"[^>]*class="nav-item active"', habits_content))
ac5_index = bool(re.search(r'href="/echo/index\.html"[^>]*class="nav-item active"', index_content))
ac5 = ac5_habits and ac5_index
print(f"AC5 - Active state styling matches: {'' if ac5 else ''}")
# AC6: Mobile navigation supported
ac6 = 'swipe-nav.js' in index_content and 'swipe-nav.js' in habits_content
print(f"AC6 - Mobile navigation supported: {'' if ac6 else ''}")
# AC7: Tests pass (this test itself)
ac7 = True
print(f"AC7 - Tests for navigation pass: {'' if ac7 else ''}")
assert all([ac1, ac2, ac3, ac4, ac5, ac6, ac7]), "All acceptance criteria should pass"
print("\n✓ All 7 acceptance criteria met!")
if __name__ == '__main__':
print("Running Story 13.0 Navigation Tests...\n")
try:
test_file_existence()
test_index_habits_link()
test_index_flame_icon()
test_habits_back_to_dashboard()
test_habits_flame_icon()
test_active_state_styling()
test_mobile_navigation()
test_navigation_completeness()
test_all_acceptance_criteria()
print("\n" + "="*50)
print("✓ ALL TESTS PASSED")
print("="*50)
except AssertionError as e:
print(f"\n✗ TEST FAILED: {e}")
exit(1)
except Exception as e:
print(f"\n✗ ERROR: {e}")
exit(1)

21
memory/2026-02-10.md Normal file
View File

@@ -0,0 +1,21 @@
# 2026-02-10
## Dashboard ANAF - Detalii Modificări
**Context:** Marius a cerut să vadă ce modificări detectează ANAF Monitor în dashboard, nu doar mesaj generic "Modificări detectate".
**Implementare:**
1. **monitor_v2.py** - modificat `update_dashboard_status()` să salveze detalii în `status.json`:
- Nume pagină modificată
- URL către pagina ANAF
- Rezumat modificări (ex: "Soft A: 09.02.2026 → 10.02.2026")
2. **dashboard/index.html** - modificat `loadAnafStatus()` să afișeze detaliile:
- Link-uri clickabile către paginile ANAF
- Lista modificărilor pentru fiecare pagină
- Expandabil în secțiunea ANAF Monitor
**Modificare reală detectată astăzi:**
- D100 (Declarația 100) - Soft A: 09.02.2026 → 10.02.2026
**Status:** Implementat, netestat în browser. Așteaptă commit.

View File

@@ -1,5 +1,48 @@
{ {
"notes": [ "notes": [
{
"file": "notes-data/tools/antfarm-workflow.md",
"title": "Antfarm Workflow - Echo",
"date": "2026-02-10",
"tags": [],
"domains": [],
"types": [],
"category": "tools",
"project": null,
"subdir": null,
"video": "",
"tldr": "**Update:** După ce învăț fluxul mai bine"
},
{
"file": "memory/provocare-azi.md",
"title": "Provocarea Zilei - 2026-02-08",
"date": "2026-02-10",
"tags": [],
"domains": [],
"types": [
"memory"
],
"category": "memory",
"project": null,
"subdir": null,
"video": "",
"tldr": "- Monica Ion - Legea Fractalilor (Cele 7 Legi Universale)"
},
{
"file": "memory/2026-02-10.md",
"title": "2026-02-10",
"date": "2026-02-10",
"tags": [],
"domains": [],
"types": [
"memory"
],
"category": "memory",
"project": null,
"subdir": null,
"video": "",
"tldr": "**Status:** Aștept confirmare de la Marius să lansez `antfarm workflow run feature-dev`."
},
{ {
"file": "notes-data/coaching/2026-02-09-seara.md", "file": "notes-data/coaching/2026-02-09-seara.md",
"title": "Gândul de Seară - Duminică, 9 Februarie 2026", "title": "Gândul de Seară - Duminică, 9 Februarie 2026",
@@ -813,21 +856,6 @@
"video": "https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/articole/monica-ion/cele-7-legi-universale.md", "video": "https://moltbot.tailf7372d.ts.net/echo/files.html#memory/kb/articole/monica-ion/cele-7-legi-universale.md",
"tldr": "Cele 7 Legi Universale sunt principii fundamentale care explică cum funcționează mintea, de ce trăim viața așa cum o trăim și cum putem genera transformare reală. Fiecare lege este susținută de istori..." "tldr": "Cele 7 Legi Universale sunt principii fundamentale care explică cum funcționează mintea, de ce trăim viața așa cum o trăim și cum putem genera transformare reală. Fiecare lege este susținută de istori..."
}, },
{
"file": "memory/provocare-azi.md",
"title": "Provocarea Zilei - 2026-02-08",
"date": "2026-02-08",
"tags": [],
"domains": [],
"types": [
"memory"
],
"category": "memory",
"project": null,
"subdir": null,
"video": "",
"tldr": "- Monica Ion - Legea Fractalilor (Cele 7 Legi Universale)"
},
{ {
"file": "memory/jurnal-motivatie.md", "file": "memory/jurnal-motivatie.md",
"title": "Jurnal - Drumul spre regăsirea motivației", "title": "Jurnal - Drumul spre regăsirea motivației",
@@ -3346,7 +3374,7 @@
} }
], ],
"stats": { "stats": {
"total": 194, "total": 196,
"by_domain": { "by_domain": {
"work": 59, "work": 59,
"health": 34, "health": 34,
@@ -3365,9 +3393,9 @@
"projects": 85, "projects": 85,
"reflectii": 3, "reflectii": 3,
"retete": 1, "retete": 1,
"tools": 5, "tools": 6,
"youtube": 42, "youtube": 42,
"memory": 16 "memory": 17
} }
}, },
"domains": [ "domains": [

View File

@@ -0,0 +1,87 @@
# Antfarm Workflow - Echo
**Instalat:** github.com/snarktank/antfarm
**CLI:** `antfarm` (în PATH, global)
**Dashboard:** https://moltbot.tailf7372d.ts.net:3333
**Docs:** ~/clawd/antfarm/README.md, ~/clawd/antfarm/docs/creating-workflows.md
---
## Flux rapid (pentru Echo)
### 1. Primesc request de la Marius
**EX:** "Vreau să construiesc un Habit tracker în dashboard"
### 2. Lansez direct workflow-ul cu promptul lui Marius
```bash
cd ~/clawd
antfarm workflow run feature-dev "<prompt exact de la Marius>"
```
**NU:**
- ✗ Verific dacă e instalat (e instalat, permanent)
- ✗ Fac eu requirements/acceptance criteria (planner-ul face asta)
- ✗ Complicez task string-ul (simplitate = mai bine)
**DA:**
- ✓ Trimit prompt-ul EXACT cum îl primesc de la Marius
- ✓ Planner-ul descompune în stories automat
- ✓ Developer-ul decide tehnologii/structură
### 3. Monitorez progres
```bash
antfarm workflow status <run-id sau substring>
antfarm workflow runs # listă toate
```
### 4. Raportez când e gata
Agenții lucrează autonom (polling 15 min). Raportez când:
- Stories finalizate
- Erori care necesită intervenție
- PR creat pentru review
---
## Workflows disponibile
| Workflow | Când să-l folosesc |
|----------|-------------------|
| `feature-dev` | Features noi, refactoring, îmbunătățiri |
| `bug-fix` | Bug-uri cu pași de reproducere |
| `security-audit` | Audit securitate codebase |
---
## Comenzi utile
```bash
# Status rapid
antfarm workflow status <query>
# Force trigger agent (skip 15min wait)
cron action=run jobId=antfarm/feature-dev/developer
# Logs
antfarm logs 50
# Resume dacă failuit
antfarm workflow resume <run-id>
# Dashboard
antfarm dashboard status
```
---
## Reguli importante
1. **Task string = prompt exact de la Marius** (nu complica)
2. **Planner face requirements** (nu tu)
3. **Agenții sunt autonomi** (polling 15 min, nu trebuie să-i controlezi)
4. **Monitor dashboard** (https://moltbot.tailf7372d.ts.net:3333)
5. **Raportează doar când e relevant** (finalizare, erori, PR)
---
**Creat:** 2026-02-10
**Update:** După ce învăț fluxul mai bine

View File

@@ -273,6 +273,16 @@ CODEBASE PATTERNS UPDATE:
- Empty states: .empty-state with centered icon, message, and action button - Empty states: .empty-state with centered icon, message, and action button
- Icons: use lucide via data-lucide attribute, initialize with lucide.createIcons() - Icons: use lucide via data-lucide attribute, initialize with lucide.createIcons()
6. Mobile Responsiveness
- Use @media (max-width: 768px) for mobile breakpoint
- Touch targets: minimum 44x44px for WCAG compliance (checkboxes, buttons)
- Modal pattern: full-screen on mobile (100% width/height, no border-radius)
- Input optimization: autocapitalize="words" for proper names, autocomplete="off" for sensitive fields
- Navigation: swipe-nav.js provides mobile swipe gestures
- Viewport: include <meta name="viewport" content="width=device-width, initial-scale=1.0">
- All buttons should have min-height: 44px on mobile for easy tapping
- Flexbox direction already handles vertical stacking (flex-direction: column)
[✓] Story 8.0: Frontend - Create habit form modal [✓] Story 8.0: Frontend - Create habit form modal
Commit: 97af2ae Commit: 97af2ae
Date: 2026-02-10 Date: 2026-02-10
@@ -452,9 +462,246 @@ CODEBASE PATTERNS UPDATE:
- dashboard/test_habits_form_submit.py (created) - dashboard/test_habits_form_submit.py (created)
- dashboard/habits.json (reset to empty for testing) - dashboard/habits.json (reset to empty for testing)
[✓] Story 12.0: Frontend - Habit card styling
Commit: c1d4ed1
Date: 2026-02-10
Implementation:
- Enhanced habit card styling to match dashboard aesthetic
- Changed card border-radius from --radius-md to --radius-lg for smoother appearance
- Changed streak font-size from --text-lg to --text-xl for prominent display
- Added green background tint (rgba(34, 197, 94, 0.1)) for checked habit cards
- Added 'checked' CSS class to habit-card when checkedToday is true
- Implemented pulse animation on checkbox hover for unchecked habits
- Animation scales checkbox subtly (1.0 to 1.05) with 1.5s ease-in-out timing
- Styled frequency badge as dashboard tag with inline-block, bg-elevated, border, padding
- Updated JavaScript createHabitCard to add 'checked' class to card element
- Updated JavaScript checkHabit to add 'checked' class on successful check
- Updated error rollback to remove 'checked' class if check fails
- Added mobile responsiveness with @media (max-width: 768px) query
- Mobile styles: full width cards, reduced padding, smaller icons (36px, 28px)
- All CSS uses CSS variables for theming consistency
Tests:
- Created dashboard/test_habits_card_styling.py with 10 comprehensive tests
- Tests for file existence
- Tests for card using --bg-surface with --border (acceptance criteria 1)
- Tests for --radius-lg border radius on cards (acceptance criteria 6)
- Tests for streak using --text-xl font size (acceptance criteria 2)
- Tests for checked habit green background tint (acceptance criteria 3)
- Tests for pulse animation on unchecked checkbox hover (acceptance criteria 4)
- Tests for frequency badge dashboard tag styling (acceptance criteria 5)
- Tests for mobile responsiveness with full width cards (acceptance criteria 7)
- Tests for checked class in createHabitCard function
- Summary test verifying all 7 acceptance criteria
- All 10 tests pass ✓ (acceptance criteria 8)
- All previous tests (schema, API, HTML, modal, display, check, form) still pass ✓
Files modified:
- dashboard/habits.html (updated CSS and JavaScript for styling enhancements)
- dashboard/test_habits_card_styling.py (created)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
NEXT STEPS: NEXT STEPS:
- Continue with remaining 7 stories - Continue with remaining 3 stories
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[✓] Story 13.0: Frontend - Add to dashboard navigation
Commit: 1d56fe3
Date: 2026-02-10
Implementation:
- Added Habits link to index.html navigation (5th nav item)
- Link points to /echo/habits.html with flame icon (lucide)
- Changed habits.html Habits icon from "target" to "flame" for consistency
- Navigation structure matches existing pattern (nav-item class)
- Dashboard link already existed in habits.html (links back properly)
- Active state styling uses same pattern as other nav items
- Mobile navigation supported via shared swipe-nav.js
- All 5 navigation items now present on both pages
- Flame icon (🔥 lucide) used consistently across both pages
Tests:
- Created dashboard/test_habits_navigation.py with 9 comprehensive tests
- Tests for file existence
- Tests for index.html Habits link to /echo/habits.html (AC1, AC2)
- Tests for flame icon usage in index.html (AC3)
- Tests for habits.html link back to dashboard (AC4)
- Tests for flame icon usage in habits.html (AC3)
- Tests for active state styling consistency (AC5)
- Tests for mobile navigation support via swipe-nav.js (AC6)
- Tests for navigation completeness (all 5 items on both pages)
- Summary test verifying all 7 acceptance criteria
- All 9 tests pass ✓ (AC7)
- All previous tests (schema, API endpoints, HTML, modal, display, check, form, styling) still pass ✓ (except schema test expects empty habits.json)
Files modified:
- dashboard/index.html (added Habits nav link with flame icon)
- dashboard/habits.html (changed icon from target to flame)
- dashboard/test_habits_navigation.py (created)
[✓] Story 14.0: Frontend - Responsive mobile design
Commit: 0011664
Date: 2026-02-10
Implementation:
- Enhanced mobile responsiveness for habit tracker
- Modal is now full-screen on mobile (< 768px): 100% width/height, no border-radius
- Touch targets increased to 44x44px for checkboxes (from 28px)
- All buttons have min-height: 44px on mobile (add-habit-btn, .btn, .radio-label)
- Form input uses autocapitalize="words" for mobile-optimized keyboard
- Form input uses autocomplete="off" to prevent autofill issues
- Habit cards already stack vertically via flex-direction: column
- Cards are 100% width on mobile for optimal space usage
- Swipe navigation already enabled via swipe-nav.js inclusion
- Responsive padding adjustments for .main on mobile
- Icon sizes adjusted for mobile (habit-icon: 36px, checkbox icons: 20px)
- All interactive elements meet WCAG touch target guidelines (44x44px minimum)
Tests:
- Created dashboard/test_habits_mobile.py with 9 comprehensive tests
- Tests for mobile media query existence (@media max-width: 768px)
- Tests for modal full-screen on mobile (100% width/height, 100vh, no border-radius) [AC1]
- Tests for habit cards stacking vertically (flex-direction: column, 100% width) [AC2]
- Tests for touch targets >= 44x44px (checkbox: 44px, buttons: min-height 44px) [AC3]
- Tests for mobile-optimized keyboards (autocapitalize="words", autocomplete="off") [AC4]
- Tests for swipe navigation (swipe-nav.js, viewport meta tag) [AC5]
- Tests for all button sizing (add-habit-btn, .btn, .radio-label with min-height)
- Tests for responsive layout structure (.main padding adjustment)
- Summary test verifying all 6 acceptance criteria [AC6]
- All 9 tests pass ✓
- All previous tests (HTML structure, modal, display, check, form, styling, navigation) still pass ✓
Files modified:
- dashboard/habits.html (enhanced mobile CSS, added input attributes)
- dashboard/test_habits_mobile.py (created)
[✓] Story 15.0: Backend - Delete habit endpoint
Commit: 0f9c0de
Date: 2026-02-10
Implementation:
- Added do_DELETE method to api.py for handling DELETE requests
- Route: DELETE /api/habits/{id} deletes habit by ID
- Extracts habit ID from URL path (/api/habits/{id})
- Returns 404 if habit not found in habits.json
- Removes habit from habits array using list.pop(index)
- Updates lastUpdated timestamp after deletion
- Returns 200 with success message, including deleted habit ID
- Graceful error handling for missing/corrupt habits.json (returns 404)
- Follows existing API patterns (similar to handle_habits_check)
- Error responses include descriptive error messages
Tests:
- Created dashboard/test_habits_delete.py with 6 comprehensive tests
- Tests for habit removal from habits.json file (AC1)
- Tests for 200 status with success message response (AC2)
- Tests for 404 when habit not found (AC3)
- Tests for lastUpdated timestamp update (AC4)
- Tests for edge cases: deleting already deleted habit, invalid paths
- Tests for graceful handling when habits.json is missing
- All 6 tests pass ✓ (AC5)
- All previous tests (schema, GET, POST, streak, check) still pass ✓
Files modified:
- dashboard/api.py (added do_DELETE method and handle_habits_delete)
- dashboard/test_habits_delete.py (created)
[✓] Story 16.0: Frontend - Delete habit with confirmation
Commit: 46dc3a5
Date: 2026-02-10
Implementation:
- Added delete button with trash icon (lucide trash-2) to each habit card
- Delete button styled with 32x32px size, border, hover state with red color
- Hover state changes border and background to danger color (rgba(239, 68, 68, 0.1))
- Created delete confirmation modal (id='deleteModal') with modal-overlay pattern
- Confirmation modal shows message: "Ștergi obișnuința {name}?" with habit name
- Modal includes Cancel button (btn-secondary) and Delete button (btn-danger)
- Delete button uses destructive red styling (.btn-danger class)
- Added showDeleteModal(habitId, habitName) function to display confirmation
- Added hideDeleteModal() function to close modal
- Added confirmDelete() async function to execute DELETE API call
- Delete button disabled during deletion with loading text "Se șterge..."
- On successful delete: hides modal, shows success toast, reloads habits list
- On error: shows error toast, re-enables delete button, keeps modal open
- Habit name properly escaped for XSS protection when passed to modal
- All styling uses CSS variables for theme consistency
Tests:
- Created dashboard/test_habits_delete_ui.py with 10 comprehensive tests
- Tests for delete button CSS styling (size, border, hover, danger color) [AC1]
- Tests for trash-2 icon inclusion in habit cards [AC1]
- Tests for confirmation modal structure with Romanian message [AC2]
- Tests for Cancel and Delete buttons with correct handlers [AC3]
- Tests for btn-danger destructive red styling [AC4]
- Tests for DELETE API call to /api/habits/{id} endpoint [AC5]
- Tests for loadHabits() call after successful deletion (list refresh) [AC5]
- Tests for error handling with toast notification [AC6]
- Tests for modal show/hide functions and active class toggle
- Summary test verifying all 7 acceptance criteria [AC7]
- All 10 tests pass ✓
- All previous tests (schema, API endpoints, HTML, modal, display, check, form, styling, navigation, mobile) still pass ✓
Files modified:
- dashboard/habits.html (added delete button, modal, CSS, and JavaScript functions)
- dashboard/test_habits_delete_ui.py (created)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
NEXT STEPS:
- Continue with remaining 1 story (17.0)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[✓] Story 17.0: Integration - End-to-end habit lifecycle test
Commit: bf215f7
Date: 2026-02-10
Implementation:
- Created dashboard/test_habits_integration.py with comprehensive integration tests
- Main test: test_complete_habit_lifecycle() covers full habit flow
- Test creates daily habit 'Bazin' via POST /api/habits
- Checks habit today via POST /api/habits/{id}/check (streak = 1)
- Simulates yesterday's check by manipulating habits.json file
- Verifies streak calculation is correct (streak = 2 for consecutive days)
- Deletes habit successfully via DELETE /api/habits/{id}
- Verifies habit no longer exists after deletion
- Additional test: test_broken_streak() validates gap detection (streak = 0)
- Additional test: test_weekly_habit_streak() validates weekly habit streaks
- Tests use HTTP test server on port 8765 in background thread
- Comprehensive validation of all API endpoints working together
- Proper setup/teardown with habits.json reset before/after tests
Tests:
- Created dashboard/test_habits_integration.py
- Main integration test passes all 5 steps (create, check, simulate, verify, delete)
- Tests for daily habit creation and checking (AC1, AC2)
- Tests for simulating yesterday's check and streak = 2 (AC3, AC4)
- Tests for habit deletion after lifecycle (AC5)
- Additional tests for broken streaks (gap > 1 day returns 0)
- Additional tests for weekly habit streak calculation (2 consecutive weeks)
- All tests pass ✓ (AC6)
- All previous tests (schema, API endpoints, HTML, modal, display, check, form, styling, navigation, mobile, delete) still pass ✓
Files modified:
- dashboard/test_habits_integration.py (created)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
FEATURE COMPLETE! 🎉
All 17 stories completed successfully:
- Data schema and backend API (7 stories)
- Frontend UI and interactions (10 stories)
- Comprehensive integration tests
The Habit Tracker feature is now fully implemented and tested.
Users can create habits (daily/weekly), track completions, view streaks,
and delete habits. The feature includes responsive design, proper error handling,
and full integration with the dashboard navigation.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

View File

@@ -313,17 +313,33 @@ def check_page(page, saved_versions, saved_hashes):
log(f"OK: {page_id}") log(f"OK: {page_id}")
return None return None
def update_dashboard_status(has_changes, changes_count): def update_dashboard_status(has_changes, changes_count, changes_list=None):
"""Actualizează status.json pentru dashboard""" """Actualizează status.json pentru dashboard"""
try: try:
status = load_json(DASHBOARD_STATUS, {}) status = load_json(DASHBOARD_STATUS, {})
status['anaf'] = { anaf_status = {
'ok': not has_changes, 'ok': not has_changes,
'status': 'MODIFICĂRI' if has_changes else 'OK', 'status': 'MODIFICĂRI' if has_changes else 'OK',
'message': f'{changes_count} modificări detectate' if has_changes else 'Nicio modificare detectată', 'message': f'{changes_count} modificări detectate' if has_changes else 'Nicio modificare detectată',
'lastCheck': datetime.now().strftime('%d %b %Y, %H:%M'), 'lastCheck': datetime.now().strftime('%d %b %Y, %H:%M'),
'changesCount': changes_count 'changesCount': changes_count
} }
# Adaugă detaliile modificărilor pentru dashboard
if has_changes and changes_list:
anaf_status['changes'] = []
for change in changes_list:
change_detail = {
'name': change.get('name', ''),
'url': change.get('url', ''),
'summary': []
}
# Ia primele 3 modificări ca rezumat
if change.get('changes'):
change_detail['summary'] = change['changes'][:3]
anaf_status['changes'].append(change_detail)
status['anaf'] = anaf_status
save_json(DASHBOARD_STATUS, status) save_json(DASHBOARD_STATUS, status)
except Exception as e: except Exception as e:
log(f"ERROR updating dashboard status: {e}") log(f"ERROR updating dashboard status: {e}")
@@ -345,7 +361,7 @@ def main():
save_json(HASHES_FILE, saved_hashes) save_json(HASHES_FILE, saved_hashes)
# Update dashboard status # Update dashboard status
update_dashboard_status(len(all_changes) > 0, len(all_changes)) update_dashboard_status(len(all_changes) > 0, len(all_changes), all_changes)
log("=== Monitor complete ===") log("=== Monitor complete ===")