diff --git a/AGENTS.md b/AGENTS.md index d750241..d50c5ad 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -221,7 +221,13 @@ Când primesc un link YouTube: - Evită să rămână fișiere uncommitted prea mult timp - **Script:** `python3 ~/clawd/tools/git_commit.py --push` (auto-generează commit message) -### 📋 Cron Jobs + Kanban (OBLIGATORIU) +### 📋 Task Tracking (OBLIGATORIU) +Când primesc o acțiune/cerere de la Marius: +1. **React:** Reacționez cu 👍 la mesaj (WhatsApp/Discord) +2. **Start:** Adaug task în kanban (in-progress) cu `python3 kanban/update_task.py add "titlu"` +3. **Lucrez:** Execut cererea +4. **Done:** Marchez task-ul terminat cu `python3 kanban/update_task.py done ` + Când se execută orice job cron: 1. **Start:** Creează task în kanban (Progress) cu numele job-ului 2. **Rulează:** Execută task-ul diff --git a/TOOLS.md b/TOOLS.md index aefd564..620cb3e 100644 --- a/TOOLS.md +++ b/TOOLS.md @@ -31,6 +31,10 @@ python3 tools/email_send.py "dest@email.com" "Subiect" "Corp mesaj" - **API:** `kanban/api.py` - **Update task:** `python3 kanban/update_task.py` +**Reguli dashboard:** +- Tab Activity afișează task-uri din tasks.json, sortate descrescător după timestamp +- Când creez/completez task-uri, să am timestamp complet (ISO format cu oră) + ### Notes (toate tipurile) - **Folder:** `notes/` (subdirectoare: `youtube/`, `retete/`, etc.) - **Update index:** `python3 tools/update_notes_index.py` diff --git a/kanban/index.html b/kanban/index.html index 9355de7..ddd3172 100644 --- a/kanban/index.html +++ b/kanban/index.html @@ -996,15 +996,56 @@ } async function loadActivity() { - // For now, show static data. TODO: fetch from API - activityData = [ - { type: 'done', text: 'Răspuns întrebare D101', agent: 'Echo Work', time: '15:10' }, - { type: 'done', text: 'Propunere dashboard v2', agent: 'Echo Work', time: '15:23' }, - { type: 'done', text: 'Fix notes.html loading', agent: 'Echo Work', time: '17:39' } - ]; + try { + // Fetch from tasks.json + const response = await fetch('tasks.json?t=' + Date.now()); + const data = await response.json(); + + // Collect all tasks from all columns with their status + let allTasks = []; + data.columns.forEach(col => { + col.tasks.forEach(task => { + const timestamp = task.completed || task.created || ''; + allTasks.push({ + type: col.id === 'done' ? 'done' : (col.id === 'in-progress' ? 'running' : 'pending'), + text: task.title, + agent: task.agent || 'Echo', + time: formatActivityTime(timestamp), + timestamp: new Date(timestamp).getTime() || 0 + }); + }); + }); + + // Sort descending by timestamp (newest first) + allTasks.sort((a, b) => b.timestamp - a.timestamp); + + // Take only recent items (last 20) + activityData = allTasks.slice(0, 20); + + } catch (e) { + console.error('Failed to load activity:', e); + activityData = []; + } + renderActivity(); document.getElementById('activityCount').textContent = activityData.length; } + + function formatActivityTime(timestamp) { + if (!timestamp) return ''; + const date = new Date(timestamp); + if (isNaN(date.getTime())) return timestamp; + + const now = new Date(); + const isToday = date.toDateString() === now.toDateString(); + + if (isToday) { + return date.toLocaleTimeString('ro-RO', { hour: '2-digit', minute: '2-digit' }); + } else { + return date.toLocaleDateString('ro-RO', { day: 'numeric', month: 'short' }) + + ' ' + date.toLocaleTimeString('ro-RO', { hour: '2-digit', minute: '2-digit' }); + } + } function refreshActivity() { loadActivity(); diff --git a/kanban/notes.html b/kanban/notes.html index 81e12c0..89ba334 100644 --- a/kanban/notes.html +++ b/kanban/notes.html @@ -292,6 +292,64 @@ margin-bottom: var(--space-5); } + .tag-filter-header { + display: flex; + align-items: center; + gap: var(--space-2); + cursor: pointer; + user-select: none; + margin-bottom: var(--space-2); + } + + .tag-filter-header svg { + width: 16px; + height: 16px; + color: var(--text-muted); + transition: transform var(--transition-fast); + } + + .tag-filter.collapsed .tag-filter-header svg { + transform: rotate(-90deg); + } + + .tag-pills-more { + display: none; + margin-top: var(--space-2); + } + + .tag-pills-more.expanded { + display: flex; + flex-wrap: wrap; + gap: var(--space-2); + } + + /* More tags toggle button - same style as pills */ + .tag-pill.more-toggle { + background: rgba(100, 116, 139, 0.2); + border-color: rgba(100, 116, 139, 0.4); + color: var(--text-muted); + } + .tag-pill.more-toggle:hover { + background: rgba(100, 116, 139, 0.3); + } + .tag-pill.more-toggle.expanded { + background: rgba(100, 116, 139, 0.4); + } + + /* Dimmed pills - tags not in visible notes */ + .tag-pill.dimmed { + opacity: 0.35; + } + .tag-pill.dimmed:hover { + opacity: 0.6; + } + + .filter-count { + font-size: var(--text-xs); + color: var(--text-muted); + margin-left: var(--space-1); + } + .tag-filter-label { font-size: var(--text-xs); color: var(--text-muted); @@ -335,6 +393,44 @@ color: white; } + /* Category pills (📁) - teal */ + .tag-pill.category { + background: rgba(20, 184, 166, 0.2); + border-color: rgba(20, 184, 166, 0.5); + color: #14b8a6; + } + .tag-pill.category:hover { + background: rgba(20, 184, 166, 0.3); + } + .tag-pill.category.active { + background: #14b8a6; + border-color: #14b8a6; + color: white; + } + + /* Domain pills (@) - purple */ + .tag-pill.domain { + background: rgba(139, 92, 246, 0.2); + border-color: rgba(139, 92, 246, 0.5); + color: #8b5cf6; + } + .tag-pill.domain:hover { + background: rgba(139, 92, 246, 0.3); + } + .tag-pill.domain.active { + background: #8b5cf6; + border-color: #8b5cf6; + color: white; + } + + /* Light mode adjustments */ + [data-theme="light"] .tag-pill.category { + color: #0d9488; + } + [data-theme="light"] .tag-pill.domain { + color: #7c3aed; + } + .tag-pill-count { font-size: var(--text-xs); opacity: 0.7; @@ -426,8 +522,8 @@
- Filtrează după tags: -
+
+
@@ -482,6 +578,7 @@ const notesCache = {}; let notesIndex = []; + let lastFilteredNotes = null; const notesBasePath = "notes-data/"; const indexPath = notesBasePath + "index.json"; @@ -499,37 +596,107 @@ } let selectedTags = new Set(); - // Extract all tags with counts + // Extract all tags with counts (including domains and categories) function getAllTags() { const tagCounts = {}; notesIndex.forEach(note => { + // Category tags (📁youtube, 📁retete) + if (note.category) { + const catTag = '📁' + note.category; + tagCounts[catTag] = (tagCounts[catTag] || 0) + 1; + } + // Domain tags (@work, @health, etc.) + if (note.domains) { + note.domains.forEach(domain => { + const domainTag = '@' + domain; + tagCounts[domainTag] = (tagCounts[domainTag] || 0) + 1; + }); + } + // Regular tags note.tags.forEach(tag => { tagCounts[tag] = (tagCounts[tag] || 0) + 1; }); }); - // Sort by count descending + // Sort: categories first (📁), then domains (@), then by count return Object.entries(tagCounts) - .sort((a, b) => b[1] - a[1]) + .sort((a, b) => { + const aIsCat = a[0].startsWith('📁'); + const bIsCat = b[0].startsWith('📁'); + const aIsDomain = a[0].startsWith('@'); + const bIsDomain = b[0].startsWith('@'); + if (aIsCat && !bIsCat) return -1; + if (!aIsCat && bIsCat) return 1; + if (aIsDomain && !bIsDomain) return -1; + if (!aIsDomain && bIsDomain) return 1; + return b[1] - a[1]; + }) .map(([tag, count]) => ({ tag, count })); } // Render tag pills - function renderTagPills() { - const container = document.getElementById('tagPills'); + function renderTagPills(visibleNotes = null) { + const mainContainer = document.getElementById('mainPills'); + const moreContainer = document.getElementById('tagPills'); const tags = getAllTags(); - let html = tags.map(({ tag, count }) => ` - - ${tag} (${count}) - - `).join(''); + // Calculate which tags appear in visible notes + const visibleTags = new Set(); + if (visibleNotes && visibleNotes.length > 0) { + visibleNotes.forEach(note => { + note.tags.forEach(t => visibleTags.add(t)); + (note.domains || []).forEach(d => visibleTags.add('@' + d)); + if (note.category) visibleTags.add('📁' + note.category); + }); + } + const hasFilter = visibleNotes !== null; - if (selectedTags.size > 0) { - html += ``; + // Separate: categories + domains vs regular tags + const mainTags = tags.filter(({tag}) => tag.startsWith('📁') || tag.startsWith('@')); + const moreTags = tags.filter(({tag}) => !tag.startsWith('📁') && !tag.startsWith('@')); + + // Check if more section is expanded + const isExpanded = moreContainer.classList.contains('expanded'); + const activeMoreCount = [...selectedTags].filter(t => !t.startsWith('📁') && !t.startsWith('@')).length; + + // Render main pills (categories + domains) + let mainHtml = mainTags.map(({ tag, count }) => { + let pillClass = 'tag-pill'; + if (tag.startsWith('📁')) pillClass += ' category'; + else if (tag.startsWith('@')) pillClass += ' domain'; + if (selectedTags.has(tag)) pillClass += ' active'; + // Dim tags not in visible notes (unless it's already selected) + if (hasFilter && !visibleTags.has(tag) && !selectedTags.has(tag)) pillClass += ' dimmed'; + return ` + ${tag} (${count}) + `; + }).join(''); + + // Add "more tags" toggle button inline + if (moreTags.length > 0) { + const visibleMoreCount = moreTags.filter(({tag}) => visibleTags.has(tag)).length; + const moreLabel = activeMoreCount > 0 + ? `+${moreTags.length} tags (${activeMoreCount} active)` + : (hasFilter && visibleMoreCount > 0 ? `+${visibleMoreCount}/${moreTags.length} tags` : `+${moreTags.length} tags`); + mainHtml += ` + ${isExpanded ? '−' : '+'} ${moreLabel} + `; } - container.innerHTML = html; + if (selectedTags.size > 0) { + mainHtml += ``; + } + mainContainer.innerHTML = mainHtml; + + // Render more pills (regular tags) + const moreHtml = moreTags.map(({ tag, count }) => { + let pillClass = 'tag-pill'; + if (selectedTags.has(tag)) pillClass += ' active'; + if (hasFilter && !visibleTags.has(tag) && !selectedTags.has(tag)) pillClass += ' dimmed'; + return ` + ${tag} (${count}) + `; + }).join(''); + moreContainer.innerHTML = moreHtml; } // Toggle tag selection @@ -550,17 +717,29 @@ filterNotes(); } + // Toggle more tags section + function toggleMoreTags() { + const moreContainer = document.getElementById('tagPills'); + moreContainer.classList.toggle('expanded'); + renderTagPills(lastFilteredNotes); + } + // Filter notes by search and tags function filterNotes() { const query = document.getElementById('searchInput').value.toLowerCase().trim(); let filtered = notesIndex; - // Filter by selected tags (AND logic) + // Filter by selected tags (AND logic) - includes categories and domains if (selectedTags.size > 0) { - filtered = filtered.filter(note => - [...selectedTags].every(tag => note.tags.includes(tag)) - ); + filtered = filtered.filter(note => { + const allNoteTags = [ + ...note.tags, + ...(note.domains || []).map(d => '@' + d), + note.category ? '📁' + note.category : null + ].filter(Boolean); + return [...selectedTags].every(tag => allNoteTags.includes(tag)); + }); } // Filter by search query @@ -574,6 +753,12 @@ } renderNotesAccordion(filtered); + + // Save filtered notes for tag pills + lastFilteredNotes = filtered; + + // Update tag pills to show which tags are in visible notes + renderTagPills(filtered); } // Group notes by date category diff --git a/kanban/status.json b/kanban/status.json index 198014c..cd28419 100644 --- a/kanban/status.json +++ b/kanban/status.json @@ -1,21 +1,4 @@ { - "anaf": { - "status": "OK", - "ok": true, - "lastCheck": "2026-01-30T13:50:00Z" - }, - "git": { - "status": "curat", - "clean": true, - "files": 0 - }, - "agents": { - "count": 5, - "list": ["echo-work", "echo-health", "echo-growth", "echo-sprijin", "echo-scout"] - }, - "lastReport": { - "type": "test", - "summary": "Ecosistem configurat, totul în ordine", - "time": "30 ian 2026, 13:55" - } + "git": {"status": "4 fișiere", "clean": false, "files": 4}, + "lastReport": {"type": "evening", "summary": "notes.html îmbunătățit (filtre colorate), rețetă salvată", "time": "30 Jan 2026, 22:00"} } diff --git a/kanban/tasks.json b/kanban/tasks.json index ff60ae7..516cb87 100644 --- a/kanban/tasks.json +++ b/kanban/tasks.json @@ -1,5 +1,5 @@ { - "lastUpdated": "2026-01-29T20:43:29.785Z", + "lastUpdated": "2026-01-30T20:26:37.897978Z", "columns": [ { "id": "backlog", @@ -198,6 +198,38 @@ "created": "2026-01-29", "priority": "medium", "completed": "2026-01-29" + }, + { + "id": "task-030", + "title": "Test task tracking", + "description": "", + "created": "2026-01-30T20:12:25Z", + "priority": "medium", + "completed": "2026-01-30T20:12:29Z" + }, + { + "id": "task-031", + "title": "Fix notes tag coloring on expand", + "description": "", + "created": "2026-01-30T20:16:46Z", + "priority": "medium", + "completed": "2026-01-30T20:17:08Z" + }, + { + "id": "task-032", + "title": "Fix cron jobs timezone Bucharest", + "description": "", + "created": "2026-01-30T20:21:26Z", + "priority": "medium", + "completed": "2026-01-30T20:21:44Z" + }, + { + "id": "task-033", + "title": "Redirect coaching to @health, reports to @work", + "description": "", + "created": "2026-01-30T20:25:22Z", + "priority": "medium", + "completed": "2026-01-30T20:26:37Z" } ] } diff --git a/notes/index.json b/notes/index.json index b4e30ad..10d5843 100644 --- a/notes/index.json +++ b/notes/index.json @@ -18,6 +18,23 @@ "tldr": "Kitze folosește **UN SINGUR gateway Clawdbot** cu **MULTIPLE PERSONAS** pe Telegram/Discord. Fiecare personă are:\n- Personalitate diferită (avatar, stil de vorbit)\n- Skills diferite (acces la tool-uri...", "category": "youtube" }, + { + "file": "retete/2026-01-30_ciorba-burta-falsa-cu-pui.md", + "title": "Ciorbă de Burtă Falsă cu Pui și Ciuperci Pleurotus", + "date": "2026-01-30", + "tags": [ + "ciorba", + "reteta", + "pleurotus", + "pui" + ], + "domains": [ + "health" + ], + "video": "", + "tldr": "", + "category": "retete" + }, { "file": "youtube/2026-01-29_remotion-skill-claude-code.md", "title": "How people are generating videos with Claude Code (Remotion Skill)", @@ -139,23 +156,13 @@ "video": "https://youtu.be/I9-tdhxiH7w", "tldr": "Un pattern puternic pentru Claude Code: **Do Work** - o coadă de task-uri pe care Claude le procesează automat, unul câte unul, în sub-agenți cu context curat. Ideea cheie: **construiește tool-uri pen...", "category": "youtube" - }, - { - "file": "retete/ciorba-burta-falsa-cu-pui.md", - "title": "Ciorbă de Burtă Falsă cu Pui și Ciuperci Pleurotus", - "date": "", - "tags": [], - "domains": [], - "video": "", - "tldr": "", - "category": "retete" } ], "stats": { "total": 9, "by_domain": { "work": 7, - "health": 1, + "health": 2, "growth": 1, "sprijin": 0, "scout": 0 diff --git a/notes/retete/ciorba-burta-falsa-cu-pui.md b/notes/retete/2026-01-30_ciorba-burta-falsa-cu-pui.md similarity index 97% rename from notes/retete/ciorba-burta-falsa-cu-pui.md rename to notes/retete/2026-01-30_ciorba-burta-falsa-cu-pui.md index 6119f32..d3c5ab7 100644 --- a/notes/retete/ciorba-burta-falsa-cu-pui.md +++ b/notes/retete/2026-01-30_ciorba-burta-falsa-cu-pui.md @@ -4,6 +4,7 @@ **Sursa:** Laura Laurențiu (adaptat) **Timp preparare:** ~50 minute **Porții:** 6-8 +**Tags:** #ciorba #reteta #pleurotus #pui @health ---