Update dashboard, memory, root (+7 ~14)

This commit is contained in:
Echo
2026-02-14 07:35:09 +00:00
parent d9f1c8c700
commit 91a566b34c
21 changed files with 1605 additions and 52 deletions

View File

@@ -28,6 +28,12 @@ KANBAN_DIR = BASE_DIR / 'dashboard'
WORKSPACE_DIR = Path('/home/moltbot/workspace')
HABITS_FILE = KANBAN_DIR / 'habits.json'
# Eco (echo-core) constants
ECO_SERVICES = ['echo-core', 'echo-whatsapp-bridge', 'echo-taskboard']
ECHO_CORE_DIR = Path('/home/moltbot/echo-core')
ECHO_LOG_FILE = ECHO_CORE_DIR / 'logs' / 'echo-core.log'
ECHO_SESSIONS_FILE = ECHO_CORE_DIR / 'sessions' / 'active.json'
# Load .env file if present
_env_file = Path(__file__).parent / '.env'
if _env_file.exists():
@@ -70,6 +76,12 @@ class TaskBoardHandler(SimpleHTTPRequestHandler):
self.handle_workspace_git_push()
elif self.path == '/api/workspace/delete':
self.handle_workspace_delete()
elif self.path == '/api/eco/restart':
self.handle_eco_restart()
elif self.path == '/api/eco/stop':
self.handle_eco_stop()
elif self.path == '/api/eco/sessions/clear':
self.handle_eco_sessions_clear()
else:
self.send_error(404)
@@ -289,6 +301,12 @@ class TaskBoardHandler(SimpleHTTPRequestHandler):
self.handle_workspace_git_diff()
elif self.path.startswith('/api/workspace/logs'):
self.handle_workspace_logs()
elif self.path == '/api/eco/status' or self.path.startswith('/api/eco/status?'):
self.handle_eco_status()
elif self.path.startswith('/api/eco/logs'):
self.handle_eco_logs()
elif self.path == '/api/eco/doctor':
self.handle_eco_doctor()
elif self.path.startswith('/api/'):
self.send_error(404)
else:
@@ -1949,6 +1967,269 @@ class TaskBoardHandler(SimpleHTTPRequestHandler):
except Exception as e:
self.send_json({'error': str(e)}, 500)
# ── Eco (echo-core) handlers ──────────────────────────────────────
def handle_eco_status(self):
"""Get status of echo-core services + active sessions."""
try:
services = []
for svc in ECO_SERVICES:
info = {'name': svc, 'active': False, 'pid': None, 'uptime': None, 'memory': None}
result = subprocess.run(
['systemctl', '--user', 'is-active', svc],
capture_output=True, text=True, timeout=5
)
info['active'] = result.stdout.strip() == 'active'
if info['active']:
# PID
result = subprocess.run(
['systemctl', '--user', 'show', '-p', 'MainPID', '--value', svc],
capture_output=True, text=True, timeout=5
)
pid = result.stdout.strip()
if pid and pid != '0':
info['pid'] = int(pid)
# Uptime via systemctl timestamp
try:
r = subprocess.run(
['systemctl', '--user', 'show', '-p', 'ActiveEnterTimestamp', '--value', svc],
capture_output=True, text=True, timeout=5
)
ts = r.stdout.strip()
if ts:
start = datetime.strptime(ts, '%a %Y-%m-%d %H:%M:%S %Z')
info['uptime'] = int((datetime.utcnow() - start).total_seconds())
except Exception:
pass
# Memory (VmRSS from /proc)
try:
for line in Path(f'/proc/{pid}/status').read_text().splitlines():
if line.startswith('VmRSS:'):
info['memory'] = line.split(':')[1].strip()
break
except Exception:
pass
services.append(info)
# Active sessions
sessions = []
if ECHO_SESSIONS_FILE.exists():
try:
sessions = json.loads(ECHO_SESSIONS_FILE.read_text())
except Exception:
pass
self.send_json({'services': services, 'sessions': sessions})
except Exception as e:
self.send_json({'error': str(e)}, 500)
def handle_eco_restart(self):
"""Restart an echo-core service (not taskboard)."""
try:
data = self._read_post_json()
svc = data.get('service', '')
if svc not in ECO_SERVICES:
self.send_json({'success': False, 'error': f'Unknown service: {svc}'}, 400)
return
if svc == 'echo-taskboard':
self.send_json({'success': False, 'error': 'Cannot restart taskboard from itself'}, 400)
return
result = subprocess.run(
['systemctl', '--user', 'restart', svc],
capture_output=True, text=True, timeout=30
)
if result.returncode == 0:
self.send_json({'success': True, 'message': f'{svc} restarted'})
else:
self.send_json({'success': False, 'error': result.stderr.strip()}, 500)
except Exception as e:
self.send_json({'success': False, 'error': str(e)}, 500)
def handle_eco_stop(self):
"""Stop an echo-core service (not taskboard)."""
try:
data = self._read_post_json()
svc = data.get('service', '')
if svc not in ECO_SERVICES:
self.send_json({'success': False, 'error': f'Unknown service: {svc}'}, 400)
return
if svc == 'echo-taskboard':
self.send_json({'success': False, 'error': 'Cannot stop taskboard from itself'}, 400)
return
result = subprocess.run(
['systemctl', '--user', 'stop', svc],
capture_output=True, text=True, timeout=30
)
if result.returncode == 0:
self.send_json({'success': True, 'message': f'{svc} stopped'})
else:
self.send_json({'success': False, 'error': result.stderr.strip()}, 500)
except Exception as e:
self.send_json({'success': False, 'error': str(e)}, 500)
def handle_eco_logs(self):
"""Return last N lines from echo-core.log."""
try:
params = parse_qs(urlparse(self.path).query)
lines = min(int(params.get('lines', ['100'])[0]), 500)
if not ECHO_LOG_FILE.exists():
self.send_json({'lines': ['(log file not found)']})
return
result = subprocess.run(
['tail', '-n', str(lines), str(ECHO_LOG_FILE)],
capture_output=True, text=True, timeout=10
)
self.send_json({'lines': result.stdout.splitlines()})
except Exception as e:
self.send_json({'error': str(e)}, 500)
def handle_eco_doctor(self):
"""Run health checks on echo-core ecosystem."""
checks = []
# 1. Services
for svc in ECO_SERVICES:
try:
r = subprocess.run(
['systemctl', '--user', 'is-active', svc],
capture_output=True, text=True, timeout=5
)
active = r.stdout.strip() == 'active'
checks.append({
'name': f'Service: {svc}',
'pass': active,
'detail': 'active' if active else r.stdout.strip()
})
except Exception as e:
checks.append({'name': f'Service: {svc}', 'pass': False, 'detail': str(e)})
# 2. Disk space
try:
st = shutil.disk_usage('/')
pct_free = (st.free / st.total) * 100
checks.append({
'name': 'Disk space',
'pass': pct_free > 5,
'detail': f'{pct_free:.1f}% free ({st.free // (1024**3)} GB)'
})
except Exception as e:
checks.append({'name': 'Disk space', 'pass': False, 'detail': str(e)})
# 3. Log file
try:
if ECHO_LOG_FILE.exists():
size = ECHO_LOG_FILE.stat().st_size
size_mb = size / (1024 * 1024)
checks.append({
'name': 'Log file',
'pass': size_mb < 100,
'detail': f'{size_mb:.1f} MB'
})
else:
checks.append({'name': 'Log file', 'pass': False, 'detail': 'Not found'})
except Exception as e:
checks.append({'name': 'Log file', 'pass': False, 'detail': str(e)})
# 4. Sessions file
try:
if ECHO_SESSIONS_FILE.exists():
data = json.loads(ECHO_SESSIONS_FILE.read_text())
count = len(data) if isinstance(data, list) else len(data.keys()) if isinstance(data, dict) else 0
checks.append({'name': 'Sessions file', 'pass': True, 'detail': f'{count} active'})
else:
checks.append({'name': 'Sessions file', 'pass': False, 'detail': 'Not found'})
except Exception as e:
checks.append({'name': 'Sessions file', 'pass': False, 'detail': str(e)})
# 5. Config
config_file = ECHO_CORE_DIR / 'config.json'
try:
if config_file.exists():
json.loads(config_file.read_text())
checks.append({'name': 'Config', 'pass': True, 'detail': 'Valid JSON'})
else:
checks.append({'name': 'Config', 'pass': False, 'detail': 'Not found'})
except Exception as e:
checks.append({'name': 'Config', 'pass': False, 'detail': str(e)})
# 6. WhatsApp bridge log
wa_log = ECHO_CORE_DIR / 'logs' / 'whatsapp-bridge.log'
try:
if wa_log.exists():
# Check last line for errors
r = subprocess.run(
['tail', '-1', str(wa_log)],
capture_output=True, text=True, timeout=5
)
last = r.stdout.strip()
has_error = 'error' in last.lower() or 'fatal' in last.lower()
checks.append({
'name': 'WhatsApp bridge log',
'pass': not has_error,
'detail': last[:80] if last else 'Empty'
})
else:
checks.append({'name': 'WhatsApp bridge log', 'pass': False, 'detail': 'Not found'})
except Exception as e:
checks.append({'name': 'WhatsApp bridge log', 'pass': False, 'detail': str(e)})
# 7. Claude CLI
try:
r = subprocess.run(
['which', 'claude'],
capture_output=True, text=True, timeout=5
)
found = r.returncode == 0
checks.append({
'name': 'Claude CLI',
'pass': found,
'detail': r.stdout.strip() if found else 'Not in PATH'
})
except Exception as e:
checks.append({'name': 'Claude CLI', 'pass': False, 'detail': str(e)})
self.send_json({'checks': checks})
def handle_eco_sessions_clear(self):
"""Clear active sessions (all or specific channel)."""
try:
data = self._read_post_json()
channel = data.get('channel', None)
if not ECHO_SESSIONS_FILE.exists():
self.send_json({'success': True, 'message': 'No sessions file'})
return
if channel:
# Remove specific channel
sessions = json.loads(ECHO_SESSIONS_FILE.read_text())
if isinstance(sessions, list):
sessions = [s for s in sessions if s.get('channel') != channel]
elif isinstance(sessions, dict):
sessions.pop(channel, None)
ECHO_SESSIONS_FILE.write_text(json.dumps(sessions, indent=2))
self.send_json({'success': True, 'message': f'Cleared session: {channel}'})
else:
# Clear all
if isinstance(json.loads(ECHO_SESSIONS_FILE.read_text()), list):
ECHO_SESSIONS_FILE.write_text('[]')
else:
ECHO_SESSIONS_FILE.write_text('{}')
self.send_json({'success': True, 'message': 'All sessions cleared'})
except Exception as e:
self.send_json({'success': False, 'error': str(e)}, 500)
def send_json(self, data, code=200):
self.send_response(code)
self.send_header('Content-Type', 'application/json')