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:
@@ -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 <task-id>`
|
||||
|
||||
Când se execută orice job cron:
|
||||
1. **Start:** Creează task în kanban (Progress) cu numele job-ului
|
||||
2. **Rulează:** Execută task-ul
|
||||
|
||||
4
TOOLS.md
4
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`
|
||||
|
||||
@@ -996,16 +996,57 @@
|
||||
}
|
||||
|
||||
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();
|
||||
showToast('Activitate reîmprospătată');
|
||||
|
||||
@@ -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 @@
|
||||
</div>
|
||||
|
||||
<div class="tag-filter">
|
||||
<span class="tag-filter-label">Filtrează după tags:</span>
|
||||
<div class="tag-pills" id="tagPills"></div>
|
||||
<div class="tag-pills" id="mainPills"></div>
|
||||
<div class="tag-pills-more" id="tagPills"></div>
|
||||
</div>
|
||||
|
||||
<div id="notesContainer">
|
||||
@@ -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 }) => `
|
||||
<span class="tag-pill ${selectedTags.has(tag) ? 'active' : ''}"
|
||||
onclick="toggleTag('${tag}')">
|
||||
${tag} <span class="tag-pill-count">(${count})</span>
|
||||
</span>
|
||||
`).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 += `<button class="clear-filters" onclick="clearTagFilters()">✕ Clear</button>`;
|
||||
// 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 `<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
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
**Sursa:** Laura Laurențiu (adaptat)
|
||||
**Timp preparare:** ~50 minute
|
||||
**Porții:** 6-8
|
||||
**Tags:** #ciorba #reteta #pleurotus #pui @health
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user