feat(dashboard): drop /api/agents and /api/activity endpoints
This commit is contained in:
@@ -34,7 +34,6 @@ from constants import ( # noqa: E402 re-exported for tests
|
||||
VENV_PYTHON,
|
||||
WORKSPACE_DIR,
|
||||
)
|
||||
from handlers.agents import AgentsHandlers # noqa: E402
|
||||
from handlers.cron import CronHandlers # noqa: E402
|
||||
from handlers.eco import EcoHandlers # noqa: E402
|
||||
from handlers.files import FilesHandlers # noqa: E402
|
||||
@@ -54,7 +53,6 @@ class TaskBoardHandler(
|
||||
YoutubeHandlers,
|
||||
WorkspaceHandlers,
|
||||
CronHandlers,
|
||||
AgentsHandlers,
|
||||
SimpleHTTPRequestHandler,
|
||||
):
|
||||
"""HTTP request handler — dispatches to handler-mixin methods."""
|
||||
@@ -90,12 +88,8 @@ class TaskBoardHandler(
|
||||
self.send_json({'status': 'ok', 'time': _dt.now().isoformat()})
|
||||
elif self.path == '/api/git' or self.path.startswith('/api/git?'):
|
||||
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?'):
|
||||
self.handle_cron_status()
|
||||
elif self.path == '/api/activity' or self.path.startswith('/api/activity?'):
|
||||
self.handle_activity()
|
||||
elif self.path == '/api/habits':
|
||||
self.handle_habits_get()
|
||||
elif self.path.startswith('/api/files'):
|
||||
|
||||
@@ -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)
|
||||
@@ -155,33 +155,6 @@
|
||||
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 {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
@@ -426,121 +399,6 @@
|
||||
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 {
|
||||
height: 4px;
|
||||
background: var(--bg-elevated);
|
||||
@@ -1231,29 +1089,6 @@
|
||||
</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>
|
||||
</main>
|
||||
|
||||
@@ -1451,7 +1286,6 @@
|
||||
|
||||
// Data
|
||||
let issuesData = null;
|
||||
let activityData = [];
|
||||
let currentFilter = 'all';
|
||||
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() {
|
||||
const gitBadge = document.getElementById('gitBadge');
|
||||
const anafBadge = document.getElementById('anafBadge');
|
||||
@@ -2063,115 +1861,6 @@
|
||||
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() {
|
||||
const body = document.getElementById('issuesBody');
|
||||
|
||||
@@ -2488,7 +2177,6 @@
|
||||
loadStatus();
|
||||
loadIssues();
|
||||
loadTodos();
|
||||
loadActivity();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user