3237 lines
127 KiB
HTML
3237 lines
127 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 · Dashboard</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: 1400px;
|
|
margin: 0 auto;
|
|
padding: var(--space-5);
|
|
}
|
|
|
|
.page-header {
|
|
margin-bottom: var(--space-4);
|
|
}
|
|
|
|
.page-title {
|
|
font-size: var(--text-xl);
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
margin-bottom: var(--space-1);
|
|
}
|
|
|
|
.page-subtitle {
|
|
font-size: var(--text-sm);
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
/* Status bar */
|
|
.status-bar {
|
|
background: var(--bg-surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-lg);
|
|
margin-bottom: var(--space-4);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.status-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-3);
|
|
padding: var(--space-3) var(--space-4);
|
|
background: linear-gradient(135deg, rgba(34, 197, 94, 0.15), rgba(22, 163, 74, 0.1));
|
|
border-bottom: 1px solid var(--border);
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
|
|
.status-header:hover {
|
|
filter: brightness(1.05);
|
|
}
|
|
|
|
.status-title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-2);
|
|
font-weight: 600;
|
|
font-size: var(--text-sm);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.status-title svg {
|
|
width: 16px;
|
|
height: 16px;
|
|
color: #22c55e;
|
|
}
|
|
|
|
.status-actions {
|
|
display: flex;
|
|
gap: var(--space-2);
|
|
margin-left: auto;
|
|
}
|
|
|
|
.status-summary {
|
|
flex: 1;
|
|
font-size: var(--text-xs);
|
|
color: var(--text-muted);
|
|
text-align: right;
|
|
}
|
|
|
|
.status-toggle {
|
|
width: 16px;
|
|
height: 16px;
|
|
color: var(--text-muted);
|
|
transition: transform var(--transition-fast);
|
|
}
|
|
|
|
.status-bar.collapsed .status-toggle {
|
|
transform: rotate(-90deg);
|
|
}
|
|
|
|
.status-bar.collapsed .status-content {
|
|
display: none;
|
|
}
|
|
|
|
.status-content {
|
|
padding: 0;
|
|
}
|
|
|
|
/* Status sections */
|
|
.status-section {
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.status-section:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.status-section-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-3);
|
|
padding: var(--space-3) var(--space-4);
|
|
cursor: pointer;
|
|
user-select: none;
|
|
transition: background var(--transition-fast);
|
|
}
|
|
|
|
.status-section-header:hover {
|
|
background: var(--bg-elevated);
|
|
}
|
|
|
|
.status-section-icon {
|
|
width: 32px;
|
|
height: 32px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: var(--radius-md);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.status-section-icon svg {
|
|
width: 18px;
|
|
height: 18px;
|
|
}
|
|
|
|
.status-section-icon.git {
|
|
background: rgba(249, 115, 22, 0.15);
|
|
color: #f97316;
|
|
}
|
|
|
|
.status-section-icon.anaf {
|
|
background: rgba(34, 197, 94, 0.15);
|
|
color: #22c55e;
|
|
}
|
|
|
|
.status-section-icon.cron {
|
|
background: rgba(99, 102, 241, 0.15);
|
|
color: #818cf8;
|
|
}
|
|
|
|
.status-section-icon.services {
|
|
background: rgba(59, 130, 246, 0.15);
|
|
color: #3b82f6;
|
|
}
|
|
|
|
.status-section-icon.sessions {
|
|
background: rgba(168, 85, 247, 0.15);
|
|
color: #a855f7;
|
|
}
|
|
|
|
.status-section-icon.logs {
|
|
background: rgba(156, 163, 175, 0.15);
|
|
color: #9ca3af;
|
|
}
|
|
|
|
.status-section-icon.doctor {
|
|
background: rgba(20, 184, 166, 0.15);
|
|
color: #14b8a6;
|
|
}
|
|
|
|
.status-section-actions {
|
|
display: flex;
|
|
gap: var(--space-2);
|
|
align-items: center;
|
|
margin-left: auto;
|
|
}
|
|
|
|
.status-section-actions .btn {
|
|
font-size: var(--text-xs);
|
|
padding: var(--space-1) var(--space-2);
|
|
}
|
|
|
|
/* Heavy content (services grid, sessions list, logs, doctor) — full width, no left indent */
|
|
.status-section-details.full-width {
|
|
padding: 0 var(--space-4) var(--space-3);
|
|
}
|
|
|
|
.status-section-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.status-section-title {
|
|
font-size: var(--text-sm);
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-2);
|
|
}
|
|
|
|
.status-section-subtitle {
|
|
font-size: 13px;
|
|
color: var(--text-secondary);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.status-badge {
|
|
padding: 3px 10px;
|
|
border-radius: var(--radius-sm);
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.status-badge.ok {
|
|
background: rgba(34, 197, 94, 0.15);
|
|
color: #22c55e;
|
|
}
|
|
|
|
.status-badge.warning {
|
|
background: rgba(249, 115, 22, 0.15);
|
|
color: #f97316;
|
|
}
|
|
|
|
.status-badge.error {
|
|
background: rgba(239, 68, 68, 0.15);
|
|
color: #ef4444;
|
|
}
|
|
|
|
.status-section-toggle {
|
|
width: 16px;
|
|
height: 16px;
|
|
color: var(--text-muted);
|
|
transition: transform var(--transition-fast);
|
|
}
|
|
|
|
.status-section.collapsed .status-section-toggle {
|
|
transform: rotate(-90deg);
|
|
}
|
|
|
|
.status-section.collapsed .status-section-details {
|
|
display: none;
|
|
}
|
|
|
|
.status-section-details {
|
|
padding: 0 var(--space-4) var(--space-3);
|
|
padding-left: calc(var(--space-4) + 32px + var(--space-3));
|
|
}
|
|
|
|
.status-detail-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-2);
|
|
font-size: 13px;
|
|
color: var(--text-primary);
|
|
padding: 2px 0;
|
|
}
|
|
|
|
.status-detail-item svg {
|
|
width: 14px;
|
|
height: 14px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.status-detail-item.uncommitted {
|
|
color: #f97316;
|
|
}
|
|
|
|
.status-detail-item code {
|
|
font-family: monospace;
|
|
background: var(--bg-elevated);
|
|
padding: 2px 6px;
|
|
border-radius: 3px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
/* Cron items - desktop compact grid */
|
|
.cron-list {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 2px var(--space-4);
|
|
}
|
|
|
|
@media (max-width: 900px) {
|
|
.cron-list {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
.cron-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-2);
|
|
font-size: var(--text-sm);
|
|
padding: 2px 0;
|
|
}
|
|
|
|
.cron-item.done {
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.cron-item.pending {
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.cron-time {
|
|
font-family: monospace;
|
|
min-width: 50px;
|
|
font-size: 13px;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.cron-date {
|
|
color: #9ca3af;
|
|
font-size: 12px;
|
|
margin-left: 4px;
|
|
}
|
|
|
|
.cron-icon {
|
|
width: 14px;
|
|
height: 14px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.cron-icon.done { color: #22c55e; }
|
|
.cron-icon.pending { color: #9ca3af; }
|
|
.cron-icon.failed { color: #ef4444; }
|
|
|
|
.cron-name {
|
|
flex: 1;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
/* Two-column dashboard */
|
|
.dashboard-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: var(--space-4);
|
|
}
|
|
|
|
@media (max-width: 900px) {
|
|
.dashboard-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
/* Panel styling */
|
|
.panel {
|
|
background: var(--bg-surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-lg);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.panel-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: var(--space-3) var(--space-4);
|
|
border-bottom: 1px solid var(--border);
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
|
|
.panel-header:hover {
|
|
filter: brightness(1.05);
|
|
}
|
|
|
|
.panel-header-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-2);
|
|
}
|
|
.panel-header-right {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-2);
|
|
margin-left: auto;
|
|
}
|
|
|
|
.panel-title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-2);
|
|
font-weight: 600;
|
|
font-size: var(--text-sm);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.panel-title svg {
|
|
width: 18px;
|
|
height: 18px;
|
|
}
|
|
|
|
.panel-toggle {
|
|
width: 16px;
|
|
height: 16px;
|
|
color: var(--text-muted);
|
|
transition: transform var(--transition-fast);
|
|
}
|
|
|
|
.panel.collapsed .panel-toggle {
|
|
transform: rotate(-90deg);
|
|
}
|
|
|
|
.panel.collapsed .panel-body {
|
|
display: none;
|
|
}
|
|
|
|
.panel-actions {
|
|
display: flex;
|
|
gap: var(--space-2);
|
|
}
|
|
|
|
.panel-count {
|
|
font-size: var(--text-xs);
|
|
color: var(--text-muted);
|
|
background: var(--bg-elevated);
|
|
padding: 2px 8px;
|
|
border-radius: var(--radius-sm);
|
|
}
|
|
|
|
.panel-body {
|
|
padding: var(--space-3);
|
|
max-height: 500px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.progress-bar {
|
|
height: 4px;
|
|
background: var(--bg-elevated);
|
|
border-radius: 2px;
|
|
margin-top: var(--space-1);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.progress-fill {
|
|
height: 100%;
|
|
background: var(--accent);
|
|
border-radius: 2px;
|
|
transition: width 0.3s ease;
|
|
}
|
|
|
|
/* Issues panel */
|
|
.issues-panel .panel-header {
|
|
background: linear-gradient(135deg, rgba(249, 115, 22, 0.15), rgba(234, 88, 12, 0.1));
|
|
}
|
|
|
|
.issues-panel .panel-title svg {
|
|
color: #f97316;
|
|
}
|
|
|
|
.issues-filters {
|
|
display: flex;
|
|
gap: var(--space-2);
|
|
padding: var(--space-2) var(--space-3);
|
|
border-bottom: 1px solid var(--border);
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.filter-btn {
|
|
padding: var(--space-1) var(--space-2);
|
|
font-size: var(--text-xs);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
background: transparent;
|
|
color: var(--text-muted);
|
|
cursor: pointer;
|
|
transition: all var(--transition-fast);
|
|
}
|
|
|
|
.filter-btn:hover, .filter-btn.active {
|
|
border-color: var(--accent);
|
|
color: var(--accent);
|
|
background: var(--accent-subtle);
|
|
}
|
|
|
|
.priority-group {
|
|
margin-bottom: var(--space-5);
|
|
}
|
|
|
|
.priority-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-2);
|
|
font-size: var(--text-sm);
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
margin-bottom: var(--space-3);
|
|
cursor: pointer;
|
|
padding: var(--space-2) 0;
|
|
}
|
|
|
|
.priority-header:hover {
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.priority-header svg {
|
|
display: none;
|
|
}
|
|
|
|
.priority-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.priority-dot.urgent-important { background: #ef4444; }
|
|
.priority-dot.important { background: #f59e0b; }
|
|
.priority-dot.urgent { background: #eab308; }
|
|
.priority-dot.backlog { background: #6b7280; }
|
|
|
|
.priority-content {
|
|
display: block;
|
|
}
|
|
|
|
.priority-content.hidden {
|
|
display: none;
|
|
}
|
|
|
|
.issue-item {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: var(--space-3);
|
|
padding: var(--space-3);
|
|
background: var(--bg-elevated);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-md);
|
|
margin-bottom: var(--space-2);
|
|
cursor: pointer;
|
|
transition: all var(--transition-fast);
|
|
}
|
|
|
|
.issue-item:hover {
|
|
border-color: var(--border-focus);
|
|
transform: translateX(2px);
|
|
}
|
|
|
|
.issue-item.done {
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.issue-item.done .issue-title {
|
|
text-decoration: line-through;
|
|
}
|
|
|
|
.issue-checkbox {
|
|
width: 18px;
|
|
height: 18px;
|
|
border: 2px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
transition: all var(--transition-fast);
|
|
}
|
|
|
|
.issue-checkbox:hover {
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
.issue-checkbox.checked {
|
|
background: var(--accent);
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
.issue-checkbox.in-progress {
|
|
background: rgba(59, 130, 246, 0.5);
|
|
border-color: #3b82f6;
|
|
}
|
|
|
|
.issue-checkbox svg {
|
|
width: 12px;
|
|
height: 12px;
|
|
color: white;
|
|
display: none;
|
|
}
|
|
|
|
.issue-checkbox.checked svg {
|
|
display: block;
|
|
}
|
|
|
|
.issue-checkbox.in-progress::after {
|
|
content: '';
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
background: #3b82f6;
|
|
}
|
|
|
|
.issue-content {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.issue-title {
|
|
font-size: var(--text-sm);
|
|
font-weight: 500;
|
|
color: var(--text-primary);
|
|
margin-bottom: var(--space-1);
|
|
}
|
|
|
|
.issue-meta {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: var(--space-2);
|
|
font-size: var(--text-xs);
|
|
}
|
|
|
|
.issue-tag {
|
|
padding: 2px 6px;
|
|
border-radius: var(--radius-sm);
|
|
background: var(--bg-surface);
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.issue-tag.program {
|
|
background: rgba(99, 102, 241, 0.15);
|
|
color: #818cf8;
|
|
}
|
|
|
|
.issue-owner {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.issue-owner.marius { color: #22c55e; }
|
|
.issue-owner.robert { color: #f59e0b; }
|
|
|
|
.issue-status {
|
|
padding: 2px 8px;
|
|
border-radius: var(--radius-sm);
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.3px;
|
|
}
|
|
.issue-status.todo {
|
|
background: rgba(156, 163, 175, 0.2);
|
|
color: #9ca3af;
|
|
}
|
|
.issue-status.in-progress {
|
|
background: rgba(59, 130, 246, 0.2);
|
|
color: #3b82f6;
|
|
}
|
|
.issue-status.done {
|
|
background: rgba(34, 197, 94, 0.2);
|
|
color: #22c55e;
|
|
}
|
|
|
|
/* Todo's Panel */
|
|
.todos-panel { border-left: 3px solid #8b5cf6; }
|
|
.todo-section { margin-bottom: 16px; }
|
|
.todo-section-title {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: #9ca3af;
|
|
margin-bottom: 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
.todo-section-title.overdue { color: #ef4444; }
|
|
.todo-item {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 12px;
|
|
padding: 10px 12px;
|
|
background: var(--bg-elevated);
|
|
border-radius: 6px;
|
|
margin-bottom: 6px;
|
|
transition: all 0.2s;
|
|
border: 1px solid var(--border-color);
|
|
}
|
|
.todo-item:hover { background: var(--bg-hover); }
|
|
.todo-item.done { opacity: 0.6; }
|
|
.todo-item.done .todo-text { text-decoration: line-through; }
|
|
.todo-checkbox {
|
|
width: 22px;
|
|
height: 22px;
|
|
border: 2px solid #6b7280;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
transition: all 0.2s;
|
|
background: transparent;
|
|
}
|
|
.todo-checkbox:hover { border-color: #8b5cf6; background: rgba(139, 92, 246, 0.1); }
|
|
.todo-checkbox.checked {
|
|
background: #8b5cf6;
|
|
border-color: #8b5cf6;
|
|
}
|
|
.todo-checkbox svg { display: none; width: 14px; height: 14px; color: white; }
|
|
.todo-checkbox.checked svg { display: block; }
|
|
.todo-content { flex: 1; min-width: 0; }
|
|
.todo-text { font-size: 14px; color: var(--text-primary); margin-bottom: 10px; font-weight: 500; }
|
|
.todo-context { font-size: 13px; color: var(--text-muted); margin-bottom: 10px; line-height: 1.5; }
|
|
.todo-example { font-size: 13px; color: var(--text-muted); margin-bottom: 10px; line-height: 1.5; font-style: italic; background: var(--bg-elevated); padding: 8px 10px; border-radius: 6px; border-left: 3px solid #8b5cf6; }
|
|
.todo-meta { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
|
.todo-domain {
|
|
font-size: 11px;
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
font-weight: 500;
|
|
}
|
|
.todo-domain.work { background: #1e40af; color: #93c5fd; }
|
|
.todo-domain.self { background: #6b21a8; color: #d8b4fe; }
|
|
.todo-domain.sprijin { background: #166534; color: #86efac; }
|
|
.todo-domain.scout { background: #c2410c; color: #fdba74; }
|
|
.todo-due {
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
}
|
|
.todo-due.overdue { color: #ef4444; font-weight: 500; }
|
|
.todo-source { font-size: 11px; color: var(--text-muted); }
|
|
.todo-source-link { font-size: 11px; color: #3b82f6; text-decoration: none; }
|
|
.todo-source-link:hover { text-decoration: underline; }
|
|
|
|
/* Note Overlay */
|
|
.note-overlay {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(0, 0, 0, 0.8);
|
|
z-index: 1000;
|
|
padding: 20px;
|
|
}
|
|
.note-overlay.active { display: flex; align-items: center; justify-content: center; }
|
|
.note-overlay-content {
|
|
background: var(--bg-surface);
|
|
border-radius: 12px;
|
|
width: 90%;
|
|
max-width: 900px;
|
|
height: 85vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
.note-overlay-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 12px 16px;
|
|
border-bottom: 1px solid var(--border-color);
|
|
}
|
|
.note-overlay-header span { font-weight: 600; color: var(--text-primary); }
|
|
#noteOverlayFrame {
|
|
flex: 1;
|
|
border: none;
|
|
width: 100%;
|
|
}
|
|
|
|
/* Button icon only */
|
|
.btn-icon {
|
|
padding: 6px;
|
|
background: transparent;
|
|
border: 1px solid #374151;
|
|
border-radius: 6px;
|
|
color: #9ca3af;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: all 0.2s;
|
|
}
|
|
.btn-icon:hover {
|
|
background: #374151;
|
|
color: #f3f4f6;
|
|
border-color: #4b5563;
|
|
}
|
|
.btn-icon svg { width: 16px; height: 16px; }
|
|
.issue-owner.clawdbot { color: #8b5cf6; }
|
|
|
|
.issue-date {
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
/* Add issue */
|
|
.add-issue-btn {
|
|
width: 100%;
|
|
padding: var(--space-3);
|
|
background: transparent;
|
|
border: 1px dashed var(--border);
|
|
border-radius: var(--radius-md);
|
|
color: var(--text-muted);
|
|
font-size: var(--text-sm);
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: var(--space-2);
|
|
transition: all var(--transition-fast);
|
|
margin-top: var(--space-3);
|
|
}
|
|
|
|
.add-issue-btn:hover {
|
|
border-color: var(--accent);
|
|
color: var(--accent);
|
|
background: var(--accent-subtle);
|
|
}
|
|
|
|
.add-issue-btn svg {
|
|
width: 16px;
|
|
height: 16px;
|
|
}
|
|
|
|
/* Add form modal */
|
|
.modal-overlay {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
z-index: 1000;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.modal-overlay.active {
|
|
display: flex;
|
|
}
|
|
|
|
.modal {
|
|
background: var(--bg-base);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-lg);
|
|
padding: var(--space-5);
|
|
width: 90%;
|
|
max-width: 500px;
|
|
max-height: 90vh;
|
|
overflow-y: auto;
|
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
|
}
|
|
|
|
.modal-title {
|
|
font-size: var(--text-lg);
|
|
font-weight: 600;
|
|
margin-bottom: var(--space-4);
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: var(--space-4);
|
|
}
|
|
|
|
.form-label {
|
|
display: block;
|
|
font-size: var(--text-sm);
|
|
font-weight: 500;
|
|
margin-bottom: var(--space-1);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.form-row {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: var(--space-3);
|
|
}
|
|
|
|
.modal-actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: var(--space-2);
|
|
margin-top: var(--space-5);
|
|
}
|
|
|
|
/* Toast */
|
|
.toast {
|
|
position: fixed;
|
|
bottom: var(--space-5);
|
|
left: 50%;
|
|
transform: translateX(-50%) translateY(100px);
|
|
background: var(--bg-elevated);
|
|
border: 1px solid var(--border);
|
|
padding: var(--space-3) var(--space-5);
|
|
border-radius: var(--radius-md);
|
|
font-size: var(--text-sm);
|
|
opacity: 0;
|
|
transition: all var(--transition-base);
|
|
z-index: 1001;
|
|
}
|
|
|
|
.toast.show {
|
|
transform: translateX(-50%) translateY(0);
|
|
opacity: 1;
|
|
}
|
|
|
|
/* Empty state */
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: var(--space-6);
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.empty-state svg {
|
|
width: 48px;
|
|
height: 48px;
|
|
margin-bottom: var(--space-3);
|
|
opacity: 0.5;
|
|
}
|
|
|
|
/* Danger button */
|
|
.btn-danger {
|
|
background: #dc2626;
|
|
color: white;
|
|
border: none;
|
|
padding: var(--space-2) var(--space-4);
|
|
border-radius: var(--radius-md);
|
|
font-size: var(--text-sm);
|
|
cursor: pointer;
|
|
transition: all var(--transition-fast);
|
|
}
|
|
|
|
.btn-danger:hover {
|
|
background: #b91c1c;
|
|
}
|
|
|
|
/* Clickable todo item */
|
|
.todo-item {
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* ─────────────────────────────────────────────────────────
|
|
Eco panels (merged from eco.html — now nested under Status)
|
|
───────────────────────────────────────────────────────── */
|
|
|
|
/* Service cards */
|
|
.services-grid {
|
|
display: grid;
|
|
grid-template-columns: 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);
|
|
min-height: 44px;
|
|
}
|
|
|
|
/* 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; }
|
|
|
|
.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;
|
|
overflow-x: 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 */
|
|
.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 (eco) */
|
|
.btn-warning {
|
|
border-color: var(--warning);
|
|
color: var(--warning);
|
|
}
|
|
.btn-warning:hover {
|
|
background: var(--warning);
|
|
color: #000;
|
|
}
|
|
|
|
/* Skeleton loading (design spec) */
|
|
.skeleton-row {
|
|
height: 14px;
|
|
margin: 6px 0;
|
|
background: linear-gradient(90deg, var(--bg-surface) 0%, var(--bg-elevated) 50%, var(--bg-surface) 100%);
|
|
background-size: 200% 100%;
|
|
border-radius: var(--radius-sm);
|
|
animation: skeleton-pulse 1.5s infinite;
|
|
}
|
|
|
|
@keyframes skeleton-pulse {
|
|
0% { background-position: 200% 0; }
|
|
100% { background-position: -200% 0; }
|
|
}
|
|
|
|
/* Panel error state (design spec) */
|
|
.panel-error {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-3);
|
|
padding: var(--space-4);
|
|
background: rgba(239, 68, 68, 0.08);
|
|
border: 1px solid rgba(239, 68, 68, 0.3);
|
|
border-radius: var(--radius-md);
|
|
color: var(--error);
|
|
font-size: var(--text-sm);
|
|
}
|
|
|
|
.panel-error button {
|
|
margin-left: auto;
|
|
background: transparent;
|
|
border: 1px solid var(--error);
|
|
color: var(--error);
|
|
padding: 4px 10px;
|
|
border-radius: var(--radius-sm);
|
|
cursor: pointer;
|
|
font-size: var(--text-xs);
|
|
min-height: 32px;
|
|
}
|
|
|
|
.panel-error button:hover {
|
|
background: var(--error);
|
|
color: white;
|
|
}
|
|
|
|
/* Empty state — warm + action (design spec) */
|
|
.empty-state-warm {
|
|
padding: var(--space-4);
|
|
text-align: center;
|
|
color: var(--text-muted);
|
|
font-size: var(--text-sm);
|
|
line-height: 1.5;
|
|
}
|
|
|
|
/* Focus ring for a11y (design spec) */
|
|
:focus-visible {
|
|
outline: 2px solid var(--accent);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
/* Responsive grid per plan: 1-col mobile / 2-col 768-1199 / 3-col ≥1200 */
|
|
@media (min-width: 768px) {
|
|
.services-grid { grid-template-columns: repeat(2, 1fr); }
|
|
}
|
|
@media (min-width: 1200px) {
|
|
.services-grid { grid-template-columns: repeat(3, 1fr); }
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.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>
|
|
<!--NAV-->
|
|
|
|
<main class="main">
|
|
<div class="page-header">
|
|
<h1 class="page-title">Dashboard</h1>
|
|
</div>
|
|
|
|
<!-- Status Bar -->
|
|
<div class="status-bar" id="statusBar">
|
|
<div class="status-header" onclick="toggleSection('statusBar')">
|
|
<div class="status-title">
|
|
<i data-lucide="activity"></i>
|
|
<span>Status</span>
|
|
</div>
|
|
<div class="status-summary" id="statusSummary">Se încarcă...</div>
|
|
<div class="status-actions" onclick="event.stopPropagation()">
|
|
<button class="btn btn-icon" onclick="gitCommit()" title="Git Commit">
|
|
<i data-lucide="git-commit"></i>
|
|
</button>
|
|
<button class="btn btn-icon" onclick="restartTaskboard()" title="Restart Taskboard">
|
|
<i data-lucide="rotate-cw"></i>
|
|
</button>
|
|
<button class="btn btn-icon" onclick="refreshStatus()" title="Refresh">
|
|
<i data-lucide="refresh-cw"></i>
|
|
</button>
|
|
</div>
|
|
<i data-lucide="chevron-down" class="status-toggle"></i>
|
|
</div>
|
|
<div class="status-content">
|
|
<!-- Git Section -->
|
|
<div class="status-section" id="gitSection">
|
|
<div class="status-section-header" onclick="toggleStatusSection('gitSection')">
|
|
<div class="status-section-icon git">
|
|
<i data-lucide="git-branch"></i>
|
|
</div>
|
|
<div class="status-section-info">
|
|
<div class="status-section-title">
|
|
Git
|
|
<span class="status-badge ok" id="gitBadge">curat</span>
|
|
</div>
|
|
<div class="status-section-subtitle" id="gitSubtitle">Se încarcă...</div>
|
|
</div>
|
|
<div class="status-section-actions" onclick="event.stopPropagation()">
|
|
<button class="btn btn-secondary" onclick="ecoGitCommit()" title="Commit & Push" aria-label="Commit și push">
|
|
<i data-lucide="git-commit"></i> Commit
|
|
</button>
|
|
<button class="btn btn-secondary" onclick="loadGitStatus()" title="Refresh" aria-label="Refresh git status">
|
|
<i data-lucide="refresh-cw"></i>
|
|
</button>
|
|
</div>
|
|
<i data-lucide="chevron-down" class="status-section-toggle"></i>
|
|
</div>
|
|
<div class="status-section-details" id="gitDetails">
|
|
<div class="skeleton-row" style="width:70%"></div>
|
|
<div class="skeleton-row" style="width:40%"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Services Section -->
|
|
<div class="status-section collapsed" id="servicesSection">
|
|
<div class="status-section-header" onclick="toggleStatusSection('servicesSection')">
|
|
<div class="status-section-icon services">
|
|
<i data-lucide="server"></i>
|
|
</div>
|
|
<div class="status-section-info">
|
|
<div class="status-section-title">
|
|
Services
|
|
<span class="status-badge ok" id="servicesBadge">—</span>
|
|
</div>
|
|
<div class="status-section-subtitle" id="servicesSubtitle">Se încarcă...</div>
|
|
</div>
|
|
<div class="status-section-actions" onclick="event.stopPropagation()">
|
|
<button class="btn btn-secondary" onclick="loadServices()" title="Refresh services" aria-label="Refresh services">
|
|
<i data-lucide="refresh-cw"></i>
|
|
</button>
|
|
</div>
|
|
<i data-lucide="chevron-down" class="status-section-toggle"></i>
|
|
</div>
|
|
<div class="status-section-details full-width">
|
|
<div class="services-grid" id="servicesGrid">
|
|
<div class="service-card"><div class="skeleton-row" style="width:60%"></div><div class="skeleton-row" style="width:80%"></div><div class="skeleton-row" style="width:40%"></div></div>
|
|
<div class="service-card"><div class="skeleton-row" style="width:70%"></div><div class="skeleton-row" style="width:50%"></div><div class="skeleton-row" style="width:60%"></div></div>
|
|
<div class="service-card"><div class="skeleton-row" style="width:50%"></div><div class="skeleton-row" style="width:70%"></div><div class="skeleton-row" style="width:55%"></div></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sessions Section -->
|
|
<div class="status-section collapsed" id="sessionsSection">
|
|
<div class="status-section-header" onclick="toggleStatusSection('sessionsSection')">
|
|
<div class="status-section-icon sessions">
|
|
<i data-lucide="message-square"></i>
|
|
</div>
|
|
<div class="status-section-info">
|
|
<div class="status-section-title">
|
|
Sessions
|
|
<span class="status-badge ok" id="sessionsBadge">—</span>
|
|
</div>
|
|
<div class="status-section-subtitle" id="sessionsSubtitle">Canale active</div>
|
|
</div>
|
|
<div class="status-section-actions" onclick="event.stopPropagation()">
|
|
<button class="btn btn-secondary" onclick="loadSessions()" title="Refresh sessions" aria-label="Refresh sessions">
|
|
<i data-lucide="refresh-cw"></i>
|
|
</button>
|
|
<button class="btn btn-secondary btn-danger" onclick="clearAllSessions()" id="clearAllBtn" aria-label="Clear all sessions" style="display:none;">
|
|
<i data-lucide="trash-2"></i> Clear All
|
|
</button>
|
|
</div>
|
|
<i data-lucide="chevron-down" class="status-section-toggle"></i>
|
|
</div>
|
|
<div class="status-section-details full-width">
|
|
<div class="sessions-card">
|
|
<div id="sessionsContent">
|
|
<div style="padding:var(--space-3)">
|
|
<div class="skeleton-row" style="width:70%"></div>
|
|
<div class="skeleton-row" style="width:50%"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ANAF Section -->
|
|
<div class="status-section collapsed" id="anafSection">
|
|
<div class="status-section-header" onclick="toggleStatusSection('anafSection')">
|
|
<div class="status-section-icon anaf">
|
|
<i data-lucide="building-2"></i>
|
|
</div>
|
|
<div class="status-section-info">
|
|
<div class="status-section-title">
|
|
ANAF Monitor
|
|
<span class="status-badge ok" id="anafBadge">OK</span>
|
|
</div>
|
|
<div class="status-section-subtitle" id="anafSubtitle">Nicio modificare detectată</div>
|
|
</div>
|
|
<i data-lucide="chevron-down" class="status-section-toggle"></i>
|
|
</div>
|
|
<div class="status-section-details" id="anafDetails">
|
|
<div class="status-detail-item">
|
|
<i data-lucide="clock"></i>
|
|
<span id="anafLastCheck">Ultima verificare: -</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Cron Section -->
|
|
<div class="status-section" id="cronSection">
|
|
<div class="status-section-header" onclick="toggleStatusSection('cronSection')">
|
|
<div class="status-section-icon cron">
|
|
<i data-lucide="clock"></i>
|
|
</div>
|
|
<div class="status-section-info">
|
|
<div class="status-section-title">
|
|
Cron Jobs
|
|
<span class="status-badge ok" id="cronBadge">0/0</span>
|
|
</div>
|
|
<div class="status-section-subtitle" id="cronSubtitle">Jobs programate azi</div>
|
|
</div>
|
|
<i data-lucide="chevron-down" class="status-section-toggle"></i>
|
|
</div>
|
|
<div class="status-section-details" id="cronDetails">
|
|
<!-- Populated by JS -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Logs Section (lazy-loaded) -->
|
|
<div class="status-section collapsed" id="logsSection" data-loaded="0">
|
|
<div class="status-section-header" onclick="toggleStatusSection('logsSection')">
|
|
<div class="status-section-icon logs">
|
|
<i data-lucide="scroll-text"></i>
|
|
</div>
|
|
<div class="status-section-info">
|
|
<div class="status-section-title">Logs</div>
|
|
<div class="status-section-subtitle">echo-core.service journalctl</div>
|
|
</div>
|
|
<i data-lucide="chevron-down" class="status-section-toggle"></i>
|
|
</div>
|
|
<div class="status-section-details full-width">
|
|
<div class="log-card">
|
|
<div class="log-toolbar">
|
|
<label for="logLines">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()" aria-label="Refresh logs" 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 for="autoRefreshLogs" 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">Apasă Refresh sau expandă secțiunea pentru a încărca.</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Doctor Section -->
|
|
<div class="status-section collapsed" id="doctorSection">
|
|
<div class="status-section-header" onclick="toggleStatusSection('doctorSection')">
|
|
<div class="status-section-icon doctor">
|
|
<i data-lucide="stethoscope"></i>
|
|
</div>
|
|
<div class="status-section-info">
|
|
<div class="status-section-title">Doctor</div>
|
|
<div class="status-section-subtitle">Diagnosticare sistem</div>
|
|
</div>
|
|
<div class="status-section-actions" onclick="event.stopPropagation()">
|
|
<button class="btn btn-primary" onclick="runDoctor()" id="doctorBtn" aria-label="Run diagnostics">
|
|
<i data-lucide="play"></i> Run
|
|
</button>
|
|
</div>
|
|
<i data-lucide="chevron-down" class="status-section-toggle"></i>
|
|
</div>
|
|
<div class="status-section-details full-width">
|
|
<div class="doctor-card" id="doctorCard">
|
|
<ul class="check-list" id="doctorChecks">
|
|
<li class="check-item" style="color:var(--text-muted);justify-content:center;">Apasă pentru a rula verificările.</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<div class="dashboard-grid">
|
|
<!-- Issues Panel -->
|
|
<div class="panel issues-panel" id="issuesPanel">
|
|
<div class="panel-header" onclick="toggleSection('issuesPanel')">
|
|
<div class="panel-header-left">
|
|
<div class="panel-title">
|
|
<span>Issues</span>
|
|
</div>
|
|
<span class="panel-count" id="issuesCount">0</span>
|
|
</div>
|
|
<div class="panel-header-right" onclick="event.stopPropagation()">
|
|
<button class="btn btn-icon" onclick="showAddModal()" title="Adaugă issue">
|
|
<i data-lucide="plus"></i>
|
|
</button>
|
|
<i data-lucide="chevron-down" class="panel-toggle" onclick="event.stopPropagation(); toggleSection('issuesPanel')"></i>
|
|
</div>
|
|
</div>
|
|
<div class="issues-filters" id="issuesFilters">
|
|
<button class="filter-btn active" data-filter="all">Toate</button>
|
|
<button class="filter-btn" data-filter="marius">👤 Marius</button>
|
|
<button class="filter-btn" data-filter="robert">👷 Robert</button>
|
|
<button class="filter-btn" data-filter="clawdbot">🤖 Clawdbot</button>
|
|
</div>
|
|
<div class="panel-body" id="issuesBody">
|
|
<div class="empty-state">
|
|
<i data-lucide="loader"></i>
|
|
<p>Se încarcă...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Todo's Panel -->
|
|
<div class="panel todos-panel" id="todosPanel">
|
|
<div class="panel-header" onclick="toggleSection('todosPanel')">
|
|
<div class="panel-header-left">
|
|
<div class="panel-title">
|
|
<span>Todo's</span>
|
|
</div>
|
|
<span class="panel-count" id="todosCount">0</span>
|
|
</div>
|
|
<div class="panel-header-right" onclick="event.stopPropagation()">
|
|
<button class="btn btn-icon" onclick="showAddTodoModal()" title="Adaugă todo">
|
|
<i data-lucide="plus"></i>
|
|
</button>
|
|
<i data-lucide="chevron-down" class="panel-toggle" onclick="event.stopPropagation(); toggleSection('todosPanel')"></i>
|
|
</div>
|
|
</div>
|
|
<div class="panel-body" id="todosBody">
|
|
<div class="empty-state">
|
|
<i data-lucide="loader"></i>
|
|
<p>Se încarcă...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</main>
|
|
|
|
<!-- Note Overlay -->
|
|
<div class="note-overlay" id="noteOverlay" onclick="if(event.target === this) closeNoteOverlay()">
|
|
<div class="note-overlay-content">
|
|
<div class="note-overlay-header">
|
|
<span id="noteOverlayTitle">Notă</span>
|
|
<button class="btn btn-icon" onclick="closeNoteOverlay()">
|
|
<i data-lucide="x"></i>
|
|
</button>
|
|
</div>
|
|
<iframe id="noteOverlayFrame" src=""></iframe>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Issue Modal (Add/Edit) -->
|
|
<div class="modal-overlay" id="issueModal">
|
|
<div class="modal">
|
|
<h2 class="modal-title" id="issueModalTitle">Issue nou</h2>
|
|
<input type="hidden" id="issueEditId">
|
|
<div class="form-group">
|
|
<label class="form-label">Titlu *</label>
|
|
<input type="text" class="input" id="issueTitle" placeholder="Ce trebuie făcut?">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Descriere</label>
|
|
<textarea class="input" id="issueDesc" rows="3" placeholder="Detalii..."></textarea>
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label">Program</label>
|
|
<select class="input" id="issueProgram">
|
|
<option value="">— Selectează —</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Owner</label>
|
|
<select class="input" id="issueOwner">
|
|
<option value="marius">👤 Marius</option>
|
|
<option value="robert">👷 Robert</option>
|
|
<option value="clawdbot">🤖 Clawdbot</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label">Prioritate</label>
|
|
<select class="input" id="issuePriority">
|
|
<option value="urgent-important">🔴 Urgent + Important</option>
|
|
<option value="important">🟠 Important</option>
|
|
<option value="urgent">🟡 Urgent</option>
|
|
<option value="backlog">⚪ Backlog</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Status</label>
|
|
<select class="input" id="issueStatus">
|
|
<option value="todo">Todo</option>
|
|
<option value="in-progress">In Progress</option>
|
|
<option value="done">Done</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label">Deadline</label>
|
|
<input type="date" class="input" id="issueDeadline">
|
|
</div>
|
|
<div class="form-group"></div>
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button class="btn btn-danger" id="issueDeleteBtn" onclick="deleteIssue()" style="margin-right: auto; display: none;">Șterge</button>
|
|
<button class="btn btn-secondary" onclick="hideIssueModal()">Anulează</button>
|
|
<button class="btn btn-primary" id="issueSaveBtn" onclick="saveIssue()">Adaugă</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Todo Modal (Add/Edit) -->
|
|
<div class="modal-overlay" id="todoModal">
|
|
<div class="modal">
|
|
<h2 class="modal-title" id="todoModalTitle">Todo nou</h2>
|
|
<input type="hidden" id="todoEditId">
|
|
<div class="form-group">
|
|
<label class="form-label">Ce trebuie făcut? *</label>
|
|
<input type="text" class="input" id="todoText" placeholder="Ex: Verifică client X">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Context</label>
|
|
<textarea class="input" id="todoContext" rows="2" placeholder="De ce? Detalii..."></textarea>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Exemplu</label>
|
|
<textarea class="input" id="todoExample" rows="2" placeholder="Un exemplu concret..."></textarea>
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label">Domeniu</label>
|
|
<select class="input" id="todoDomain">
|
|
<option value="work">💼 Work</option>
|
|
<option value="self">🧘 Self</option>
|
|
<option value="sprijin">💚 Sprijin</option>
|
|
<option value="scout">⚜️ Scout</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Data</label>
|
|
<input type="date" class="input" id="todoDueDate">
|
|
</div>
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label">Sursă</label>
|
|
<input type="text" class="input" id="todoSource" placeholder="Ex: video, articol...">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Link sursă</label>
|
|
<input type="text" class="input" id="todoSourceUrl" placeholder="URL...">
|
|
</div>
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button class="btn btn-danger" id="todoDeleteBtn" onclick="deleteTodo()" style="margin-right: auto; display: none;">Șterge</button>
|
|
<button class="btn btn-secondary" onclick="hideTodoModal()">Anulează</button>
|
|
<button class="btn btn-primary" id="todoSaveBtn" onclick="saveTodoForm()">Adaugă</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="toast" id="toast"></div>
|
|
|
|
<script>
|
|
// Theme
|
|
function initTheme() {
|
|
const saved = localStorage.getItem('theme') || 'dark';
|
|
document.documentElement.setAttribute('data-theme', saved);
|
|
updateThemeIcon(saved);
|
|
}
|
|
|
|
function toggleTheme() {
|
|
const current = document.documentElement.getAttribute('data-theme') || 'dark';
|
|
const next = current === 'dark' ? 'light' : 'dark';
|
|
document.documentElement.setAttribute('data-theme', next);
|
|
localStorage.setItem('theme', next);
|
|
updateThemeIcon(next);
|
|
}
|
|
|
|
function updateThemeIcon(theme) {
|
|
const icon = document.getElementById('themeIcon');
|
|
if (icon) {
|
|
icon.setAttribute('data-lucide', theme === 'dark' ? 'sun' : 'moon');
|
|
lucide.createIcons();
|
|
}
|
|
}
|
|
|
|
initTheme();
|
|
lucide.createIcons();
|
|
|
|
// Collapsible sections
|
|
function getCollapsedSections() {
|
|
try {
|
|
return JSON.parse(localStorage.getItem('collapsedSections') || '["statusBar"]');
|
|
} catch { return ['statusBar']; }
|
|
}
|
|
|
|
function setCollapsedSections(sections) {
|
|
localStorage.setItem('collapsedSections', JSON.stringify(sections));
|
|
}
|
|
|
|
function initCollapsedSections() {
|
|
const collapsed = getCollapsedSections();
|
|
collapsed.forEach(id => {
|
|
const el = document.getElementById(id);
|
|
if (el) el.classList.add('collapsed');
|
|
});
|
|
}
|
|
|
|
function toggleSection(id) {
|
|
const el = document.getElementById(id);
|
|
if (!el) return;
|
|
|
|
el.classList.toggle('collapsed');
|
|
|
|
const collapsed = getCollapsedSections();
|
|
const idx = collapsed.indexOf(id);
|
|
if (el.classList.contains('collapsed')) {
|
|
if (idx === -1) collapsed.push(id);
|
|
} else {
|
|
if (idx > -1) collapsed.splice(idx, 1);
|
|
}
|
|
setCollapsedSections(collapsed);
|
|
}
|
|
|
|
initCollapsedSections();
|
|
|
|
// Data
|
|
let issuesData = null;
|
|
let currentFilter = 'all';
|
|
let collapsedPriorities = new Set(['backlog', 'done']);
|
|
|
|
// Priority labels
|
|
const priorityLabels = {
|
|
'urgent-important': 'Urgent + Important',
|
|
'important': 'Important',
|
|
'urgent': 'Urgent',
|
|
'backlog': 'Backlog'
|
|
};
|
|
|
|
const priorityOrder = ['urgent-important', 'important', 'urgent', 'backlog'];
|
|
|
|
// Status sections collapse state
|
|
function getCollapsedStatusSections() {
|
|
const DEFAULTS = ['servicesSection', 'sessionsSection', 'anafSection', 'logsSection', 'doctorSection'];
|
|
try {
|
|
return JSON.parse(localStorage.getItem('collapsedStatusSections') || JSON.stringify(DEFAULTS));
|
|
} catch { return DEFAULTS; }
|
|
}
|
|
|
|
function setCollapsedStatusSections(sections) {
|
|
localStorage.setItem('collapsedStatusSections', JSON.stringify(sections));
|
|
}
|
|
|
|
function initStatusSections() {
|
|
const collapsed = new Set(getCollapsedStatusSections());
|
|
document.querySelectorAll('#statusBar .status-section').forEach(el => {
|
|
if (collapsed.has(el.id)) {
|
|
el.classList.add('collapsed');
|
|
} else {
|
|
el.classList.remove('collapsed');
|
|
}
|
|
});
|
|
}
|
|
|
|
function toggleStatusSection(id) {
|
|
const el = document.getElementById(id);
|
|
if (!el) return;
|
|
|
|
const wasCollapsed = el.classList.contains('collapsed');
|
|
el.classList.toggle('collapsed');
|
|
|
|
// Lazy-load logs the first time the section opens.
|
|
if (id === 'logsSection' && wasCollapsed && el.dataset.loaded !== '1') {
|
|
el.dataset.loaded = '1';
|
|
loadLogs();
|
|
}
|
|
|
|
const collapsed = getCollapsedStatusSections();
|
|
const idx = collapsed.indexOf(id);
|
|
if (el.classList.contains('collapsed')) {
|
|
if (idx === -1) collapsed.push(id);
|
|
} else {
|
|
if (idx > -1) collapsed.splice(idx, 1);
|
|
}
|
|
setCollapsedStatusSections(collapsed);
|
|
}
|
|
|
|
// Status loading
|
|
async function loadStatus() {
|
|
await Promise.all([
|
|
loadGitStatus(),
|
|
loadAnafStatus(),
|
|
loadCronStatus()
|
|
]);
|
|
updateStatusSummary();
|
|
}
|
|
|
|
async function restartTaskboard() {
|
|
if (!confirm('Restart taskboard? Pagina va fi indisponibilă ~5 secunde.')) return;
|
|
showToast('Se restartează taskboard...', 'info');
|
|
try {
|
|
await fetch('/echo/api/eco/restart-taskboard', { method: 'POST' });
|
|
} catch (e) { /* expected — server dies */ }
|
|
setTimeout(() => {
|
|
const check = setInterval(async () => {
|
|
try {
|
|
const r = await fetch('/echo/api/status');
|
|
if (r.ok) { clearInterval(check); location.reload(); }
|
|
} catch (e) { /* still restarting */ }
|
|
}, 1000);
|
|
}, 2000);
|
|
}
|
|
|
|
async function gitCommit() {
|
|
if (!confirm('Fac commit și push la toate modificările?')) return;
|
|
showToast('Se execută commit...', 'info');
|
|
try {
|
|
const response = await fetch('/echo/api/eco/git-commit', { method: 'POST' });
|
|
const text = await response.text();
|
|
let result;
|
|
try {
|
|
result = JSON.parse(text);
|
|
} catch {
|
|
showToast('Proxy timeout — verific status în 3s...', 'info');
|
|
setTimeout(refreshStatus, 3000);
|
|
return;
|
|
}
|
|
if (result.success) {
|
|
showToast('Commit reușit: ' + (result.files || 0) + ' fișiere', 'success');
|
|
setTimeout(refreshStatus, 1000);
|
|
} else {
|
|
showToast('Eroare: ' + (result.error || 'necunoscută'), 'error');
|
|
}
|
|
} catch (error) {
|
|
showToast('Eroare la commit: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
async function refreshStatus() {
|
|
showToast('Se reîmprospătează...');
|
|
await loadStatus();
|
|
showToast('Status actualizat!');
|
|
}
|
|
|
|
async function loadGitStatus() {
|
|
const badge = document.getElementById('gitBadge');
|
|
const subtitle = document.getElementById('gitSubtitle');
|
|
const details = document.getElementById('gitDetails');
|
|
try {
|
|
const response = await fetch('/echo/api/git?' + Date.now());
|
|
if (!response.ok) throw new Error('HTTP ' + response.status);
|
|
const git = await response.json();
|
|
|
|
if (git.clean) {
|
|
badge.textContent = 'curat';
|
|
badge.className = 'status-badge ok';
|
|
} else {
|
|
badge.textContent = git.uncommittedCount + ' modificări';
|
|
badge.className = 'status-badge warning';
|
|
}
|
|
|
|
subtitle.textContent = `${git.branch} · ${git.lastCommit.time}`;
|
|
|
|
const GITEA_URL = 'https://gitea.romfast.ro/romfast/echo-core';
|
|
let html = `
|
|
<div class="status-detail-item">
|
|
<i data-lucide="git-commit"></i>
|
|
<span><a href="${GITEA_URL}/commit/${git.lastCommit.hash}" target="_blank" style="color:var(--accent)">${git.lastCommit.hash}</a> ${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="status-detail-item uncommitted">
|
|
<i data-lucide="alert-circle"></i>
|
|
<span><a href="/echo/files.html?git=1" style="color:var(--warning)"><strong>${git.uncommittedCount}</strong> necomise</a>: ${files}${more}</span>
|
|
</div>`;
|
|
}
|
|
if (git.diffStat) {
|
|
html += `<div class="status-detail-item" style="font-family:var(--font-mono);white-space:pre-wrap;"><i data-lucide="bar-chart-3"></i><span>${escapeHtml(git.diffStat)}</span></div>`;
|
|
}
|
|
html += `<div class="status-detail-item">
|
|
<i data-lucide="external-link"></i>
|
|
<a href="${GITEA_URL}" target="_blank" style="color:var(--accent)">gitea.romfast.ro/romfast/echo-core</a>
|
|
</div>`;
|
|
|
|
details.innerHTML = html;
|
|
lucide.createIcons();
|
|
return git;
|
|
} catch (e) {
|
|
console.error('Git status error:', e);
|
|
badge.textContent = 'eroare';
|
|
badge.className = 'status-badge error';
|
|
subtitle.textContent = 'nu se poate încărca status-ul git';
|
|
details.innerHTML = `<div class="panel-error" role="alert"><i data-lucide="alert-triangle"></i><span>${escapeHtml(e.message || 'eroare necunoscută')}</span><button onclick="loadGitStatus()">Retry</button></div>`;
|
|
lucide.createIcons();
|
|
}
|
|
}
|
|
|
|
async function loadAnafStatus() {
|
|
try {
|
|
const response = await fetch('/echo/status.json?' + Date.now());
|
|
if (!response.ok) throw new Error('No status.json');
|
|
const status = await response.json();
|
|
|
|
if (status.anaf) {
|
|
const badge = document.getElementById('anafBadge');
|
|
badge.textContent = status.anaf.status || 'OK';
|
|
badge.className = 'status-badge ' + (status.anaf.ok !== false ? 'ok' : 'warning');
|
|
|
|
const subtitle = document.getElementById('anafSubtitle');
|
|
const lastCheck = status.anaf.lastCheck || '-';
|
|
const msg = status.anaf.ok !== false ? 'Nicio modificare' : (status.anaf.message || 'Modificări!');
|
|
subtitle.textContent = `${msg} · ${lastCheck}`;
|
|
|
|
// Actualizează detaliile
|
|
const details = document.getElementById('anafDetails');
|
|
let html = '';
|
|
|
|
// Adaugă detaliile modificărilor dacă există
|
|
if (status.anaf.changes && status.anaf.changes.length > 0) {
|
|
status.anaf.changes.forEach(change => {
|
|
const summaryText = change.summary && change.summary.length > 0
|
|
? ' - ' + change.summary.join(', ')
|
|
: '';
|
|
html += `
|
|
<div class="status-detail-item uncommitted">
|
|
<i data-lucide="alert-circle"></i>
|
|
<span><a href="${change.url}" target="_blank" style="color:var(--warning)">${change.name}</a>${summaryText}</span>
|
|
</div>
|
|
`;
|
|
});
|
|
} else {
|
|
html = `
|
|
<div class="status-detail-item">
|
|
<i data-lucide="check-circle"></i>
|
|
<span>Toate paginile sunt la zi</span>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
details.innerHTML = html;
|
|
lucide.createIcons();
|
|
}
|
|
|
|
return status;
|
|
} catch (e) {
|
|
console.log('No ANAF status');
|
|
}
|
|
}
|
|
|
|
async function loadCronStatus() {
|
|
try {
|
|
const response = await fetch('/echo/api/cron?' + Date.now());
|
|
if (!response.ok) throw new Error('API error');
|
|
const data = await response.json();
|
|
|
|
const jobs = data.jobs || [];
|
|
const ranToday = data.ranToday || 0;
|
|
|
|
// Update badge
|
|
const badge = document.getElementById('cronBadge');
|
|
badge.textContent = `${ranToday}/${jobs.length}`;
|
|
badge.className = 'status-badge ok';
|
|
|
|
// Update subtitle - find next job to run
|
|
const subtitle = document.getElementById('cronSubtitle');
|
|
const now = Date.now();
|
|
const pendingJobs = jobs.filter(j => !j.ranToday);
|
|
const nextJob = pendingJobs.length > 0 ? pendingJobs[0] : null;
|
|
subtitle.textContent = nextJob
|
|
? `Următorul: ${nextJob.time} ${nextJob.name}`
|
|
: 'Toate job-urile au rulat azi';
|
|
|
|
// Sort jobs: today first (by time), then other days (by date+time)
|
|
const today = new Date().toDateString();
|
|
jobs.sort((a, b) => {
|
|
const aDate = new Date(a.nextRunAtMs || 0);
|
|
const bDate = new Date(b.nextRunAtMs || 0);
|
|
const aIsToday = aDate.toDateString() === today;
|
|
const bIsToday = bDate.toDateString() === today;
|
|
|
|
// Today jobs come first
|
|
if (aIsToday && !bIsToday) return -1;
|
|
if (!aIsToday && bIsToday) return 1;
|
|
|
|
// Within same category, sort by time
|
|
return (a.nextRunAtMs || 0) - (b.nextRunAtMs || 0);
|
|
});
|
|
|
|
// Update details - compact grid layout
|
|
const details = document.getElementById('cronDetails');
|
|
const jobsHtml = jobs.map(job => {
|
|
const done = job.ranToday;
|
|
const failed = job.lastStatus === 'error';
|
|
const statusClass = failed ? 'failed' : (done ? 'done' : 'pending');
|
|
const icon = failed ? 'x-circle' : (done ? 'check-circle' : 'clock');
|
|
|
|
// Check if job is today
|
|
const jobDate = new Date(job.nextRunAtMs || 0);
|
|
const isToday = jobDate.toDateString() === today;
|
|
const dateStr = isToday ? '' : `<span class="cron-date">${jobDate.getDate()} ${jobDate.toLocaleDateString('ro-RO', { month: 'short' })}</span>`;
|
|
|
|
return `
|
|
<div class="cron-item ${statusClass}">
|
|
<i data-lucide="${icon}" class="cron-icon ${statusClass}"></i>
|
|
<span class="cron-time">${job.time}${dateStr}</span>
|
|
<span class="cron-name">${job.name}</span>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
details.innerHTML = `<div class="cron-list">${jobsHtml}</div>`;
|
|
|
|
lucide.createIcons();
|
|
} catch (e) {
|
|
console.error('Error loading cron status:', e);
|
|
const badge = document.getElementById('cronBadge');
|
|
badge.textContent = 'eroare';
|
|
badge.className = 'status-badge error';
|
|
}
|
|
}
|
|
|
|
function updateStatusSummary() {
|
|
const gitBadge = document.getElementById('gitBadge');
|
|
const anafBadge = document.getElementById('anafBadge');
|
|
const cronBadge = document.getElementById('cronBadge');
|
|
|
|
const summary = document.getElementById('statusSummary');
|
|
summary.textContent = `Git: ${gitBadge?.textContent || '-'} · ANAF: ${anafBadge?.textContent || '-'} · Cron: ${cronBadge?.textContent || '-'}`;
|
|
}
|
|
|
|
// Auto-refresh on page focus
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (document.visibilityState === 'visible') {
|
|
loadStatus();
|
|
}
|
|
});
|
|
|
|
// Load data
|
|
// ===== TODO'S =====
|
|
let todosData = { items: [] };
|
|
|
|
async function loadTodos() {
|
|
try {
|
|
const response = await fetch('/echo/todos.json?' + Date.now());
|
|
if (response.ok) {
|
|
todosData = await response.json();
|
|
renderTodos();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading todos:', error);
|
|
}
|
|
}
|
|
|
|
function renderTodos() {
|
|
const container = document.getElementById('todosBody');
|
|
const today = new Date().toISOString().split('T')[0];
|
|
const tomorrow = new Date(Date.now() + 86400000).toISOString().split('T')[0];
|
|
|
|
// Group by status
|
|
const todayItems = todosData.items.filter(t => !t.done && t.dueDate === today);
|
|
const tomorrowItems = todosData.items.filter(t => !t.done && t.dueDate === tomorrow);
|
|
const overdueItems = todosData.items.filter(t => !t.done && t.dueDate < today);
|
|
const futureItems = todosData.items.filter(t => !t.done && t.dueDate > tomorrow);
|
|
const doneToday = todosData.items.filter(t => t.done && t.doneAt && t.doneAt.startsWith(today));
|
|
|
|
// Update count
|
|
const activeCount = todosData.items.filter(t => !t.done).length;
|
|
document.getElementById('todosCount').textContent = activeCount;
|
|
|
|
let html = '';
|
|
|
|
if (overdueItems.length > 0) {
|
|
html += '<div class="todo-section"><div class="todo-section-title overdue">⚠️ RESTANTE</div>';
|
|
html += overdueItems.map(t => renderTodoItem(t)).join('');
|
|
html += '</div>';
|
|
}
|
|
|
|
if (todayItems.length > 0) {
|
|
html += '<div class="todo-section"><div class="todo-section-title">📅 AZI</div>';
|
|
html += todayItems.map(t => renderTodoItem(t)).join('');
|
|
html += '</div>';
|
|
}
|
|
|
|
if (tomorrowItems.length > 0) {
|
|
html += '<div class="todo-section"><div class="todo-section-title">📅 MÂINE</div>';
|
|
html += tomorrowItems.map(t => renderTodoItem(t)).join('');
|
|
html += '</div>';
|
|
}
|
|
|
|
if (futureItems.length > 0) {
|
|
html += '<div class="todo-section"><div class="todo-section-title">📆 VIITOR</div>';
|
|
html += futureItems.map(t => renderTodoItem(t)).join('');
|
|
html += '</div>';
|
|
}
|
|
|
|
if (doneToday.length > 0) {
|
|
html += '<div class="todo-section"><div class="todo-section-title">✅ COMPLETATE AZI</div>';
|
|
html += doneToday.map(t => renderTodoItem(t)).join('');
|
|
html += '</div>';
|
|
}
|
|
|
|
if (html === '') {
|
|
html = '<div class="empty-state"><i data-lucide="check-circle"></i><p>Niciun todo activ</p></div>';
|
|
}
|
|
|
|
container.innerHTML = html;
|
|
lucide.createIcons();
|
|
}
|
|
|
|
function renderTodoItem(todo) {
|
|
const isOverdue = !todo.done && todo.dueDate < new Date().toISOString().split('T')[0];
|
|
return `
|
|
<div class="todo-item ${todo.done ? 'done' : ''}" data-id="${todo.id}" onclick="editTodo('${todo.id}')">
|
|
<div class="todo-checkbox ${todo.done ? 'checked' : ''}" onclick="event.stopPropagation(); toggleTodo('${todo.id}')">
|
|
<i data-lucide="check"></i>
|
|
</div>
|
|
<div class="todo-content">
|
|
<div class="todo-text">${todo.text}</div>
|
|
${todo.context ? `<div class="todo-context"><strong>Context:</strong> ${todo.context}</div>` : ''}
|
|
${todo.example ? `<div class="todo-example"><strong>Exemplu:</strong> ${todo.example}</div>` : ''}
|
|
<div class="todo-meta">
|
|
<span class="todo-domain ${todo.domain}">@${todo.domain}</span>
|
|
${todo.dueDate ? `<span class="todo-due ${isOverdue ? 'overdue' : ''}">${formatDate(todo.dueDate)}</span>` : ''}
|
|
${todo.source ? (todo.sourceUrl
|
|
? `<a href="javascript:void(0)" class="todo-source-link" onclick="event.stopPropagation(); openNoteOverlay('${todo.sourceUrl}')">${todo.source}</a>`
|
|
: `<span class="todo-source">${todo.source}</span>`) : ''}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function formatDate(dateStr) {
|
|
const date = new Date(dateStr);
|
|
const today = new Date();
|
|
const tomorrow = new Date(Date.now() + 86400000);
|
|
|
|
if (dateStr === today.toISOString().split('T')[0]) return 'Azi';
|
|
if (dateStr === tomorrow.toISOString().split('T')[0]) return 'Mâine';
|
|
|
|
return date.toLocaleDateString('ro-RO', { day: 'numeric', month: 'short' });
|
|
}
|
|
|
|
async function toggleTodo(id) {
|
|
const todo = todosData.items.find(t => t.id === id);
|
|
if (todo) {
|
|
todo.done = !todo.done;
|
|
todo.doneAt = todo.done ? new Date().toISOString() : null;
|
|
await saveTodos();
|
|
renderTodos();
|
|
}
|
|
}
|
|
|
|
async function saveTodos() {
|
|
todosData.lastUpdated = new Date().toISOString();
|
|
try {
|
|
const response = await fetch('/echo/api/files', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
path: 'dashboard/todos.json',
|
|
content: JSON.stringify(todosData, null, 2)
|
|
})
|
|
});
|
|
if (!response.ok) throw new Error('Save failed');
|
|
} catch (error) {
|
|
showToast('Eroare la salvare', 'error');
|
|
}
|
|
}
|
|
|
|
function openNoteOverlay(url) {
|
|
// Convert files.html URL to the actual file path for iframe
|
|
const overlay = document.getElementById('noteOverlay');
|
|
const frame = document.getElementById('noteOverlayFrame');
|
|
const title = document.getElementById('noteOverlayTitle');
|
|
|
|
// Extract filename from URL
|
|
const match = url.match(/#(.+)$/);
|
|
if (match) {
|
|
const path = match[1];
|
|
const filename = path.split('/').pop().replace('.md', '');
|
|
title.textContent = filename;
|
|
}
|
|
|
|
frame.src = url;
|
|
overlay.classList.add('active');
|
|
}
|
|
|
|
function closeNoteOverlay() {
|
|
const overlay = document.getElementById('noteOverlay');
|
|
const frame = document.getElementById('noteOverlayFrame');
|
|
overlay.classList.remove('active');
|
|
frame.src = '';
|
|
}
|
|
|
|
function showAddTodoModal() {
|
|
document.getElementById('todoModalTitle').textContent = 'Todo nou';
|
|
document.getElementById('todoEditId').value = '';
|
|
document.getElementById('todoText').value = '';
|
|
document.getElementById('todoContext').value = '';
|
|
document.getElementById('todoExample').value = '';
|
|
document.getElementById('todoDomain').value = 'work';
|
|
document.getElementById('todoDueDate').value = new Date().toISOString().split('T')[0];
|
|
document.getElementById('todoSource').value = '';
|
|
document.getElementById('todoSourceUrl').value = '';
|
|
document.getElementById('todoDeleteBtn').style.display = 'none';
|
|
document.getElementById('todoSaveBtn').textContent = 'Adaugă';
|
|
document.getElementById('todoModal').classList.add('active');
|
|
document.getElementById('todoText').focus();
|
|
}
|
|
|
|
function editTodo(id) {
|
|
const todo = todosData.items.find(t => t.id === id);
|
|
if (!todo) return;
|
|
|
|
document.getElementById('todoModalTitle').textContent = 'Editare todo';
|
|
document.getElementById('todoEditId').value = id;
|
|
document.getElementById('todoText').value = todo.text || '';
|
|
document.getElementById('todoContext').value = todo.context || '';
|
|
document.getElementById('todoExample').value = todo.example || '';
|
|
document.getElementById('todoDomain').value = todo.domain || 'work';
|
|
document.getElementById('todoDueDate').value = todo.dueDate || '';
|
|
document.getElementById('todoSource').value = todo.source || '';
|
|
document.getElementById('todoSourceUrl').value = todo.sourceUrl || '';
|
|
document.getElementById('todoDeleteBtn').style.display = 'block';
|
|
document.getElementById('todoSaveBtn').textContent = 'Salvează';
|
|
document.getElementById('todoModal').classList.add('active');
|
|
document.getElementById('todoText').focus();
|
|
}
|
|
|
|
function hideTodoModal() {
|
|
document.getElementById('todoModal').classList.remove('active');
|
|
}
|
|
|
|
async function saveTodoForm() {
|
|
const text = document.getElementById('todoText').value.trim();
|
|
if (!text) {
|
|
showToast('Textul este obligatoriu', 'error');
|
|
return;
|
|
}
|
|
|
|
const editId = document.getElementById('todoEditId').value;
|
|
|
|
if (editId) {
|
|
// Edit existing
|
|
const todo = todosData.items.find(t => t.id === editId);
|
|
if (todo) {
|
|
todo.text = text;
|
|
todo.context = document.getElementById('todoContext').value.trim() || null;
|
|
todo.example = document.getElementById('todoExample').value.trim() || null;
|
|
todo.domain = document.getElementById('todoDomain').value;
|
|
todo.dueDate = document.getElementById('todoDueDate').value || null;
|
|
todo.source = document.getElementById('todoSource').value.trim() || null;
|
|
todo.sourceUrl = document.getElementById('todoSourceUrl').value.trim() || null;
|
|
todo.updatedAt = new Date().toISOString();
|
|
}
|
|
showToast('Todo actualizat');
|
|
} else {
|
|
// Add new
|
|
const todo = {
|
|
id: 'todo-' + Date.now(),
|
|
text: text,
|
|
context: document.getElementById('todoContext').value.trim() || null,
|
|
example: document.getElementById('todoExample').value.trim() || null,
|
|
domain: document.getElementById('todoDomain').value,
|
|
dueDate: document.getElementById('todoDueDate').value || new Date().toISOString().split('T')[0],
|
|
done: false,
|
|
doneAt: null,
|
|
source: document.getElementById('todoSource').value.trim() || 'manual',
|
|
sourceUrl: document.getElementById('todoSourceUrl').value.trim() || null,
|
|
createdAt: new Date().toISOString()
|
|
};
|
|
todosData.items.push(todo);
|
|
showToast('Todo adăugat');
|
|
}
|
|
|
|
hideTodoModal();
|
|
await saveTodos();
|
|
renderTodos();
|
|
}
|
|
|
|
async function deleteTodo() {
|
|
const editId = document.getElementById('todoEditId').value;
|
|
if (!editId) return;
|
|
|
|
if (!confirm('Sigur ștergi acest todo?')) return;
|
|
|
|
todosData.items = todosData.items.filter(t => t.id !== editId);
|
|
hideTodoModal();
|
|
await saveTodos();
|
|
renderTodos();
|
|
showToast('Todo șters');
|
|
}
|
|
|
|
// ===== ISSUES =====
|
|
async function loadIssues() {
|
|
try {
|
|
const response = await fetch('/echo/issues.json?' + Date.now());
|
|
issuesData = await response.json();
|
|
populateProgramSelect();
|
|
renderIssues();
|
|
updateIssuesCount();
|
|
} catch (error) {
|
|
console.error('Error loading issues:', error);
|
|
document.getElementById('issuesBody').innerHTML = `
|
|
<div class="empty-state">
|
|
<i data-lucide="alert-circle"></i>
|
|
<p>Eroare la încărcare</p>
|
|
</div>
|
|
`;
|
|
lucide.createIcons();
|
|
}
|
|
}
|
|
|
|
function updateIssuesCount() {
|
|
if (!issuesData) return;
|
|
const todoCount = issuesData.issues.filter(i => i.status !== 'done').length;
|
|
document.getElementById('issuesCount').textContent = todoCount;
|
|
}
|
|
|
|
function renderIssues() {
|
|
const body = document.getElementById('issuesBody');
|
|
|
|
if (!issuesData || issuesData.issues.length === 0) {
|
|
body.innerHTML = `
|
|
<div class="empty-state">
|
|
<i data-lucide="check-circle"></i>
|
|
<p>Niciun issue.</p>
|
|
</div>
|
|
`;
|
|
lucide.createIcons();
|
|
return;
|
|
}
|
|
|
|
// Filter issues
|
|
let filtered = issuesData.issues;
|
|
if (currentFilter !== 'all') {
|
|
filtered = filtered.filter(i => i.owner === currentFilter);
|
|
}
|
|
|
|
// Separate done from active
|
|
const activeIssues = filtered.filter(i => i.status !== 'done');
|
|
const doneIssues = filtered.filter(i => i.status === 'done');
|
|
|
|
// Group active by priority
|
|
const grouped = {};
|
|
priorityOrder.forEach(p => grouped[p] = []);
|
|
|
|
activeIssues.forEach(issue => {
|
|
const p = issue.priority || 'backlog';
|
|
if (grouped[p]) grouped[p].push(issue);
|
|
else grouped['backlog'].push(issue);
|
|
});
|
|
|
|
// Sort each group by date (newest first)
|
|
Object.keys(grouped).forEach(p => {
|
|
grouped[p].sort((a, b) => new Date(b.created) - new Date(a.created));
|
|
});
|
|
|
|
let html = '';
|
|
|
|
// Render active issues by priority
|
|
priorityOrder.forEach(priority => {
|
|
const issues = grouped[priority];
|
|
if (issues.length === 0) return;
|
|
|
|
const isCollapsed = collapsedPriorities.has(priority);
|
|
|
|
html += `
|
|
<div class="priority-group">
|
|
<div class="priority-header ${isCollapsed ? 'collapsed' : ''}" onclick="togglePriority('${priority}')">
|
|
<span>${priorityLabels[priority]}</span>
|
|
<span style="margin-left: auto; opacity: 0.7; font-size: var(--text-xs);">${issues.length}</span>
|
|
</div>
|
|
<div class="priority-content ${isCollapsed ? 'hidden' : ''}">
|
|
${issues.map(issue => renderIssueItem(issue)).join('')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
// Render done issues separately at the end
|
|
if (doneIssues.length > 0) {
|
|
const isDoneCollapsed = collapsedPriorities.has('done');
|
|
doneIssues.sort((a, b) => new Date(b.completed || b.updated) - new Date(a.completed || a.updated));
|
|
|
|
html += `
|
|
<div class="priority-group">
|
|
<div class="priority-header ${isDoneCollapsed ? 'collapsed' : ''}" onclick="togglePriority('done')">
|
|
<span>Executate</span>
|
|
<span style="margin-left: auto; opacity: 0.7; font-size: var(--text-xs);">${doneIssues.length}</span>
|
|
</div>
|
|
<div class="priority-content ${isDoneCollapsed ? 'hidden' : ''}">
|
|
${doneIssues.map(issue => renderIssueItem(issue)).join('')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
body.innerHTML = html;
|
|
lucide.createIcons();
|
|
}
|
|
|
|
function renderIssueItem(issue) {
|
|
const isDone = issue.status === 'done';
|
|
const isInProgress = issue.status === 'in-progress';
|
|
const ownerIcons = { 'clawdbot': '🤖', 'robert': '👷', 'marius': '👤' };
|
|
const ownerIcon = ownerIcons[issue.owner] || '👤';
|
|
const dateStr = new Date(issue.created).toLocaleDateString('ro-RO', { day: 'numeric', month: 'short' });
|
|
const statusLabels = { 'todo': 'Todo', 'in-progress': 'In Progress', 'done': 'Done' };
|
|
const statusLabel = statusLabels[issue.status] || 'Todo';
|
|
const checkboxClass = isDone ? 'checked' : (isInProgress ? 'in-progress' : '');
|
|
|
|
return `
|
|
<div class="issue-item ${isDone ? 'done' : ''}" data-id="${issue.id}">
|
|
<div class="issue-checkbox ${checkboxClass}" onclick="toggleIssue('${issue.id}')" title="Click pentru a schimba statusul">
|
|
<i data-lucide="check"></i>
|
|
</div>
|
|
<div class="issue-content" onclick="editIssue('${issue.id}')">
|
|
<div class="issue-title">${issue.title}</div>
|
|
<div class="issue-meta">
|
|
<span class="issue-status ${issue.status || 'todo'}">${statusLabel}</span>
|
|
${issue.program ? `<span class="issue-tag program">${issue.program}</span>` : ''}
|
|
<span class="issue-owner ${issue.owner}">${ownerIcon} ${issue.owner === 'clawdbot' ? 'Clawdbot' : (issue.owner === 'robert' ? 'Robert' : 'Marius')}</span>
|
|
<span class="issue-date">${dateStr}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function togglePriority(priority) {
|
|
if (collapsedPriorities.has(priority)) {
|
|
collapsedPriorities.delete(priority);
|
|
} else {
|
|
collapsedPriorities.add(priority);
|
|
}
|
|
renderIssues();
|
|
}
|
|
|
|
async function toggleIssue(id) {
|
|
const issue = issuesData.issues.find(i => i.id === id);
|
|
if (!issue) return;
|
|
|
|
// Cycle: todo → in-progress → done → todo
|
|
const statusCycle = { 'todo': 'in-progress', 'in-progress': 'done', 'done': 'todo' };
|
|
const currentStatus = issue.status || 'todo';
|
|
issue.status = statusCycle[currentStatus] || 'in-progress';
|
|
|
|
if (issue.status === 'done') {
|
|
issue.completed = new Date().toISOString();
|
|
} else {
|
|
delete issue.completed;
|
|
}
|
|
|
|
const statusMessages = {
|
|
'in-progress': '🔄 In Progress',
|
|
'done': '✅ Done!',
|
|
'todo': '📋 Todo'
|
|
};
|
|
|
|
renderIssues();
|
|
updateIssuesCount();
|
|
await saveIssues();
|
|
showToast(statusMessages[issue.status]);
|
|
}
|
|
|
|
// Filters
|
|
document.getElementById('issuesFilters').addEventListener('click', (e) => {
|
|
if (e.target.classList.contains('filter-btn')) {
|
|
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
|
e.target.classList.add('active');
|
|
currentFilter = e.target.dataset.filter;
|
|
renderIssues();
|
|
}
|
|
});
|
|
|
|
// Issue Modal
|
|
function showAddModal() {
|
|
document.getElementById('issueModalTitle').textContent = 'Issue nou';
|
|
document.getElementById('issueEditId').value = '';
|
|
document.getElementById('issueTitle').value = '';
|
|
document.getElementById('issueDesc').value = '';
|
|
document.getElementById('issueProgram').value = '';
|
|
document.getElementById('issueOwner').value = 'marius';
|
|
document.getElementById('issuePriority').value = 'urgent-important';
|
|
document.getElementById('issueStatus').value = 'todo';
|
|
document.getElementById('issueDeadline').value = '';
|
|
document.getElementById('issueDeleteBtn').style.display = 'none';
|
|
document.getElementById('issueSaveBtn').textContent = 'Adaugă';
|
|
document.getElementById('issueModal').classList.add('active');
|
|
document.getElementById('issueTitle').focus();
|
|
}
|
|
|
|
function editIssue(id) {
|
|
const issue = issuesData.issues.find(i => i.id === id);
|
|
if (!issue) return;
|
|
|
|
document.getElementById('issueModalTitle').textContent = 'Editare issue';
|
|
document.getElementById('issueEditId').value = id;
|
|
document.getElementById('issueTitle').value = issue.title || '';
|
|
document.getElementById('issueDesc').value = issue.description || '';
|
|
document.getElementById('issueProgram').value = issue.program || '';
|
|
document.getElementById('issueOwner').value = issue.owner || 'marius';
|
|
document.getElementById('issuePriority').value = issue.priority || 'backlog';
|
|
document.getElementById('issueStatus').value = issue.status || 'todo';
|
|
document.getElementById('issueDeadline').value = issue.deadline || '';
|
|
document.getElementById('issueDeleteBtn').style.display = 'block';
|
|
document.getElementById('issueSaveBtn').textContent = 'Salvează';
|
|
document.getElementById('issueModal').classList.add('active');
|
|
document.getElementById('issueTitle').focus();
|
|
}
|
|
|
|
function hideIssueModal() {
|
|
document.getElementById('issueModal').classList.remove('active');
|
|
}
|
|
|
|
// Legacy alias
|
|
function hideAddModal() { hideIssueModal(); }
|
|
|
|
function populateProgramSelect() {
|
|
const select = document.getElementById('issueProgram');
|
|
select.innerHTML = '<option value="">— Selectează —</option>';
|
|
if (issuesData && issuesData.programs) {
|
|
issuesData.programs.forEach(p => {
|
|
select.innerHTML += `<option value="${p}">${p}</option>`;
|
|
});
|
|
}
|
|
}
|
|
|
|
async function saveIssue() {
|
|
const title = document.getElementById('issueTitle').value.trim();
|
|
if (!title) {
|
|
showToast('Titlul este obligatoriu', 'error');
|
|
return;
|
|
}
|
|
|
|
const editId = document.getElementById('issueEditId').value;
|
|
|
|
if (editId) {
|
|
// Edit existing
|
|
const issue = issuesData.issues.find(i => i.id === editId);
|
|
if (issue) {
|
|
issue.title = title;
|
|
issue.description = document.getElementById('issueDesc').value.trim();
|
|
issue.program = document.getElementById('issueProgram').value;
|
|
issue.owner = document.getElementById('issueOwner').value;
|
|
issue.priority = document.getElementById('issuePriority').value;
|
|
const newStatus = document.getElementById('issueStatus').value;
|
|
if (newStatus === 'done' && issue.status !== 'done') {
|
|
issue.completed = new Date().toISOString();
|
|
} else if (newStatus !== 'done') {
|
|
delete issue.completed;
|
|
}
|
|
issue.status = newStatus;
|
|
issue.deadline = document.getElementById('issueDeadline').value || null;
|
|
issue.updated = new Date().toISOString();
|
|
}
|
|
showToast('Issue actualizat!');
|
|
} else {
|
|
// Add new
|
|
const newIssue = {
|
|
id: 'ROA-' + String(issuesData.issues.length + 1).padStart(3, '0'),
|
|
title: title,
|
|
description: document.getElementById('issueDesc').value.trim(),
|
|
program: document.getElementById('issueProgram').value,
|
|
owner: document.getElementById('issueOwner').value,
|
|
priority: document.getElementById('issuePriority').value,
|
|
status: document.getElementById('issueStatus').value || 'todo',
|
|
created: new Date().toISOString(),
|
|
deadline: document.getElementById('issueDeadline').value || null
|
|
};
|
|
issuesData.issues.unshift(newIssue);
|
|
showToast('Issue adăugat!');
|
|
}
|
|
|
|
hideIssueModal();
|
|
renderIssues();
|
|
updateIssuesCount();
|
|
await saveIssues();
|
|
}
|
|
|
|
async function deleteIssue() {
|
|
const editId = document.getElementById('issueEditId').value;
|
|
if (!editId) return;
|
|
|
|
if (!confirm('Sigur ștergi acest issue?')) return;
|
|
|
|
issuesData.issues = issuesData.issues.filter(i => i.id !== editId);
|
|
hideIssueModal();
|
|
renderIssues();
|
|
updateIssuesCount();
|
|
await saveIssues();
|
|
showToast('Issue șters');
|
|
}
|
|
|
|
// Legacy alias
|
|
async function addIssue() { await saveIssue(); }
|
|
|
|
async function saveIssues() {
|
|
issuesData.lastUpdated = new Date().toISOString();
|
|
try {
|
|
const response = await fetch('/echo/api/files', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
path: 'dashboard/issues.json',
|
|
content: JSON.stringify(issuesData, null, 2)
|
|
})
|
|
});
|
|
if (!response.ok) throw new Error('Save failed');
|
|
} catch (error) {
|
|
showToast('Eroare la salvare', 'error');
|
|
}
|
|
}
|
|
|
|
function showToast(message, type = 'success') {
|
|
const toast = document.getElementById('toast');
|
|
toast.textContent = message;
|
|
toast.className = 'toast show';
|
|
setTimeout(() => toast.classList.remove('show'), 3000);
|
|
}
|
|
|
|
// Close modals on outside click
|
|
document.getElementById('issueModal').addEventListener('click', (e) => {
|
|
if (e.target.id === 'issueModal') hideIssueModal();
|
|
});
|
|
document.getElementById('todoModal').addEventListener('click', (e) => {
|
|
if (e.target.id === 'todoModal') hideTodoModal();
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────────
|
|
// Eco panel functions (merged from eco.html)
|
|
// ─────────────────────────────────────────────────────────
|
|
|
|
let servicesRefreshInterval = null;
|
|
let logRefreshInterval = null;
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text == null ? '' : String(text);
|
|
return div.innerHTML;
|
|
}
|
|
|
|
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('-', ' ');
|
|
}
|
|
|
|
// ── Services ────────────────────────────────────────────
|
|
|
|
async function loadServices() {
|
|
const grid = document.getElementById('servicesGrid');
|
|
try {
|
|
const res = await fetch('/echo/api/eco/status');
|
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
const data = await res.json();
|
|
if (data.error) throw new Error(data.error);
|
|
if (!data.services || data.services.length === 0) {
|
|
grid.innerHTML = '<div class="empty-state-warm">Niciun serviciu detectat. Adaugă unul în <code>ECO_SERVICES</code> din <code>dashboard/constants.py</code>.</div>';
|
|
return;
|
|
}
|
|
renderServices(data.services);
|
|
} catch (e) {
|
|
grid.innerHTML = `<div class="panel-error" role="alert"><i data-lucide="alert-triangle"></i><span>Nu am putut încărca services: ${escapeHtml(e.message)}</span><button onclick="loadServices()">Retry</button></div>`;
|
|
lucide.createIcons();
|
|
}
|
|
}
|
|
|
|
function renderServices(services) {
|
|
const grid = document.getElementById('servicesGrid');
|
|
|
|
// Update the Status sub-section header (badge + subtitle).
|
|
const badge = document.getElementById('servicesBadge');
|
|
const subtitle = document.getElementById('servicesSubtitle');
|
|
if (badge && subtitle) {
|
|
const total = services.length;
|
|
const active = services.filter(s => s.active).length;
|
|
badge.textContent = `${active}/${total}`;
|
|
badge.className = active === total ? 'status-badge ok' : 'status-badge warning';
|
|
const stopped = services.filter(s => !s.active).map(s => svcLabel(s.name));
|
|
subtitle.textContent = stopped.length === 0
|
|
? 'Toate servicile rulează'
|
|
: `Oprite: ${stopped.join(', ')}`;
|
|
}
|
|
|
|
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}')" aria-label="Restart ${svcLabel(svc.name)}">
|
|
<i data-lucide="rotate-cw"></i> Restart
|
|
</button>
|
|
<button class="btn btn-secondary btn-danger" onclick="stopService('${svc.name}')" aria-label="Stop ${svcLabel(svc.name)}">
|
|
<i data-lucide="square"></i> Stop
|
|
</button>
|
|
`;
|
|
} else {
|
|
actionsHtml = `
|
|
<button class="btn btn-primary" onclick="restartService('${svc.name}')" aria-label="Start ${svcLabel(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'}" aria-hidden="true"></span>
|
|
${escapeHtml(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: ${escapeHtml(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(loadServices, 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(loadServices, 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() {
|
|
const container = document.getElementById('sessionsContent');
|
|
try {
|
|
const res = await fetch('/echo/api/eco/sessions');
|
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
const data = await res.json();
|
|
sessionsCache = data.sessions || [];
|
|
renderSessions(sessionsCache);
|
|
} catch (e) {
|
|
container.innerHTML = `<div class="panel-error" role="alert"><i data-lucide="alert-triangle"></i><span>Nu am putut încărca sessions: ${escapeHtml(e.message)}</span><button onclick="loadSessions()">Retry</button></div>`;
|
|
lucide.createIcons();
|
|
}
|
|
}
|
|
|
|
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');
|
|
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);">${escapeHtml(e.message)}</div>`;
|
|
}
|
|
}
|
|
|
|
function renderSessions(sessions) {
|
|
const container = document.getElementById('sessionsContent');
|
|
const clearBtn = document.getElementById('clearAllBtn');
|
|
|
|
// Update the Status sub-section header (badge + subtitle).
|
|
const badge = document.getElementById('sessionsBadge');
|
|
const subtitle = document.getElementById('sessionsSubtitle');
|
|
if (badge && subtitle) {
|
|
const count = sessions ? sessions.length : 0;
|
|
badge.textContent = String(count);
|
|
badge.className = 'status-badge ok';
|
|
if (count === 0) {
|
|
subtitle.textContent = 'Nicio sesiune activă';
|
|
} else {
|
|
const platforms = [...new Set(sessions.map(s => s.platform || 'unknown'))];
|
|
subtitle.textContent = platforms.join(', ');
|
|
}
|
|
}
|
|
|
|
if (!sessions || sessions.length === 0) {
|
|
container.innerHTML = '<div class="empty-state-warm">Nicio sesiune activă. Un DM către Echo pornește una.</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}" aria-hidden="true">${ic}</div>
|
|
<span class="s-name">${escapeHtml(nm)}</span>
|
|
${tag}
|
|
<div class="s-stats">
|
|
<span>${s.message_count||0}msg</span>
|
|
<span>${escapeHtml(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" aria-hidden="true"><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>${escapeHtml(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||'')}')" aria-label="Clear session">
|
|
<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 (lazy-loaded) ──────────────────────────────────
|
|
|
|
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}`);
|
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
const data = await res.json();
|
|
const arr = data.lines || [];
|
|
if (arr.length === 0) {
|
|
content.textContent = 'Nu există log-uri pentru ultimele ' + lines + ' linii. Dacă serviciul a fost restartat recent, logarea începe de la zero.';
|
|
} else {
|
|
content.textContent = arr.join('\n');
|
|
content.scrollTop = content.scrollHeight;
|
|
}
|
|
} catch (e) {
|
|
content.textContent = 'Eroare: ' + 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() {
|
|
// Expand both the parent Status bar and the Doctor sub-section so the user sees results.
|
|
document.getElementById('statusBar').classList.remove('collapsed');
|
|
const doctorSection = document.getElementById('doctorSection');
|
|
if (doctorSection.classList.contains('collapsed')) {
|
|
toggleStatusSection('doctorSection');
|
|
}
|
|
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');
|
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
const data = await res.json();
|
|
list.innerHTML = (data.checks || []).map(c => `
|
|
<li class="check-item">
|
|
<span class="check-icon ${c.pass ? 'pass' : 'fail'}" aria-hidden="true">
|
|
${c.pass ? '✓' : '✗'}
|
|
</span>
|
|
<span class="check-name">${escapeHtml(c.name)}</span>
|
|
<span class="check-detail">${escapeHtml(c.detail)}</span>
|
|
</li>
|
|
`).join('') || '<li class="check-item">No checks returned</li>';
|
|
} catch (e) {
|
|
list.innerHTML = `<li class="check-item" style="color:var(--error);" role="alert">Error: ${escapeHtml(e.message)}</li>`;
|
|
}
|
|
|
|
btn.disabled = false;
|
|
}
|
|
|
|
async function ecoGitCommit() {
|
|
if (!confirm('Fac commit și push la toate modificările echo-core?')) return;
|
|
try {
|
|
const res = await fetch('/echo/api/eco/git-commit', { method: 'POST' });
|
|
const text = await res.text();
|
|
let result;
|
|
try {
|
|
result = JSON.parse(text);
|
|
} catch {
|
|
showToast('Proxy timeout — verific status în 3s...', 'info');
|
|
setTimeout(loadGitStatus, 3000);
|
|
return;
|
|
}
|
|
if (result.success) {
|
|
showToast('Commit reușit: ' + (result.files || 0) + ' fișiere', 'success');
|
|
setTimeout(loadGitStatus, 1000);
|
|
} else {
|
|
showToast('Eroare: ' + (result.error || 'necunoscută'), 'error');
|
|
}
|
|
} catch (e) {
|
|
showToast('Eroare la commit: ' + e.message, 'error');
|
|
}
|
|
}
|
|
|
|
// Init
|
|
initStatusSections();
|
|
loadStatus();
|
|
loadIssues();
|
|
loadTodos();
|
|
loadServices();
|
|
loadSessions();
|
|
// Auto-refresh services every 10s (sessions/git refresh manually).
|
|
servicesRefreshInterval = setInterval(loadServices, 10000);
|
|
</script>
|
|
</body>
|
|
</html>
|