feat(dashboard): drop /api/agents and /api/activity endpoints

This commit is contained in:
2026-04-21 07:16:20 +00:00
parent bee21594f5
commit e0abe5cdfc
3 changed files with 0 additions and 521 deletions

View File

@@ -34,7 +34,6 @@ from constants import ( # noqa: E402 re-exported for tests
VENV_PYTHON, VENV_PYTHON,
WORKSPACE_DIR, WORKSPACE_DIR,
) )
from handlers.agents import AgentsHandlers # noqa: E402
from handlers.cron import CronHandlers # noqa: E402 from handlers.cron import CronHandlers # noqa: E402
from handlers.eco import EcoHandlers # noqa: E402 from handlers.eco import EcoHandlers # noqa: E402
from handlers.files import FilesHandlers # noqa: E402 from handlers.files import FilesHandlers # noqa: E402
@@ -54,7 +53,6 @@ class TaskBoardHandler(
YoutubeHandlers, YoutubeHandlers,
WorkspaceHandlers, WorkspaceHandlers,
CronHandlers, CronHandlers,
AgentsHandlers,
SimpleHTTPRequestHandler, SimpleHTTPRequestHandler,
): ):
"""HTTP request handler — dispatches to handler-mixin methods.""" """HTTP request handler — dispatches to handler-mixin methods."""
@@ -90,12 +88,8 @@ class TaskBoardHandler(
self.send_json({'status': 'ok', 'time': _dt.now().isoformat()}) self.send_json({'status': 'ok', 'time': _dt.now().isoformat()})
elif self.path == '/api/git' or self.path.startswith('/api/git?'): elif self.path == '/api/git' or self.path.startswith('/api/git?'):
self.handle_git_status() self.handle_git_status()
elif self.path == '/api/agents' or self.path.startswith('/api/agents?'):
self.handle_agents_status()
elif self.path == '/api/cron' or self.path.startswith('/api/cron?'): elif self.path == '/api/cron' or self.path.startswith('/api/cron?'):
self.handle_cron_status() self.handle_cron_status()
elif self.path == '/api/activity' or self.path.startswith('/api/activity?'):
self.handle_activity()
elif self.path == '/api/habits': elif self.path == '/api/habits':
self.handle_habits_get() self.handle_habits_get()
elif self.path.startswith('/api/files'): elif self.path.startswith('/api/files'):

View File

@@ -1,203 +0,0 @@
"""LEGACY: /api/agents and /api/activity endpoints (clawdbot era).
These read from ~/.clawdbot/ and shell out to the `clawdbot` CLI.
Scheduled for removal once the post-decommission work completes.
"""
import json
import subprocess
from datetime import datetime, timezone as dt_timezone
from pathlib import Path
from zoneinfo import ZoneInfo
import constants
class AgentsHandlers:
"""Mixin providing /api/agents and /api/activity (deprecated)."""
def handle_agents_status(self):
"""Get agents status — reads session files from ~/.clawdbot/agents."""
try:
agents_config = [
{'id': 'echo', 'name': 'Echo', 'emoji': '🌀'},
{'id': 'echo-work', 'name': 'Work', 'emoji': ''},
{'id': 'echo-health', 'name': 'Health', 'emoji': '❤️'},
{'id': 'echo-growth', 'name': 'Growth', 'emoji': '🪜'},
{'id': 'echo-sprijin', 'name': 'Sprijin', 'emoji': ''},
{'id': 'echo-scout', 'name': 'Scout', 'emoji': '⚜️'},
]
active_agents = set()
sessions_base = Path.home() / '.clawdbot' / 'agents'
if sessions_base.exists():
for agent_dir in sessions_base.iterdir():
if agent_dir.is_dir():
sessions_file = agent_dir / 'sessions' / 'sessions.json'
if sessions_file.exists():
try:
data = json.loads(sessions_file.read_text())
now = datetime.now().timestamp() * 1000
for _key, sess in data.items():
if isinstance(sess, dict):
last_active = sess.get('updatedAt', 0)
if now - last_active < 30 * 60 * 1000:
active_agents.add(agent_dir.name)
break
except Exception:
pass
agents = [
{**cfg, 'active': cfg['id'] in active_agents}
for cfg in agents_config
]
self.send_json({'agents': agents})
except Exception as e:
self.send_json({'error': str(e)}, 500)
def handle_activity(self):
"""Aggregate activity from multiple sources: cron jobs, git commits, file changes."""
try:
activities = []
bucharest = ZoneInfo('Europe/Bucharest')
workspace = constants.GIT_WORKSPACE
# 1. Cron jobs ran today
try:
result = subprocess.run(
['clawdbot', 'cron', 'list', '--json'],
capture_output=True, text=True, timeout=10,
)
if result.returncode == 0:
cron_data = json.loads(result.stdout)
today_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
today_start_ms = today_start.timestamp() * 1000
for job in cron_data.get('jobs', []):
state = job.get('state', {})
last_run = state.get('lastRunAtMs', 0)
if last_run >= today_start_ms:
run_time = datetime.fromtimestamp(last_run / 1000, tz=dt_timezone.utc)
local_time = run_time.astimezone(bucharest)
activities.append({
'type': 'cron',
'icon': 'clock',
'text': f"Job: {job.get('name', 'unknown')}",
'agent': job.get('agentId', 'echo'),
'time': local_time.strftime('%H:%M'),
'timestamp': last_run,
'status': state.get('lastStatus', 'ok'),
})
except Exception:
pass
# 2. Git commits (last 24h)
try:
result = subprocess.run(
['git', 'log', '--oneline', '--since=24 hours ago', '--format=%H|%s|%at'],
cwd=str(workspace), capture_output=True, text=True, timeout=10,
)
if result.returncode == 0:
for line in result.stdout.strip().split('\n'):
if '|' in line:
parts = line.split('|')
if len(parts) >= 3:
commit_hash, message, timestamp = parts[0], parts[1], int(parts[2])
commit_time = datetime.fromtimestamp(timestamp, tz=dt_timezone.utc)
local_time = commit_time.astimezone(bucharest)
activities.append({
'type': 'git',
'icon': 'git-commit',
'text': message[:60] + ('...' if len(message) > 60 else ''),
'agent': 'git',
'time': local_time.strftime('%H:%M'),
'timestamp': timestamp * 1000,
'commitHash': commit_hash[:8],
})
except Exception:
pass
# 2b. Git uncommitted files
try:
result = subprocess.run(
['git', 'status', '--short'],
cwd=str(workspace), capture_output=True, text=True, timeout=10,
)
if result.returncode == 0 and result.stdout.strip():
for line in result.stdout.strip().split('\n'):
if len(line) >= 4:
status = line[:2]
filepath = line[2:].lstrip()
if not filepath:
continue
status_clean = status.strip()
status_labels = {'M': 'modificat', 'A': 'adăugat', 'D': 'șters', '??': 'nou', 'R': 'redenumit'}
status_label = status_labels.get(status_clean, status_clean)
activities.append({
'type': 'git-file',
'icon': 'file-diff',
'text': f"{filepath}",
'agent': f"git ({status_label})",
'time': 'acum',
'timestamp': int(datetime.now().timestamp() * 1000),
'path': filepath,
'gitStatus': status_clean,
})
except Exception:
pass
# 3. Recent files in memory/kb/ (last 24h)
try:
kb_dir = workspace / 'kb'
cutoff = datetime.now().timestamp() - (24 * 3600)
for md_file in kb_dir.rglob('*.md'):
stat = md_file.stat()
if stat.st_mtime > cutoff:
file_time = datetime.fromtimestamp(stat.st_mtime, tz=dt_timezone.utc)
local_time = file_time.astimezone(bucharest)
rel_path = md_file.relative_to(workspace)
activities.append({
'type': 'file',
'icon': 'file-text',
'text': f"Fișier: {md_file.name}",
'agent': str(rel_path.parent),
'time': local_time.strftime('%H:%M'),
'timestamp': int(stat.st_mtime * 1000),
'path': str(rel_path),
})
except Exception:
pass
# 4. Tasks from tasks.json
try:
tasks_file = workspace / 'dashboard' / 'tasks.json'
if tasks_file.exists():
tasks_data = json.loads(tasks_file.read_text())
for col in tasks_data.get('columns', []):
for task in col.get('tasks', []):
ts_str = task.get('completed') or task.get('created', '')
if ts_str:
try:
ts = datetime.fromisoformat(ts_str.replace('Z', '+00:00'))
if ts.timestamp() > (datetime.now().timestamp() - 7 * 24 * 3600):
local_time = ts.astimezone(bucharest)
activities.append({
'type': 'task',
'icon': 'check-circle' if task.get('completed') else 'circle',
'text': task.get('title', ''),
'agent': task.get('agent', 'Echo'),
'time': local_time.strftime('%d %b %H:%M'),
'timestamp': int(ts.timestamp() * 1000),
'status': 'done' if task.get('completed') else col['id'],
})
except Exception:
pass
except Exception:
pass
activities.sort(key=lambda x: x.get('timestamp', 0), reverse=True)
activities = activities[:30]
self.send_json({'activities': activities, 'total': len(activities)})
except Exception as e:
self.send_json({'error': str(e)}, 500)

View File

@@ -155,33 +155,6 @@
color: #818cf8; color: #818cf8;
} }
.status-section-icon.agents {
background: rgba(168, 85, 247, 0.15);
color: #a855f7;
}
.agents-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: var(--space-2);
}
.agent-chip {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
background: var(--bg-elevated);
border-radius: var(--radius-md);
font-size: var(--text-xs);
}
.agent-chip .emoji { font-size: 14px; }
.agent-chip .name { font-weight: 500; color: var(--text-primary); }
.agent-chip .status { color: var(--text-muted); }
.agent-chip.active { background: rgba(34, 197, 94, 0.1); border: 1px solid rgba(34, 197, 94, 0.3); }
.agent-chip.active .status { color: #22c55e; }
.status-section-info { .status-section-info {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
@@ -426,121 +399,6 @@
overflow-y: auto; overflow-y: auto;
} }
/* Activity panel */
.activity-panel .panel-header {
background: linear-gradient(135deg, rgba(99, 102, 241, 0.15), rgba(139, 92, 246, 0.1));
}
.activity-panel .panel-title svg {
color: #8b5cf6;
}
.activity-section {
margin-bottom: var(--space-4);
}
.activity-section-title {
font-size: var(--text-xs);
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: var(--space-2);
display: flex;
align-items: center;
gap: var(--space-2);
}
.activity-item {
display: flex;
align-items: flex-start;
gap: var(--space-3);
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-md);
margin-bottom: var(--space-1);
transition: background var(--transition-fast);
}
.activity-item:hover {
background: var(--bg-elevated);
}
.activity-icon {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
flex-shrink: 0;
}
.activity-icon.done, .activity-icon.task {
background: rgba(34, 197, 94, 0.2);
color: #22c55e;
}
.activity-icon.running {
background: rgba(59, 130, 246, 0.2);
color: #3b82f6;
}
.activity-icon.cron {
background: rgba(168, 85, 247, 0.2);
color: #a855f7;
}
.activity-icon.git {
background: rgba(249, 115, 22, 0.2);
color: #f97316;
}
.activity-icon.git-file {
background: rgba(234, 179, 8, 0.2);
color: #eab308;
}
.activity-icon.file {
background: rgba(20, 184, 166, 0.2);
color: #14b8a6;
}
.activity-icon svg {
width: 14px;
height: 14px;
}
.activity-type {
font-size: var(--text-xs);
padding: 2px 6px;
border-radius: 4px;
background: var(--bg-surface);
}
.activity-content {
flex: 1;
min-width: 0;
}
.activity-text {
font-size: var(--text-sm);
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.activity-meta {
font-size: var(--text-xs);
color: var(--text-muted);
display: flex;
gap: var(--space-2);
}
.activity-agent {
color: var(--accent);
}
.progress-bar { .progress-bar {
height: 4px; height: 4px;
background: var(--bg-elevated); background: var(--bg-elevated);
@@ -1231,29 +1089,6 @@
</div> </div>
</div> </div>
<!-- Activity Panel -->
<div class="panel activity-panel" id="activityPanel">
<div class="panel-header" onclick="toggleSection('activityPanel')">
<div class="panel-header-left">
<div class="panel-title">
<span>Activity</span>
</div>
<span class="panel-count" id="activityCount">0</span>
</div>
<div class="panel-header-right" onclick="event.stopPropagation()">
<button class="btn btn-icon" onclick="refreshActivity()" title="Refresh">
<i data-lucide="refresh-cw"></i>
</button>
<i data-lucide="chevron-down" class="panel-toggle" onclick="event.stopPropagation(); toggleSection('activityPanel')"></i>
</div>
</div>
<div class="panel-body" id="activityBody">
<div class="empty-state">
<i data-lucide="loader"></i>
<p>Se încarcă...</p>
</div>
</div>
</div>
</div> </div>
</main> </main>
@@ -1451,7 +1286,6 @@
// Data // Data
let issuesData = null; let issuesData = null;
let activityData = [];
let currentFilter = 'all'; let currentFilter = 'all';
let collapsedPriorities = new Set(['backlog', 'done']); let collapsedPriorities = new Set(['backlog', 'done']);
@@ -1728,42 +1562,6 @@
} }
} }
async function loadAgentsStatus() {
try {
const response = await fetch('/echo/api/agents?' + Date.now());
if (!response.ok) throw new Error('API error');
const data = await response.json();
const agents = data.agents || [];
const activeCount = agents.filter(a => a.active).length;
// Update badge
const badge = document.getElementById('agentsBadge');
badge.textContent = `${activeCount}/${agents.length}`;
badge.className = 'status-badge ' + (activeCount > 0 ? 'ok' : 'warning');
// Update subtitle
const subtitle = document.getElementById('agentsSubtitle');
const activeNames = agents.filter(a => a.active).map(a => a.name).join(', ');
subtitle.textContent = activeCount > 0 ? `Activi: ${activeNames}` : 'Niciun agent activ';
// Update grid
const grid = document.getElementById('agentsGrid');
grid.innerHTML = agents.map(agent => `
<div class="agent-chip ${agent.active ? 'active' : ''}">
<span class="emoji">${agent.emoji || '🤖'}</span>
<span class="name">${agent.name}</span>
<span class="status">${agent.active ? '●' : '○'}</span>
</div>
`).join('');
} catch (e) {
console.log('Agents status error:', e);
document.getElementById('agentsBadge').textContent = '-';
document.getElementById('agentsSubtitle').textContent = 'Nu se poate încărca';
}
}
function updateStatusSummary() { function updateStatusSummary() {
const gitBadge = document.getElementById('gitBadge'); const gitBadge = document.getElementById('gitBadge');
const anafBadge = document.getElementById('anafBadge'); const anafBadge = document.getElementById('anafBadge');
@@ -2063,115 +1861,6 @@
document.getElementById('issuesCount').textContent = todoCount; document.getElementById('issuesCount').textContent = todoCount;
} }
async function loadActivity() {
try {
// Fetch from unified activity API
const response = await fetch('/echo/api/activity?t=' + Date.now());
const data = await response.json();
if (data.error) throw new Error(data.error);
activityData = (data.activities || []).map(a => ({
type: a.type,
icon: a.icon || 'activity',
text: a.text,
agent: a.agent || 'Echo',
time: a.time,
timestamp: a.timestamp,
status: a.status,
path: a.path
}));
} 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ă');
}
function renderActivity() {
const body = document.getElementById('activityBody');
if (activityData.length === 0) {
body.innerHTML = `
<div class="empty-state">
<i data-lucide="inbox"></i>
<p>Nicio activitate recentă</p>
</div>
`;
lucide.createIcons();
return;
}
const today = new Date().toLocaleDateString('ro-RO', { day: 'numeric', month: 'short' });
// Group by type for better display
const typeLabels = {
'cron': '⏰ Cron Jobs',
'git': '📦 Git Commits',
'git-file': '🔸 Git Changes',
'file': '📄 Fișiere',
'task': '✅ Task-uri'
};
body.innerHTML = `
<div class="activity-section">
<div class="activity-section-title">
<i data-lucide="activity"></i>
Ultimele 24h
</div>
${activityData.map(item => {
let clickAttr = '';
if (item.type === 'git-file' && item.path) {
clickAttr = `onclick="window.open('files.html#${item.path}', '_blank')" style="cursor:pointer"`;
} else if (item.path) {
clickAttr = `onclick="window.open('files.html#${item.path}', '_blank')" style="cursor:pointer"`;
} else if (item.type === 'git' && item.commitHash) {
clickAttr = `onclick="window.open('https://gitea.romfast.ro/romfast/clawd/commit/${item.commitHash}', '_blank')" style="cursor:pointer"`;
}
return `
<div class="activity-item" ${clickAttr}>
<div class="activity-icon ${item.type}">
<i data-lucide="${item.icon || 'activity'}"></i>
</div>
<div class="activity-content">
<div class="activity-text">${item.type === 'git' && item.commitHash ? `<code style="font-size:10px;margin-right:4px">${item.commitHash}</code>` : ''}${item.text}</div>
<div class="activity-meta">
<span class="activity-type">${typeLabels[item.type] || item.type}</span>
<span class="activity-agent">${item.agent}</span>
<span>${item.time}</span>
</div>
</div>
</div>
`}).join('')}
</div>
`;
lucide.createIcons();
}
function renderIssues() { function renderIssues() {
const body = document.getElementById('issuesBody'); const body = document.getElementById('issuesBody');
@@ -2488,7 +2177,6 @@
loadStatus(); loadStatus();
loadIssues(); loadIssues();
loadTodos(); loadTodos();
loadActivity();
</script> </script>
</body> </body>
</html> </html>