1253 lines
48 KiB
HTML
1253 lines
48 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 (collapsible) */
|
|
.section {
|
|
margin-bottom: var(--space-4);
|
|
}
|
|
|
|
.section-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: var(--space-3);
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
|
|
.section.collapsed .section-body { display: none; }
|
|
.section.collapsed .section-header { margin-bottom: 0; }
|
|
.section.collapsed .sec-chev { transform: rotate(180deg); }
|
|
|
|
.sec-chev {
|
|
width: 16px;
|
|
height: 16px;
|
|
color: var(--text-muted);
|
|
transition: transform var(--transition-fast);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.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 */
|
|
.sessions-empty {
|
|
text-align: center;
|
|
padding: var(--space-4);
|
|
color: var(--text-muted);
|
|
font-size: var(--text-xs);
|
|
}
|
|
|
|
.sessions-card {
|
|
background: var(--bg-surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-lg);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.session-item {
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.session-item:last-child { border-bottom: none; }
|
|
|
|
.session-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-2);
|
|
padding: 6px var(--space-3);
|
|
cursor: pointer;
|
|
transition: background var(--transition-fast);
|
|
user-select: none;
|
|
}
|
|
|
|
.session-row:hover { background: var(--bg-elevated); }
|
|
|
|
.p-icon {
|
|
flex-shrink: 0;
|
|
width: 18px;
|
|
height: 18px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.p-icon.discord { color: #5865F2; }
|
|
.p-icon.telegram { color: #26A5E4; }
|
|
.p-icon.whatsapp { color: #25D366; }
|
|
.p-icon.unknown { color: var(--text-muted); }
|
|
|
|
.s-name {
|
|
font-size: var(--text-xs);
|
|
font-weight: 500;
|
|
color: var(--text-primary);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.s-tag {
|
|
font-size: 9px;
|
|
font-weight: 600;
|
|
padding: 0 4px;
|
|
border-radius: 3px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.s-tag.grup { background: rgba(37, 211, 102, 0.12); color: #25D366; }
|
|
.s-tag.dm { background: rgba(156, 163, 175, 0.12); color: var(--text-muted); }
|
|
|
|
.s-stats {
|
|
flex: 1;
|
|
display: flex;
|
|
gap: var(--space-2);
|
|
font-size: 10px;
|
|
color: var(--text-muted);
|
|
justify-content: flex-end;
|
|
white-space: nowrap;
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
.s-time {
|
|
font-size: 10px;
|
|
color: var(--text-muted);
|
|
white-space: nowrap;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.s-chevron {
|
|
color: var(--text-muted);
|
|
transition: transform var(--transition-fast);
|
|
flex-shrink: 0;
|
|
width: 12px;
|
|
height: 12px;
|
|
}
|
|
|
|
.s-chevron.open { transform: rotate(180deg); }
|
|
|
|
.session-details {
|
|
display: none;
|
|
padding: 4px var(--space-3) var(--space-3);
|
|
padding-left: calc(18px + var(--space-2) + var(--space-3));
|
|
}
|
|
|
|
.session-details.open { display: block; }
|
|
|
|
.s-detail-row {
|
|
display: flex;
|
|
gap: var(--space-3);
|
|
font-size: 10px;
|
|
color: var(--text-muted);
|
|
margin-bottom: 6px;
|
|
font-family: var(--font-mono);
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.s-detail-row span { white-space: nowrap; }
|
|
|
|
/* Conversation messages */
|
|
.s-messages {
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
background: var(--bg-elevated);
|
|
font-size: 11px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.s-msg {
|
|
padding: 4px 8px;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.s-msg:last-child { border-bottom: none; }
|
|
.s-msg.user { color: var(--text-secondary); }
|
|
.s-msg.assistant { color: var(--accent); }
|
|
|
|
.s-msg-role {
|
|
font-size: 9px;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
margin-right: 6px;
|
|
}
|
|
|
|
.s-msg.user .s-msg-role { color: var(--warning); }
|
|
.s-msg.assistant .s-msg-role { color: var(--accent); }
|
|
|
|
.s-msg-text {
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.s-loading {
|
|
padding: 8px;
|
|
text-align: center;
|
|
color: var(--text-muted);
|
|
font-size: 10px;
|
|
}
|
|
|
|
.s-actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
margin-top: 6px;
|
|
}
|
|
|
|
.s-actions .btn {
|
|
font-size: 10px;
|
|
padding: 2px 8px;
|
|
}
|
|
|
|
/* 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);
|
|
}
|
|
|
|
/* Git status */
|
|
.git-card {
|
|
background: var(--bg-surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-lg);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.git-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-3);
|
|
padding: var(--space-3) var(--space-4);
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
|
|
.git-header:hover { background: var(--bg-elevated); }
|
|
|
|
.git-icon {
|
|
width: 32px;
|
|
height: 32px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: var(--radius-md);
|
|
background: rgba(249, 115, 22, 0.15);
|
|
color: #f97316;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.git-icon svg { width: 18px; height: 18px; }
|
|
|
|
.git-info { flex: 1; min-width: 0; }
|
|
|
|
.git-title {
|
|
font-size: var(--text-sm);
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-2);
|
|
}
|
|
|
|
.git-subtitle {
|
|
font-size: 13px;
|
|
color: var(--text-secondary);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.git-badge {
|
|
padding: 3px 10px;
|
|
border-radius: var(--radius-sm);
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.git-badge.ok { background: rgba(34, 197, 94, 0.15); color: #22c55e; }
|
|
.git-badge.warning { background: rgba(249, 115, 22, 0.15); color: #f97316; }
|
|
.git-badge.error { background: rgba(239, 68, 68, 0.15); color: #ef4444; }
|
|
|
|
.git-actions {
|
|
display: flex;
|
|
gap: var(--space-2);
|
|
margin-left: auto;
|
|
}
|
|
|
|
.git-toggle {
|
|
width: 16px;
|
|
height: 16px;
|
|
color: var(--text-muted);
|
|
transition: transform var(--transition-fast);
|
|
}
|
|
|
|
.git-card.collapsed .git-toggle { transform: rotate(-90deg); }
|
|
.git-card.collapsed .git-details { display: none; }
|
|
|
|
.git-details {
|
|
padding: 0 var(--space-4) var(--space-3);
|
|
padding-left: calc(var(--space-4) + 32px + var(--space-3));
|
|
}
|
|
|
|
.git-detail-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-2);
|
|
font-size: 13px;
|
|
color: var(--text-primary);
|
|
padding: 2px 0;
|
|
}
|
|
|
|
.git-detail-item svg { width: 14px; height: 14px; color: var(--text-secondary); }
|
|
.git-detail-item.uncommitted { color: #f97316; }
|
|
|
|
/* 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;
|
|
}
|
|
.s-stats { 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" id="sec-services">
|
|
<div class="section-header" onclick="toggleSec('sec-services')">
|
|
<h2 class="section-title">
|
|
<i data-lucide="server" style="width:18px;height:18px;"></i>
|
|
Services
|
|
</h2>
|
|
<svg class="s-chevron sec-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
|
</div>
|
|
<div class="section-body">
|
|
<div class="services-grid" id="servicesGrid">
|
|
<div style="color:var(--text-muted);">Loading...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Git -->
|
|
<div class="section" id="sec-git">
|
|
<div class="section-header" onclick="toggleSec('sec-git')">
|
|
<h2 class="section-title">
|
|
<i data-lucide="git-branch" style="width:18px;height:18px;"></i>
|
|
Git
|
|
</h2>
|
|
<svg class="s-chevron sec-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
|
</div>
|
|
<div class="section-body">
|
|
<div class="git-card" id="gitCard">
|
|
<div class="git-header" onclick="toggleGitDetails()">
|
|
<div class="git-icon">
|
|
<i data-lucide="git-branch"></i>
|
|
</div>
|
|
<div class="git-info">
|
|
<div class="git-title">
|
|
Git
|
|
<span class="git-badge ok" id="gitBadge">curat</span>
|
|
</div>
|
|
<div class="git-subtitle" id="gitSubtitle">Se incarca...</div>
|
|
</div>
|
|
<div class="git-actions" onclick="event.stopPropagation()">
|
|
<button class="btn btn-secondary" onclick="ecoGitCommit()" title="Commit & Push" style="font-size:var(--text-xs);padding:var(--space-1) var(--space-2);">
|
|
<i data-lucide="git-commit"></i> Commit
|
|
</button>
|
|
<button class="btn btn-secondary" onclick="loadGitStatus()" title="Refresh" style="font-size:var(--text-xs);padding:var(--space-1) var(--space-2);">
|
|
<i data-lucide="refresh-cw"></i>
|
|
</button>
|
|
</div>
|
|
<i data-lucide="chevron-down" class="git-toggle"></i>
|
|
</div>
|
|
<div class="git-details" id="gitDetails">
|
|
<!-- Populated by JS -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sessions -->
|
|
<div class="section" id="sec-sessions">
|
|
<div class="section-header" onclick="toggleSec('sec-sessions')">
|
|
<h2 class="section-title">
|
|
<i data-lucide="message-square" style="width:18px;height:18px;"></i>
|
|
Sessions
|
|
</h2>
|
|
<div style="display:flex;gap:var(--space-2);align-items:center;">
|
|
<button class="btn btn-secondary" onclick="event.stopPropagation();loadSessions()" style="font-size:var(--text-xs);padding:var(--space-1) var(--space-2);">
|
|
<i data-lucide="refresh-cw"></i>
|
|
</button>
|
|
<button class="btn btn-secondary btn-danger" onclick="event.stopPropagation();clearAllSessions()" id="clearAllBtn" style="display:none;font-size:var(--text-xs);padding:var(--space-1) var(--space-2);">
|
|
<i data-lucide="trash-2"></i> Clear All
|
|
</button>
|
|
<svg class="s-chevron sec-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
|
</div>
|
|
</div>
|
|
<div class="section-body">
|
|
<div class="sessions-card">
|
|
<div id="sessionsContent">
|
|
<div class="sessions-empty">Loading...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Logs -->
|
|
<div class="section collapsed" id="sec-logs">
|
|
<div class="section-header" onclick="toggleSec('sec-logs')">
|
|
<h2 class="section-title">
|
|
<i data-lucide="scroll-text" style="width:18px;height:18px;"></i>
|
|
Logs
|
|
</h2>
|
|
<svg class="s-chevron sec-chev open" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
|
</div>
|
|
<div class="section-body">
|
|
<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>
|
|
</div>
|
|
|
|
<!-- Doctor -->
|
|
<div class="section collapsed" id="sec-doctor">
|
|
<div class="section-header" onclick="toggleSec('sec-doctor')">
|
|
<h2 class="section-title">
|
|
<i data-lucide="stethoscope" style="width:18px;height:18px;"></i>
|
|
Doctor
|
|
</h2>
|
|
<div style="display:flex;gap:var(--space-2);align-items:center;">
|
|
<button class="btn btn-primary" onclick="event.stopPropagation();runDoctor()" id="doctorBtn" style="font-size:var(--text-xs);padding:var(--space-1) var(--space-2);">
|
|
<i data-lucide="play"></i> Run
|
|
</button>
|
|
<svg class="s-chevron sec-chev open" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
|
</div>
|
|
</div>
|
|
<div class="section-body">
|
|
<div class="doctor-card" id="doctorCard">
|
|
<ul class="check-list" id="doctorChecks">
|
|
<li class="check-item" style="color:var(--text-muted);justify-content:center;">Click Run to check</li>
|
|
</ul>
|
|
</div>
|
|
</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);
|
|
|
|
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 ────────────────────────────────────────────
|
|
|
|
const pIcon = {
|
|
discord: '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128c.12-.098.246-.198.373-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03z"/></svg>',
|
|
telegram: '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/></svg>',
|
|
whatsapp: '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 0 1-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 0 1-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 0 1 2.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0 0 12.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 0 0 5.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 0 0-3.48-8.413z"/></svg>',
|
|
unknown: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/></svg>'
|
|
};
|
|
|
|
function fmtTime(iso) {
|
|
if (!iso) return '-';
|
|
const d = new Date(iso), now = new Date();
|
|
const t = d.toLocaleTimeString('ro-RO', {hour:'2-digit', minute:'2-digit'});
|
|
if (d.toDateString() === now.toDateString()) return t;
|
|
const yesterday = new Date(now); yesterday.setDate(yesterday.getDate()-1);
|
|
if (d.toDateString() === yesterday.toDateString()) return 'ieri ' + t;
|
|
return d.toLocaleDateString('ro-RO', {day:'2-digit', month:'2-digit'}) + ' ' + t;
|
|
}
|
|
|
|
function fmtCost(u) { return u ? '$' + u.toFixed(3) : '-'; }
|
|
function fmtTok(n) { return !n ? '0' : n >= 1000 ? (n/1000).toFixed(1)+'k' : String(n); }
|
|
function fmtDur(ms) { if (!ms) return '-'; const s = Math.round(ms/1000); return s<60 ? s+'s' : Math.floor(s/60)+'m'+((s%60)?(' '+s%60+'s'):''); }
|
|
|
|
let sessionsCache = [];
|
|
|
|
async function loadSessions() {
|
|
try {
|
|
const res = await fetch('/echo/api/eco/sessions');
|
|
const data = await res.json();
|
|
sessionsCache = data.sessions || [];
|
|
renderSessions(sessionsCache);
|
|
} catch (e) {
|
|
document.getElementById('sessionsContent').innerHTML =
|
|
`<div class="sessions-empty" style="color:var(--error);">Error: ${e.message}</div>`;
|
|
}
|
|
}
|
|
|
|
function toggleSession(i) {
|
|
const det = document.getElementById('sd-'+i);
|
|
const chv = document.getElementById('sc-'+i);
|
|
if (!det) return;
|
|
const opening = !det.classList.contains('open');
|
|
det.classList.toggle('open');
|
|
if (chv) chv.classList.toggle('open');
|
|
// Load content on first open
|
|
if (opening && !det.dataset.loaded) {
|
|
const s = sessionsCache[i];
|
|
if (s && s.session_id) {
|
|
det.dataset.loaded = '1';
|
|
loadSessionContent(s.session_id, i);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function loadSessionContent(sessionId, idx) {
|
|
const el = document.getElementById('sm-'+idx);
|
|
if (!el) return;
|
|
el.innerHTML = '<div class="s-loading"><span class="spinner"></span></div>';
|
|
try {
|
|
const res = await fetch(`/echo/api/eco/sessions/content?id=${sessionId}`);
|
|
const data = await res.json();
|
|
if (!data.messages || data.messages.length === 0) {
|
|
el.innerHTML = '<div class="s-loading">No messages</div>';
|
|
return;
|
|
}
|
|
el.innerHTML = data.messages.map(m =>
|
|
`<div class="s-msg ${m.role}"><span class="s-msg-role">${m.role === 'user' ? 'USR' : 'ECH'}</span><span class="s-msg-text">${escapeHtml(m.text)}</span></div>`
|
|
).join('');
|
|
el.scrollTop = el.scrollHeight;
|
|
} catch (e) {
|
|
el.innerHTML = `<div class="s-loading" style="color:var(--error);">${e.message}</div>`;
|
|
}
|
|
}
|
|
|
|
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 = '';
|
|
|
|
container.innerHTML = sessions.map((s, i) => {
|
|
const pl = s.platform || 'unknown';
|
|
const ic = pIcon[pl] || pIcon.unknown;
|
|
const nm = s.channel_name || s.channel_id || '-';
|
|
const tag = s.is_group ? '<span class="s-tag grup">GRUP</span>' : '';
|
|
const model = (s.model||'?').toUpperCase();
|
|
|
|
return `<div class="session-item">
|
|
<div class="session-row" onclick="toggleSession(${i})">
|
|
<div class="p-icon ${pl}">${ic}</div>
|
|
<span class="s-name">${escapeHtml(nm)}</span>
|
|
${tag}
|
|
<div class="s-stats">
|
|
<span>${s.message_count||0}msg</span>
|
|
<span>${model}</span>
|
|
<span>${fmtCost(s.total_cost_usd)}</span>
|
|
</div>
|
|
<span class="s-time">${fmtTime(s.last_message_at)}</span>
|
|
<svg class="s-chevron" id="sc-${i}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
|
</div>
|
|
<div class="session-details" id="sd-${i}">
|
|
<div class="s-detail-row">
|
|
<span>created ${fmtTime(s.created_at)}</span>
|
|
<span>${s.message_count||0} msgs</span>
|
|
<span>${model}</span>
|
|
<span>${fmtTok(s.total_input_tokens)}/${fmtTok(s.total_output_tokens)} tok</span>
|
|
<span>ctx ${fmtTok(s.context_tokens)}</span>
|
|
<span>${fmtCost(s.total_cost_usd)}</span>
|
|
<span>${fmtDur(s.duration_ms)}</span>
|
|
</div>
|
|
<div class="s-messages" id="sm-${i}">
|
|
<div class="s-loading">Click to load messages...</div>
|
|
</div>
|
|
<div class="s-actions">
|
|
<button class="btn btn-secondary btn-danger" onclick="event.stopPropagation();clearSession('${escapeHtml(s.channel_id||'')}')">
|
|
<i data-lucide="trash-2"></i> Clear
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
lucide.createIcons();
|
|
}
|
|
|
|
async function clearSession(channelId) {
|
|
if (!confirm('Clear this session?')) return;
|
|
try {
|
|
const res = await fetch('/echo/api/eco/sessions/clear', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({channel: channelId})
|
|
});
|
|
const data = await res.json();
|
|
if (data.success) loadSessions();
|
|
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) loadSessions();
|
|
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() {
|
|
// Open section if collapsed
|
|
document.getElementById('sec-doctor').classList.remove('collapsed');
|
|
const list = document.getElementById('doctorChecks');
|
|
const btn = document.getElementById('doctorBtn');
|
|
|
|
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 ? '✓' : '✗'}
|
|
</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 toggleSec(id) {
|
|
document.getElementById(id).classList.toggle('collapsed');
|
|
}
|
|
|
|
function refreshAll() {
|
|
loadStatus();
|
|
loadGitStatus();
|
|
loadLogs();
|
|
}
|
|
|
|
// ── Git ──────────────────────────────────────────────────
|
|
|
|
function toggleGitDetails() {
|
|
document.getElementById('gitCard').classList.toggle('collapsed');
|
|
}
|
|
|
|
async function loadGitStatus() {
|
|
try {
|
|
const response = await fetch('/echo/api/eco/git?' + Date.now());
|
|
if (!response.ok) throw new Error('API error');
|
|
const git = await response.json();
|
|
|
|
const badge = document.getElementById('gitBadge');
|
|
if (git.clean) {
|
|
badge.textContent = 'curat';
|
|
badge.className = 'git-badge ok';
|
|
} else {
|
|
badge.textContent = git.uncommittedCount + ' modificari';
|
|
badge.className = 'git-badge warning';
|
|
}
|
|
|
|
const subtitle = document.getElementById('gitSubtitle');
|
|
subtitle.textContent = `${git.branch} · ${git.lastCommit.time}`;
|
|
|
|
const details = document.getElementById('gitDetails');
|
|
let html = `
|
|
<div class="git-detail-item">
|
|
<i data-lucide="git-commit"></i>
|
|
<span><strong>${git.lastCommit.hash}</strong> ${git.lastCommit.message.substring(0, 50)}${git.lastCommit.message.length > 50 ? '...' : ''} (${git.lastCommit.time})</span>
|
|
</div>
|
|
`;
|
|
|
|
if (git.uncommittedCount > 0) {
|
|
const files = (git.uncommittedParsed || []).slice(0, 5).map(f => f.path).join(', ');
|
|
const more = git.uncommittedCount > 5 ? ` +${git.uncommittedCount - 5}` : '';
|
|
html += `<div class="git-detail-item uncommitted">
|
|
<i data-lucide="alert-circle"></i>
|
|
<span><strong>${git.uncommittedCount}</strong> necomise: ${files}${more}</span>
|
|
</div>`;
|
|
}
|
|
|
|
details.innerHTML = html;
|
|
lucide.createIcons();
|
|
} catch (e) {
|
|
console.error('Git status error:', e);
|
|
document.getElementById('gitBadge').textContent = 'eroare';
|
|
document.getElementById('gitBadge').className = 'git-badge error';
|
|
}
|
|
}
|
|
|
|
async function ecoGitCommit() {
|
|
if (!confirm('Fac commit si push la toate modificarile echo-core?')) return;
|
|
try {
|
|
const res = await fetch('/echo/api/eco/git-commit', { method: 'POST' });
|
|
const result = await res.json();
|
|
if (result.success) {
|
|
alert('Commit reusit: ' + (result.files || 0) + ' fisiere');
|
|
setTimeout(loadGitStatus, 1000);
|
|
} else {
|
|
alert('Eroare: ' + (result.error || 'necunoscuta'));
|
|
}
|
|
} catch (e) {
|
|
alert('Eroare la commit: ' + e.message);
|
|
}
|
|
}
|
|
|
|
// ── Init ────────────────────────────────────────────────
|
|
|
|
loadStatus();
|
|
loadSessions();
|
|
loadGitStatus();
|
|
loadLogs();
|
|
lucide.createIcons();
|
|
|
|
// Auto-refresh status every 10s (sessions are manual only)
|
|
statusRefreshInterval = setInterval(loadStatus, 10000);
|
|
</script>
|
|
</body>
|
|
</html>
|