Improve Activity panel + task tracking rules

- Activity loads from tasks.json dynamically
- Added task tracking workflow in AGENTS.md
- Notes UI improvements
- Renamed recipe with date prefix
This commit is contained in:
Echo
2026-01-30 20:58:30 +00:00
parent fdabea7271
commit ea4101710f
8 changed files with 317 additions and 58 deletions

View File

@@ -221,7 +221,13 @@ Când primesc un link YouTube:
- Evită să rămână fișiere uncommitted prea mult timp - Evită să rămână fișiere uncommitted prea mult timp
- **Script:** `python3 ~/clawd/tools/git_commit.py --push` (auto-generează commit message) - **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 <task-id>`
Când se execută orice job cron: Când se execută orice job cron:
1. **Start:** Creează task în kanban (Progress) cu numele job-ului 1. **Start:** Creează task în kanban (Progress) cu numele job-ului
2. **Rulează:** Execută task-ul 2. **Rulează:** Execută task-ul

View File

@@ -31,6 +31,10 @@ python3 tools/email_send.py "dest@email.com" "Subiect" "Corp mesaj"
- **API:** `kanban/api.py` - **API:** `kanban/api.py`
- **Update task:** `python3 kanban/update_task.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) ### Notes (toate tipurile)
- **Folder:** `notes/` (subdirectoare: `youtube/`, `retete/`, etc.) - **Folder:** `notes/` (subdirectoare: `youtube/`, `retete/`, etc.)
- **Update index:** `python3 tools/update_notes_index.py` - **Update index:** `python3 tools/update_notes_index.py`

View File

