Update agents, dashboard, kb +2 more (+14 ~20 -3)

This commit is contained in:
Echo
2026-01-31 13:36:24 +00:00
parent a44b9ef852
commit 6555ea28ee
34 changed files with 1919 additions and 225 deletions

View File

@@ -102,6 +102,10 @@ class TaskBoardHandler(SimpleHTTPRequestHandler):
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.startswith('/api/files'):
self.handle_files_get()
elif self.path.startswith('/api/'):
@@ -215,6 +219,215 @@ class TaskBoardHandler(SimpleHTTPRequestHandler):
except Exception as e:
self.send_json({'error': str(e)}, 500)
def handle_cron_status(self):
"""Get cron jobs status from ~/.clawdbot/cron/jobs.json"""
try:
jobs_file = Path.home() / '.clawdbot' / 'cron' / 'jobs.json'
if not jobs_file.exists():
self.send_json({'jobs': [], 'error': 'No jobs file found'})
return
data = json.loads(jobs_file.read_text())
all_jobs = data.get('jobs', [])
# Filter enabled jobs and format for dashboard
now_ms = datetime.now().timestamp() * 1000
today_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
today_start_ms = today_start.timestamp() * 1000
jobs = []
for job in all_jobs:
if not job.get('enabled', False):
continue
# Parse cron expression to get time
schedule = job.get('schedule', {})
expr = schedule.get('expr', '')
# Simple cron parsing for display - convert UTC to Bucharest
parts = expr.split()
if len(parts) >= 2:
minute = parts[0]
hour = parts[1]
if minute.isdigit() and (hour.isdigit() or '-' in hour):
# Handle hour ranges like "7-17"
if '-' in hour:
hour_start, hour_end = hour.split('-')
hour = hour_start # Show first hour
# Convert UTC to Bucharest (UTC+2 winter, UTC+3 summer)
from datetime import timezone as dt_timezone
from zoneinfo import ZoneInfo
try:
bucharest = ZoneInfo('Europe/Bucharest')
utc_hour = int(hour)
utc_minute = int(minute)
# Create UTC datetime for today
utc_dt = datetime.now(dt_timezone.utc).replace(hour=utc_hour, minute=utc_minute, second=0, microsecond=0)
local_dt = utc_dt.astimezone(bucharest)
time_str = f"{local_dt.hour:02d}:{local_dt.minute:02d}"
except:
time_str = f"{int(hour):02d}:{int(minute):02d}"
else:
time_str = expr[:15]
else:
time_str = expr[:15]
# Check if ran today
state = job.get('state', {})
last_run = state.get('lastRunAtMs', 0)
ran_today = last_run >= today_start_ms
last_status = state.get('lastStatus', 'unknown')
jobs.append({
'id': job.get('id'),
'name': job.get('name'),
'agentId': job.get('agentId'),
'time': time_str,
'schedule': expr,
'ranToday': ran_today,
'lastStatus': last_status if ran_today else None,
'lastRunAtMs': last_run,
'nextRunAtMs': state.get('nextRunAtMs')
})
# Sort by time
jobs.sort(key=lambda j: j['time'])
self.send_json({
'jobs': jobs,
'total': len(jobs),
'ranToday': sum(1 for j in jobs if j['ranToday'])
})
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."""
from datetime import timezone as dt_timezone
from zoneinfo import ZoneInfo
try:
activities = []
bucharest = ZoneInfo('Europe/Bucharest')
workspace = Path('/home/moltbot/clawd')
# 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:
pass
# 2. Git commits (last 24h)
try:
result = subprocess.run(
['git', 'log', '--oneline', '--since=24 hours ago', '--format=%H|%s|%at'],
cwd=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
})
except:
pass
# 3. Recent files in 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:
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:
pass
except:
pass
# Sort by timestamp descending
activities.sort(key=lambda x: x.get('timestamp', 0), reverse=True)
# Limit to 30 items
activities = activities[:30]
self.send_json({
'activities': activities,
'total': len(activities)
})
except Exception as e:
self.send_json({'error': str(e)}, 500)
def handle_files_get(self):
"""List files or get file content."""
from urllib.parse import urlparse, parse_qs
@@ -259,12 +472,12 @@ class TaskBoardHandler(SimpleHTTPRequestHandler):
items = []
try:
for item in sorted(target.iterdir()):
if item.name.startswith('.'):
continue
stat = item.stat()
items.append({
'name': item.name,
'type': 'dir' if item.is_dir() else 'file',
'size': item.stat().st_size if item.is_file() else None,
'size': stat.st_size if item.is_file() else None,
'mtime': stat.st_mtime,
'path': str(item.relative_to(workspace))
})
self.send_json({
@@ -314,6 +527,9 @@ class TaskBoardHandler(SimpleHTTPRequestHandler):
self.send_response(code)
self.send_header('Content-Type', 'application/json')
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
self.send_header('Pragma', 'no-cache')
self.send_header('Expires', '0')
self.end_headers()
self.wfile.write(json.dumps(data).encode())