Update agents, dashboard, kb +2 more (+14 ~20 -3)
This commit is contained in:
222
dashboard/api.py
222
dashboard/api.py
@@ -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())
|
||||
|
||||
|
||||
Reference in New Issue
Block a user