@@ -996,15 +996,56 @@
} }
async function loadActivity() { async function loadActivity() {
// For now, show static data. TODO: fetch from API try {
activityData = [ // Fetch from tasks.json
{ type: 'done', text: 'Răspuns întrebare D101', agent: 'Echo Work', time: '15:10' }, const response = await fetch('tasks.json?t=' + Date.now());
{ type: 'done', text: 'Propunere dashboard v2', agent: 'Echo Work', time: '15:23' }, const data = await response.json();
{ type: 'done', text: 'Fix notes.html loading', agent: 'Echo Work', time: '17:39' }
]; // 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(); renderActivity();
document.getElementById('activityCount').textContent = activityData.length; 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() { function refreshActivity() {
loadActivity(); loadActivity();

View File

@@ -292,6 +292,64 @@
margin-bottom: var(--space-5); 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 { .tag-filter-label {
font-size: var(--text-xs); font-size: var(--text-xs);
color: var(--text-muted); color: var(--text-muted);
@@ -335,6 +393,44 @@
color: white; 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 { .tag-pill-count {
font-size: var(--text-xs); font-size: var(--text-xs);
opacity: 0.7; opacity: 0.7;
@@ -426,8 +522,8 @@
</div> </div>
<div class="tag-filter"> <div class="tag-filter">
<span class="tag-filter-label">Filtrează după tags:</span> <div class="tag-pills" id="mainPills"></div>
<div class="tag-pills" id="tagPills"></div> <div class="tag-pills-more" id="tagPills"></div>
</div> </div>
<div id="notesContainer"> <div id="notesContainer">
@@ -482,6 +578,7 @@
const notesCache = {}; const notesCache = {};
let notesIndex = []; let notesIndex = [];
let lastFilteredNotes = null;
const notesBasePath = "notes-data/"; const notesBasePath = "notes-data/";
const indexPath = notesBasePath + "index.json"; const indexPath = notesBasePath + "index.json";
@@ -499,37 +596,107 @@
} }
let selectedTags = new Set(); let selectedTags = new Set();
// Extract all tags with counts // Extract all tags with counts (including domains and categories)
function getAllTags() { function getAllTags() {
const tagCounts = {}; const tagCounts = {};
notesIndex.forEach(note => { 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 => { note.tags.forEach(tag => {
tagCounts[tag] = (tagCounts[tag] || 0) + 1; tagCounts[tag] = (tagCounts[tag] || 0) + 1;
}); });
}); });
// Sort by count descending // Sort: categories first (📁), then domains (@), then by count
return Object.entries(tagCounts) 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 })); .map(([tag, count]) => ({ tag, count }));
} }
// Render tag pills // Render tag pills
function renderTagPills() { function renderTagPills(visibleNotes = null) {
const container = document.getElementById('tagPills'); const mainContainer = document.getElementById('mainPills');
const moreContainer = document.getElementById('tagPills');
const tags = getAllTags(); const tags = getAllTags();
let html = tags.map(({ tag, count }) => ` // Calculate which tags appear in visible notes
<span class="tag-pill ${selectedTags.has(tag) ? 'active' : ''}" const visibleTags = new Set();
onclick="toggleTag('${tag}')"> if (visibleNotes && visibleNotes.length > 0) {
${tag} <span class="tag-pill-count">(${count})</span> visibleNotes.forEach(note => {
</span> note.tags.forEach(t => visibleTags.add(t));
`).join(''); (note.domains || []).forEach(d => visibleTags.add('@' + d));
if (note.category) visibleTags.add('📁' + note.category);
});
}
const hasFilter = visibleNotes !== null;
if (selectedTags.size > 0) { // Separate: categories + domains vs regular tags
html += `<button class="clear-filters" onclick="clearTagFilters()">✕ Clear</button>`; 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 `<span class="${pillClass}" onclick="toggleTag('${tag}')">
${tag} <span class="tag-pill-count">(${count})</span>
</span>`;
}).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 += `<span class="tag-pill more-toggle ${isExpanded ? 'expanded' : ''}" onclick="toggleMoreTags()">
${isExpanded ? '' : '+'} ${moreLabel}
</span>`;
} }
container.innerHTML = html; if (selectedTags.size > 0) {
mainHtml += `<button class="clear-filters" onclick="clearTagFilters()">✕ Clear</button>`;
}
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 `<span class="${pillClass}" onclick="toggleTag('${tag}')">
${tag} <span class="tag-pill-count">(${count})</span>
</span>`;
}).join('');
moreContainer.innerHTML = moreHtml;
} }
// Toggle tag selection // Toggle tag selection
@@ -550,17 +717,29 @@
filterNotes(); filterNotes();
} }
// Toggle more tags section
function toggleMoreTags() {
const moreContainer = document.getElementById('tagPills');
moreContainer.classList.toggle('expanded');
renderTagPills(lastFilteredNotes);
}
// Filter notes by search and tags // Filter notes by search and tags
function filterNotes() { function filterNotes() {
const query = document.getElementById('searchInput').value.toLowerCase().trim(); const query = document.getElementById('searchInput').value.toLowerCase().trim();
let filtered = notesIndex; let filtered = notesIndex;
// Filter by selected tags (AND logic) // Filter by selected tags (AND logic) - includes categories and domains
if (selectedTags.size > 0) { if (selectedTags.size > 0) {
filtered = filtered.filter(note => filtered = filtered.filter(note => {
[...selectedTags].every(tag => note.tags.includes(tag)) 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 // Filter by search query
@@ -574,6 +753,12 @@
} }
renderNotesAccordion(filtered); 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 // Group notes by date category

View File

@@ -1,21 +1,4 @@
{ {
"anaf": { "git": {"status": "4 fișiere", "clean": false, "files": 4},
"status": "OK", "lastReport": {"type": "evening", "summary": "notes.html îmbunătățit (filtre colorate), rețetă salvată", "time": "30 Jan 2026, 22:00"}
"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"
}
} }

View File

@@ -1,5 +1,5 @@
{ {
"lastUpdated": "2026-01-29T20:43:29.785Z", "lastUpdated": "2026-01-30T20:26:37.897978Z",
"columns": [ "columns": [
{ {
"id": "backlog", "id": "backlog",
@@ -198,6 +198,38 @@
"created": "2026-01-29", "created": "2026-01-29",
"priority": "medium", "priority": "medium",
"completed": "2026-01-29" "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"
} }
] ]
} }

View File

@@ -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...", "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" "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", "file": "youtube/2026-01-29_remotion-skill-claude-code.md",
"title": "How people are generating videos with Claude Code (Remotion Skill)", "title": "How people are generating videos with Claude Code (Remotion Skill)",
@@ -139,23 +156,13 @@
"video": "https://youtu.be/I9-tdhxiH7w", "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...", "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" "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": { "stats": {
"total": 9, "total": 9,
"by_domain": { "by_domain": {
"work": 7, "work": 7,
"health": 1, "health": 2,
"growth": 1, "growth": 1,
"sprijin": 0, "sprijin": 0,
"scout": 0 "scout": 0

View File

@@ -4,6 +4,7 @@
**Sursa:** Laura Laurențiu (adaptat) **Sursa:** Laura Laurențiu (adaptat)
**Timp preparare:** ~50 minute **Timp preparare:** ~50 minute
**Porții:** 6-8 **Porții:** 6-8
**Tags:** #ciorba #reteta #pleurotus #pui @health
--- ---