Files
clawd/dashboard/eco.html
2026-02-14 07:35:09 +00:00

822 lines
28 KiB
HTML

<!DOCTYPE html>
<html lang="ro">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/svg+xml" href="/echo/favicon.svg">
<title>Echo · Eco</title>
<link rel="stylesheet" href="/echo/common.css">
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
<script src="/echo/swipe-nav.js"></script>
<style>
.main {
max-width: 1200px;
margin: 0 auto;
padding: var(--space-5);
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-5);
}
.page-title {
font-size: var(--text-xl);
font-weight: 600;
color: var(--text-primary);
}
.page-subtitle {
font-size: var(--text-sm);
color: var(--text-muted);
margin-top: var(--space-1);
}
/* Section */
.section {
margin-bottom: var(--space-5);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-3);
}
.section-title {
font-size: var(--text-lg);
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
gap: var(--space-2);
}
/* Service cards */
.services-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: var(--space-4);
}
.service-card {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: var(--space-4);
transition: border-color var(--transition-base);
}
.service-card:hover {
border-color: var(--border-focus);
}
.service-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--space-3);
}
.service-name {
font-size: var(--text-base);
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
gap: var(--space-2);
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--text-muted);
flex-shrink: 0;
}
.status-dot.active {
background: var(--success);
box-shadow: 0 0 6px rgba(34, 197, 94, 0.5);
}
.status-dot.inactive {
background: var(--error);
}
.service-meta {
display: flex;
flex-direction: column;
gap: var(--space-1);
font-size: var(--text-xs);
color: var(--text-muted);
margin-bottom: var(--space-3);
}
.service-meta span {
display: flex;
align-items: center;
gap: var(--space-1);
}
.service-actions {
display: flex;
gap: var(--space-2);
}
.service-actions .btn {
font-size: var(--text-xs);
padding: var(--space-1) var(--space-3);
}
/* Sessions table */
.sessions-table {
width: 100%;
border-collapse: collapse;
font-size: var(--text-sm);
}
.sessions-table th,
.sessions-table td {
padding: var(--space-2) var(--space-3);
text-align: left;
border-bottom: 1px solid var(--border);
}
.sessions-table th {
color: var(--text-muted);
font-weight: 500;
font-size: var(--text-xs);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.sessions-table td {
color: var(--text-secondary);
}
.sessions-empty {
text-align: center;
padding: var(--space-5);
color: var(--text-muted);
font-size: var(--text-sm);
}
.sessions-card {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
overflow: hidden;
}
/* Log viewer */
.log-card {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
overflow: hidden;
}
.log-toolbar {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--border);
flex-wrap: wrap;
}
.log-toolbar label {
font-size: var(--text-xs);
color: var(--text-muted);
}
.log-toolbar select {
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text-primary);
font-size: var(--text-xs);
padding: var(--space-1) var(--space-2);
}
.log-content {
font-family: var(--font-mono);
font-size: 11px;
line-height: 1.6;
color: var(--text-secondary);
white-space: pre-wrap;
word-break: break-all;
padding: var(--space-4);
max-height: 400px;
overflow-y: auto;
background: var(--bg-elevated);
}
/* Auto-refresh toggle */
.toggle-switch {
position: relative;
display: inline-block;
width: 36px;
height: 20px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0; left: 0; right: 0; bottom: 0;
background: var(--bg-surface-active);
border-radius: 20px;
transition: var(--transition-fast);
}
.toggle-slider:before {
content: "";
position: absolute;
height: 14px;
width: 14px;
left: 3px;
bottom: 3px;
background: var(--text-muted);
border-radius: 50%;
transition: var(--transition-fast);
}
.toggle-switch input:checked + .toggle-slider {
background: var(--accent);
}
.toggle-switch input:checked + .toggle-slider:before {
transform: translateX(16px);
background: white;
}
/* Doctor checks */
.doctor-card {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
overflow: hidden;
}
.check-list {
list-style: none;
padding: 0;
margin: 0;
}
.check-item {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--border);
font-size: var(--text-sm);
}
.check-item:last-child {
border-bottom: none;
}
.check-icon {
flex-shrink: 0;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
}
.check-icon.pass { color: var(--success); }
.check-icon.fail { color: var(--error); }
.check-name {
font-weight: 500;
color: var(--text-primary);
min-width: 160px;
}
.check-detail {
color: var(--text-muted);
font-size: var(--text-xs);
}
/* Spinner */
.spinner {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid var(--text-muted);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Button variants */
.btn-danger {
border-color: var(--error);
color: var(--error);
}
.btn-danger:hover {
background: var(--error);
color: white;
}
.btn-warning {
border-color: var(--warning);
color: var(--warning);
}
.btn-warning:hover {
background: var(--warning);
color: #000;
}
/* Responsive */
@media (max-width: 768px) {
.services-grid {
grid-template-columns: 1fr;
}
.service-header {
flex-direction: column;
align-items: flex-start;
gap: var(--space-2);
}
.check-item {
flex-wrap: wrap;
}
.check-name {
min-width: auto;
}
.sessions-table {
font-size: var(--text-xs);
}
.sessions-table th:nth-child(3),
.sessions-table td:nth-child(3) {
display: none;
}
}
</style>
</head>
<body>
<header class="header">
<a href="/echo/index.html" class="logo">
<i data-lucide="circle-dot"></i>
Echo
</a>
<nav class="nav">
<a href="/echo/index.html" class="nav-item">
<i data-lucide="layout-dashboard"></i>
<span>Dashboard</span>
</a>
<a href="/echo/workspace.html" class="nav-item">
<i data-lucide="code"></i>
<span>Workspace</span>
</a>
<a href="/echo/notes.html" class="nav-item">
<i data-lucide="file-text"></i>
<span>KB</span>
</a>
<a href="/echo/habits.html" class="nav-item">
<i data-lucide="dumbbell"></i>
<span>Habits</span>
</a>
<a href="/echo/files.html" class="nav-item">
<i data-lucide="folder"></i>
<span>Files</span>
</a>
<a href="/echo/eco.html" class="nav-item active">
<i data-lucide="cpu"></i>
<span>Eco</span>
</a>
<button class="theme-toggle" onclick="toggleTheme()" title="Schimba tema">
<i data-lucide="sun" id="themeIcon"></i>
</button>
</nav>
</header>
<main class="main">
<div class="page-header">
<div>
<h1 class="page-title">Eco Control Panel</h1>
<div class="page-subtitle" id="statusSummary">Loading...</div>
</div>
<button class="btn btn-secondary" onclick="refreshAll()">
<i data-lucide="refresh-cw"></i>
Refresh
</button>
</div>
<!-- Services -->
<div class="section">
<div class="section-header">
<h2 class="section-title">
<i data-lucide="server" style="width:18px;height:18px;"></i>
Services
</h2>
</div>
<div class="services-grid" id="servicesGrid">
<div style="color:var(--text-muted);">Loading...</div>
</div>
</div>
<!-- Sessions -->
<div class="section">
<div class="section-header">
<h2 class="section-title">
<i data-lucide="message-square" style="width:18px;height:18px;"></i>
Sessions
</h2>
<button class="btn btn-secondary btn-danger" onclick="clearAllSessions()" id="clearAllBtn" style="display:none;">
<i data-lucide="trash-2"></i>
Clear All
</button>
</div>
<div class="sessions-card">
<div id="sessionsContent">
<div class="sessions-empty">Loading...</div>
</div>
</div>
</div>
<!-- Logs -->
<div class="section">
<div class="section-header">
<h2 class="section-title">
<i data-lucide="scroll-text" style="width:18px;height:18px;"></i>
Logs
</h2>
</div>
<div class="log-card">
<div class="log-toolbar">
<label>Lines:</label>
<select id="logLines" onchange="loadLogs()">
<option value="50">50</option>
<option value="100" selected>100</option>
<option value="200">200</option>
</select>
<button class="btn btn-secondary" onclick="loadLogs()" style="font-size:var(--text-xs);padding:var(--space-1) var(--space-2);">
<i data-lucide="refresh-cw"></i>
Refresh
</button>
<div style="margin-left:auto;display:flex;align-items:center;gap:var(--space-2);">
<label style="font-size:var(--text-xs);color:var(--text-muted);">Auto</label>
<label class="toggle-switch">
<input type="checkbox" id="autoRefreshLogs" onchange="toggleAutoRefreshLogs()">
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="log-content" id="logContent">Loading...</div>
</div>
</div>
<!-- Doctor -->
<div class="section">
<div class="section-header">
<h2 class="section-title">
<i data-lucide="stethoscope" style="width:18px;height:18px;"></i>
Doctor
</h2>
<button class="btn btn-primary" onclick="runDoctor()" id="doctorBtn">
<i data-lucide="play"></i>
Run Doctor
</button>
</div>
<div class="doctor-card" id="doctorCard" style="display:none;">
<ul class="check-list" id="doctorChecks"></ul>
</div>
</div>
</main>
<script>
let statusRefreshInterval = null;
let logRefreshInterval = null;
// Theme
function toggleTheme() {
const current = document.documentElement.getAttribute('data-theme');
const next = current === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
}
(function() {
const saved = localStorage.getItem('theme');
if (saved) document.documentElement.setAttribute('data-theme', saved);
})();
function formatUptime(seconds) {
if (!seconds && seconds !== 0) return '-';
const d = Math.floor(seconds / 86400);
const h = Math.floor((seconds % 86400) / 3600);
const m = Math.floor((seconds % 3600) / 60);
if (d > 0) return `${d}d ${h}h`;
if (h > 0) return `${h}h ${m}m`;
return `${m}m`;
}
function svcLabel(name) {
return name.replace('echo-', '').replace('-', ' ');
}
// ── Status ──────────────────────────────────────────────
async function loadStatus() {
try {
const res = await fetch('/echo/api/eco/status');
const data = await res.json();
if (data.error) {
document.getElementById('servicesGrid').innerHTML =
`<div style="color:var(--error);">${data.error}</div>`;
return;
}
renderServices(data.services);
renderSessions(data.sessions);
const active = data.services.filter(s => s.active).length;
document.getElementById('statusSummary').textContent =
`${active}/${data.services.length} services active`;
} catch (e) {
document.getElementById('servicesGrid').innerHTML =
`<div style="color:var(--error);">Error: ${e.message}</div>`;
}
}
function renderServices(services) {
const grid = document.getElementById('servicesGrid');
grid.innerHTML = services.map(svc => {
const isTaskboard = svc.name === 'echo-taskboard';
const canControl = !isTaskboard;
let actionsHtml = '';
if (canControl) {
if (svc.active) {
actionsHtml = `
<button class="btn btn-secondary" onclick="restartService('${svc.name}')">
<i data-lucide="rotate-cw"></i> Restart
</button>
<button class="btn btn-secondary btn-danger" onclick="stopService('${svc.name}')">
<i data-lucide="square"></i> Stop
</button>
`;
} else {
actionsHtml = `
<button class="btn btn-primary" onclick="restartService('${svc.name}')">
<i data-lucide="play"></i> Start
</button>
`;
}
}
return `
<div class="service-card">
<div class="service-header">
<div class="service-name">
<span class="status-dot ${svc.active ? 'active' : 'inactive'}"></span>
${svcLabel(svc.name)}
</div>
<span style="font-size:var(--text-xs);color:${svc.active ? 'var(--success)' : 'var(--error)'};">
${svc.active ? 'running' : 'stopped'}
</span>
</div>
<div class="service-meta">
<span><i data-lucide="hash" style="width:12px;height:12px;"></i> PID: ${svc.pid || '-'}</span>
<span><i data-lucide="clock" style="width:12px;height:12px;"></i> Uptime: ${formatUptime(svc.uptime)}</span>
<span><i data-lucide="memory-stick" style="width:12px;height:12px;"></i> Memory: ${svc.memory || '-'}</span>
</div>
<div class="service-actions">
${actionsHtml}
</div>
</div>
`;
}).join('');
lucide.createIcons();
}
async function restartService(name) {
try {
const res = await fetch('/echo/api/eco/restart', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({service: name})
});
const data = await res.json();
if (data.success) {
setTimeout(loadStatus, 1500);
} else {
alert('Error: ' + data.error);
}
} catch (e) {
alert('Failed: ' + e.message);
}
}
async function stopService(name) {
if (!confirm(`Stop ${svcLabel(name)}?`)) return;
try {
const res = await fetch('/echo/api/eco/stop', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({service: name})
});
const data = await res.json();
if (data.success) {
setTimeout(loadStatus, 1000);
} else {
alert('Error: ' + data.error);
}
} catch (e) {
alert('Failed: ' + e.message);
}
}
// ── Sessions ────────────────────────────────────────────
function renderSessions(sessions) {
const container = document.getElementById('sessionsContent');
const clearBtn = document.getElementById('clearAllBtn');
if (!sessions || sessions.length === 0) {
container.innerHTML = '<div class="sessions-empty">No active sessions</div>';
clearBtn.style.display = 'none';
return;
}
clearBtn.style.display = '';
// Handle both array and object formats
let rows = [];
if (Array.isArray(sessions)) {
rows = sessions;
} else if (typeof sessions === 'object') {
rows = Object.entries(sessions).map(([k, v]) => ({
channel: k,
...(typeof v === 'object' ? v : {value: v})
}));
}
if (rows.length === 0) {
container.innerHTML = '<div class="sessions-empty">No active sessions</div>';
clearBtn.style.display = 'none';
return;
}
container.innerHTML = `
<table class="sessions-table">
<thead>
<tr>
<th>Channel</th>
<th>Platform</th>
<th>Started</th>
<th></th>
</tr>
</thead>
<tbody>
${rows.map(s => `
<tr>
<td>${escapeHtml(s.channel || s.id || '-')}</td>
<td>${escapeHtml(s.platform || s.type || '-')}</td>
<td>${s.started || s.timestamp || '-'}</td>
<td>
<button class="btn btn-secondary btn-danger" style="font-size:var(--text-xs);padding:2px 8px;"
onclick="clearSession('${escapeHtml(s.channel || s.id || '')}')">
<i data-lucide="x"></i>
</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
lucide.createIcons();
}
async function clearSession(channel) {
try {
const res = await fetch('/echo/api/eco/sessions/clear', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({channel: channel})
});
const data = await res.json();
if (data.success) loadStatus();
else alert('Error: ' + data.error);
} catch (e) {
alert('Failed: ' + e.message);
}
}
async function clearAllSessions() {
if (!confirm('Clear all active sessions?')) return;
try {
const res = await fetch('/echo/api/eco/sessions/clear', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({})
});
const data = await res.json();
if (data.success) loadStatus();
else alert('Error: ' + data.error);
} catch (e) {
alert('Failed: ' + e.message);
}
}
// ── Logs ────────────────────────────────────────────────
async function loadLogs() {
const lines = document.getElementById('logLines').value;
const content = document.getElementById('logContent');
try {
const res = await fetch(`/echo/api/eco/logs?lines=${lines}`);
const data = await res.json();
content.textContent = (data.lines || []).join('\n') || '(empty)';
content.scrollTop = content.scrollHeight;
} catch (e) {
content.textContent = 'Error: ' + e.message;
}
}
function toggleAutoRefreshLogs() {
const enabled = document.getElementById('autoRefreshLogs').checked;
if (logRefreshInterval) {
clearInterval(logRefreshInterval);
logRefreshInterval = null;
}
if (enabled) {
logRefreshInterval = setInterval(loadLogs, 5000);
}
}
// ── Doctor ──────────────────────────────────────────────
async function runDoctor() {
const card = document.getElementById('doctorCard');
const list = document.getElementById('doctorChecks');
const btn = document.getElementById('doctorBtn');
card.style.display = '';
list.innerHTML = '<li class="check-item"><span class="spinner"></span> Running checks...</li>';
btn.disabled = true;
try {
const res = await fetch('/echo/api/eco/doctor');
const data = await res.json();
list.innerHTML = data.checks.map(c => `
<li class="check-item">
<span class="check-icon ${c.pass ? 'pass' : 'fail'}">
${c.pass ? '&#10003;' : '&#10007;'}
</span>
<span class="check-name">${escapeHtml(c.name)}</span>
<span class="check-detail">${escapeHtml(c.detail)}</span>
</li>
`).join('');
} catch (e) {
list.innerHTML = `<li class="check-item" style="color:var(--error);">Error: ${escapeHtml(e.message)}</li>`;
}
btn.disabled = false;
}
// ── Helpers ─────────────────────────────────────────────
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function refreshAll() {
loadStatus();
loadLogs();
}
// ── Init ────────────────────────────────────────────────
loadStatus();
loadLogs();
lucide.createIcons();
// Auto-refresh status every 10s
statusRefreshInterval = setInterval(loadStatus, 10000);
</script>
</body>
</html>