Compare commits
10 Commits
4acf5d6c9b
...
d36685c66b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d36685c66b | ||
|
|
bf215f768c | ||
|
|
3c3d6926b1 | ||
|
|
46dc3a5041 | ||
|
|
0f9c0de1a2 | ||
|
|
6837d6a925 | ||
|
|
0011664b58 | ||
|
|
1d56fe388e | ||
|
|
c7bea57cd3 | ||
|
|
c1d4ed1b03 |
1
antfarm
Submodule
1
antfarm
Submodule
Submodule antfarm added at 2fff211502
@@ -166,6 +166,12 @@ class TaskBoardHandler(SimpleHTTPRequestHandler):
|
||||
else:
|
||||
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):
|
||||
"""Run git commit and push."""
|
||||
try:
|
||||
@@ -988,6 +994,59 @@ class TaskBoardHandler(SimpleHTTPRequestHandler):
|
||||
except Exception as e:
|
||||
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):
|
||||
"""List files or get file content."""
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
.habit-card {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -100,6 +100,10 @@
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.habit-card.checked {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
}
|
||||
|
||||
.habit-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
@@ -129,8 +133,13 @@
|
||||
}
|
||||
|
||||
.habit-frequency {
|
||||
display: inline-block;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -138,12 +147,42 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
font-size: var(--text-lg);
|
||||
font-size: var(--text-xl);
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
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 {
|
||||
width: 32px;
|
||||
@@ -162,6 +201,12 @@
|
||||
.habit-checkbox:hover:not(.disabled) {
|
||||
border-color: var(--accent);
|
||||
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 {
|
||||
@@ -343,6 +388,121 @@
|
||||
transform: translateX(-50%) translateY(0);
|
||||
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>
|
||||
</head>
|
||||
<body>
|
||||
@@ -369,7 +529,7 @@
|
||||
<span>Files</span>
|
||||
</a>
|
||||
<a href="/echo/habits.html" class="nav-item active">
|
||||
<i data-lucide="target"></i>
|
||||
<i data-lucide="flame"></i>
|
||||
<span>Habits</span>
|
||||
</a>
|
||||
<button class="theme-toggle" onclick="toggleTheme()" title="Schimbă tema">
|
||||
@@ -422,7 +582,7 @@
|
||||
<h2 class="modal-title">Obișnuință nouă</h2>
|
||||
<div class="form-group">
|
||||
<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 class="form-group">
|
||||
<label class="form-label">Frecvență</label>
|
||||
@@ -444,6 +604,20 @@
|
||||
</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>
|
||||
|
||||
<script>
|
||||
@@ -627,13 +801,13 @@
|
||||
// Create habit card element
|
||||
function createHabitCard(habit) {
|
||||
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
|
||||
const iconName = habit.frequency === 'daily' ? 'calendar' : 'clock';
|
||||
|
||||
// Checkbox state
|
||||
const isChecked = habit.checkedToday || false;
|
||||
const checkboxClass = isChecked ? 'habit-checkbox checked disabled' : 'habit-checkbox';
|
||||
const checkIcon = isChecked ? '<i data-lucide="check"></i>' : '';
|
||||
|
||||
@@ -653,6 +827,9 @@
|
||||
<span id="streak-${habit.id}">${habit.streak || 0}</span>
|
||||
<span>🔥</span>
|
||||
</div>
|
||||
<button class="habit-delete-btn" onclick="showDeleteModal('${habit.id}', '${escapeHtml(habit.name).replace(/'/g, "'")}')">
|
||||
<i data-lucide="trash-2"></i>
|
||||
</button>
|
||||
`;
|
||||
|
||||
return card;
|
||||
@@ -677,6 +854,12 @@
|
||||
checkboxElement.innerHTML = '<i data-lucide="check"></i>';
|
||||
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
|
||||
const originalCheckbox = checkboxElement.cloneNode(true);
|
||||
const streakElement = document.getElementById(`streak-${habitId}`);
|
||||
@@ -708,6 +891,12 @@
|
||||
checkboxElement.classList.remove('checked', 'disabled');
|
||||
checkboxElement.innerHTML = '';
|
||||
|
||||
// Revert card background
|
||||
const card = checkboxElement.closest('.habit-card');
|
||||
if (card) {
|
||||
card.classList.remove('checked');
|
||||
}
|
||||
|
||||
// Revert streak
|
||||
if (streakElement) {
|
||||
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
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadHabits();
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
{
|
||||
"habits": [],
|
||||
"lastUpdated": "2026-02-10T12:39:00Z"
|
||||
"habits": [
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -1075,6 +1075,10 @@
|
||||
<i data-lucide="folder"></i>
|
||||
<span>Files</span>
|
||||
</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">
|
||||
<i data-lucide="sun" id="themeIcon"></i>
|
||||
</button>
|
||||
@@ -1600,10 +1604,34 @@
|
||||
const msg = status.anaf.ok !== false ? 'Nicio modificare' : (status.anaf.message || 'Modificări!');
|
||||
subtitle.textContent = `${msg} · ${lastCheck}`;
|
||||
|
||||
if (status.anaf.lastCheck) {
|
||||
document.getElementById('anafLastCheck').textContent =
|
||||
'Ultima verificare: ' + status.anaf.lastCheck;
|
||||
// Actualizează detaliile
|
||||
const details = document.getElementById('anafDetails');
|
||||
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;
|
||||
|
||||
@@ -13,7 +13,16 @@
|
||||
"ok": false,
|
||||
"status": "MODIFICĂRI",
|
||||
"message": "1 modificări detectate",
|
||||
"lastCheck": "09 Feb 2026, 14:00",
|
||||
"changesCount": 1
|
||||
"lastCheck": "10 Feb 2026, 12:39",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
256
dashboard/test_habits_card_styling.py
Normal file
256
dashboard/test_habits_card_styling.py
Normal 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}")
|
||||
203
dashboard/test_habits_delete.py
Normal file
203
dashboard/test_habits_delete.py
Normal 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)
|
||||
274
dashboard/test_habits_delete_ui.py
Normal file
274
dashboard/test_habits_delete_ui.py
Normal 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)
|
||||
279
dashboard/test_habits_integration.py
Normal file
279
dashboard/test_habits_integration.py
Normal 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()
|
||||
256
dashboard/test_habits_mobile.py
Normal file
256
dashboard/test_habits_mobile.py
Normal 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)
|
||||
235
dashboard/test_habits_navigation.py
Normal file
235
dashboard/test_habits_navigation.py
Normal 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
21
memory/2026-02-10.md
Normal 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.
|
||||
@@ -1,5 +1,48 @@
|
||||
{
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"title": "Jurnal - Drumul spre regăsirea motivației",
|
||||
@@ -3346,7 +3374,7 @@
|
||||
}
|
||||
],
|
||||
"stats": {
|
||||
"total": 194,
|
||||
"total": 196,
|
||||
"by_domain": {
|
||||
"work": 59,
|
||||
"health": 34,
|
||||
@@ -3365,9 +3393,9 @@
|
||||
"projects": 85,
|
||||
"reflectii": 3,
|
||||
"retete": 1,
|
||||
"tools": 5,
|
||||
"tools": 6,
|
||||
"youtube": 42,
|
||||
"memory": 16
|
||||
"memory": 17
|
||||
}
|
||||
},
|
||||
"domains": [
|
||||
|
||||
87
memory/kb/tools/antfarm-workflow.md
Normal file
87
memory/kb/tools/antfarm-workflow.md
Normal 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
|
||||
249
progress.txt
249
progress.txt
@@ -273,6 +273,16 @@ CODEBASE PATTERNS UPDATE:
|
||||
- Empty states: .empty-state with centered icon, message, and action button
|
||||
- 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
|
||||
Commit: 97af2ae
|
||||
Date: 2026-02-10
|
||||
@@ -452,9 +462,246 @@ CODEBASE PATTERNS UPDATE:
|
||||
- dashboard/test_habits_form_submit.py (created)
|
||||
- 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:
|
||||
- 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.
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
@@ -313,17 +313,33 @@ def check_page(page, saved_versions, saved_hashes):
|
||||
log(f"OK: {page_id}")
|
||||
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"""
|
||||
try:
|
||||
status = load_json(DASHBOARD_STATUS, {})
|
||||
status['anaf'] = {
|
||||
anaf_status = {
|
||||
'ok': not has_changes,
|
||||
'status': 'MODIFICĂRI' if has_changes else 'OK',
|
||||
'message': f'{changes_count} modificări detectate' if has_changes else 'Nicio modificare detectată',
|
||||
'lastCheck': datetime.now().strftime('%d %b %Y, %H:%M'),
|
||||
'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)
|
||||
except Exception as e:
|
||||
log(f"ERROR updating dashboard status: {e}")
|
||||
@@ -345,7 +361,7 @@ def main():
|
||||
save_json(HASHES_FILE, saved_hashes)
|
||||
|
||||
# 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 ===")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user