2066 lines
73 KiB
HTML
2066 lines
73 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="favicon.svg">
|
|
<title>Echo · Dashboard</title>
|
|
<link rel="stylesheet" href="common.css">
|
|
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
|
<script src="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.agents {
|
|
background: rgba(168, 85, 247, 0.15);
|
|
color: #a855f7;
|
|
}
|
|
|
|
.agents-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
|
gap: var(--space-2);
|
|
}
|
|
|
|
.agent-chip {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-2);
|
|
padding: var(--space-2) var(--space-3);
|
|
background: var(--bg-elevated);
|
|
border-radius: var(--radius-md);
|
|
font-size: var(--text-xs);
|
|
}
|
|
|
|
.agent-chip .emoji { font-size: 14px; }
|
|
.agent-chip .name { font-weight: 500; color: var(--text-primary); }
|
|
.agent-chip .status { color: var(--text-muted); }
|
|
.agent-chip.active { background: rgba(34, 197, 94, 0.1); border: 1px solid rgba(34, 197, 94, 0.3); }
|
|
.agent-chip.active .status { color: #22c55e; }
|
|
|
|
.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: var(--text-xs);
|
|
color: var(--text-muted);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.status-badge {
|
|
padding: 2px 8px;
|
|
border-radius: var(--radius-sm);
|
|
font-size: var(--text-xs);
|
|
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: var(--text-xs);
|
|
color: var(--text-secondary);
|
|
padding: var(--space-1) 0;
|
|
}
|
|
|
|
.status-detail-item svg {
|
|
width: 12px;
|
|
height: 12px;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.status-detail-item.uncommitted {
|
|
color: #f97316;
|
|
}
|
|
|
|
.status-detail-item code {
|
|
font-family: monospace;
|
|
background: var(--bg-elevated);
|
|
padding: 1px 4px;
|
|
border-radius: 2px;
|
|
font-size: 11px;
|
|
}
|
|
|
|
/* Cron items */
|
|
.cron-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-2);
|
|
font-size: var(--text-xs);
|
|
padding: var(--space-1) 0;
|
|
}
|
|
|
|
.cron-item.done {
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.cron-item.done .cron-name {
|
|
text-decoration: line-through;
|
|
}
|
|
|
|
.cron-item.pending {
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.cron-time {
|
|
font-family: monospace;
|
|
min-width: 45px;
|
|
}
|
|
|
|
.cron-icon {
|
|
width: 14px;
|
|
height: 14px;
|
|
}
|
|
|
|
.cron-icon.done { color: #22c55e; }
|
|
.cron-icon.pending { color: var(--text-muted); }
|
|
.cron-icon.failed { color: #ef4444; }
|
|
|
|
/* 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-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;
|
|
}
|
|
|
|
/* Activity panel */
|
|
.activity-panel .panel-header {
|
|
background: linear-gradient(135deg, rgba(99, 102, 241, 0.15), rgba(139, 92, 246, 0.1));
|
|
}
|
|
|
|
.activity-panel .panel-title svg {
|
|
color: #8b5cf6;
|
|
}
|
|
|
|
.activity-section {
|
|
margin-bottom: var(--space-4);
|
|
}
|
|
|
|
.activity-section-title {
|
|
font-size: var(--text-xs);
|
|
font-weight: 600;
|
|
color: var(--text-muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
margin-bottom: var(--space-2);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-2);
|
|
}
|
|
|
|
.activity-item {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: var(--space-3);
|
|
padding: var(--space-2) var(--space-3);
|
|
border-radius: var(--radius-md);
|
|
margin-bottom: var(--space-1);
|
|
transition: background var(--transition-fast);
|
|
}
|
|
|
|
.activity-item:hover {
|
|
background: var(--bg-elevated);
|
|
}
|
|
|
|
.activity-icon {
|
|
width: 24px;
|
|
height: 24px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.activity-icon.done, .activity-icon.task {
|
|
background: rgba(34, 197, 94, 0.2);
|
|
color: #22c55e;
|
|
}
|
|
|
|
.activity-icon.running {
|
|
background: rgba(59, 130, 246, 0.2);
|
|
color: #3b82f6;
|
|
}
|
|
|
|
.activity-icon.cron {
|
|
background: rgba(168, 85, 247, 0.2);
|
|
color: #a855f7;
|
|
}
|
|
|
|
.activity-icon.git {
|
|
background: rgba(249, 115, 22, 0.2);
|
|
color: #f97316;
|
|
}
|
|
|
|
.activity-icon.git-file {
|
|
background: rgba(234, 179, 8, 0.2);
|
|
color: #eab308;
|
|
}
|
|
|
|
.activity-icon.file {
|
|
background: rgba(20, 184, 166, 0.2);
|
|
color: #14b8a6;
|
|
}
|
|
|
|
.activity-icon svg {
|
|
width: 14px;
|
|
height: 14px;
|
|
}
|
|
|
|
.activity-type {
|
|
font-size: var(--text-xs);
|
|
padding: 2px 6px;
|
|
border-radius: 4px;
|
|
background: var(--bg-surface);
|
|
}
|
|
|
|
.activity-content {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.activity-text {
|
|
font-size: var(--text-sm);
|
|
color: var(--text-primary);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.activity-meta {
|
|
font-size: var(--text-xs);
|
|
color: var(--text-muted);
|
|
display: flex;
|
|
gap: var(--space-2);
|
|
}
|
|
|
|
.activity-agent {
|
|
color: var(--accent);
|
|
}
|
|
|
|
.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-3);
|
|
}
|
|
|
|
.priority-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-2);
|
|
font-size: var(--text-xs);
|
|
font-weight: 600;
|
|
color: var(--text-muted);
|
|
margin-bottom: var(--space-2);
|
|
cursor: pointer;
|
|
padding: var(--space-1) 0;
|
|
}
|
|
|
|
.priority-header:hover {
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.priority-header svg {
|
|
width: 14px;
|
|
height: 14px;
|
|
transition: transform var(--transition-fast);
|
|
}
|
|
|
|
.priority-header.collapsed svg {
|
|
transform: rotate(-90deg);
|
|
}
|
|
|
|
.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 svg {
|
|
width: 12px;
|
|
height: 12px;
|
|
color: white;
|
|
display: none;
|
|
}
|
|
|
|
.issue-checkbox.checked svg {
|
|
display: block;
|
|
}
|
|
|
|
.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; }
|
|
|
|
/* 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: 20px;
|
|
height: 20px;
|
|
border: 2px solid var(--border-color);
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
transition: all 0.2s;
|
|
}
|
|
.todo-checkbox:hover { border-color: #8b5cf6; }
|
|
.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: 4px; }
|
|
.todo-context { font-size: 13px; color: var(--text-muted); margin-bottom: 6px; font-style: italic; }
|
|
.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; }
|
|
|
|
/* 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;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header class="header">
|
|
<a href="index.html" class="logo">
|
|
<i data-lucide="circle-dot"></i>
|
|
Echo
|
|
</a>
|
|
<nav class="nav">
|
|
<a href="index.html" class="nav-item active">
|
|
<i data-lucide="layout-dashboard"></i>
|
|
<span>Dashboard</span>
|
|
</a>
|
|
<a href="notes.html" class="nav-item">
|
|
<i data-lucide="file-text"></i>
|
|
<span>KB</span>
|
|
</a>
|
|
<a href="files.html" class="nav-item">
|
|
<i data-lucide="folder"></i>
|
|
<span>Files</span>
|
|
</a>
|
|
<button class="theme-toggle" onclick="toggleTheme()" title="Schimbă tema">
|
|
<i data-lucide="sun" id="themeIcon"></i>
|
|
</button>
|
|
</nav>
|
|
</header>
|
|
|
|
<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="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>
|
|
<i data-lucide="chevron-down" class="status-section-toggle"></i>
|
|
</div>
|
|
<div class="status-section-details" id="gitDetails">
|
|
<!-- Populated by JS -->
|
|
</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>
|
|
|
|
</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-actions" onclick="event.stopPropagation()">
|
|
<button class="btn btn-icon" onclick="showAddModal()" title="Adaugă issue">
|
|
<i data-lucide="plus"></i>
|
|
</button>
|
|
</div>
|
|
<i data-lucide="chevron-down" class="panel-toggle"></i>
|
|
</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-actions" onclick="event.stopPropagation()">
|
|
<button class="btn btn-icon" onclick="showAddTodoModal()" title="Adaugă todo">
|
|
<i data-lucide="plus"></i>
|
|
</button>
|
|
</div>
|
|
<i data-lucide="chevron-down" class="panel-toggle"></i>
|
|
</div>
|
|
<div class="panel-body" id="todosBody">
|
|
<div class="empty-state">
|
|
<i data-lucide="loader"></i>
|
|
<p>Se încarcă...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Activity Panel -->
|
|
<div class="panel activity-panel" id="activityPanel">
|
|
<div class="panel-header" onclick="toggleSection('activityPanel')">
|
|
<div class="panel-header-left">
|
|
<div class="panel-title">
|
|
<span>Activity</span>
|
|
</div>
|
|
<span class="panel-count" id="activityCount">0</span>
|
|
</div>
|
|
<div class="panel-actions" onclick="event.stopPropagation()">
|
|
<button class="btn btn-icon" onclick="refreshActivity()" title="Refresh">
|
|
<i data-lucide="refresh-cw"></i>
|
|
</button>
|
|
</div>
|
|
<i data-lucide="chevron-down" class="panel-toggle"></i>
|
|
</div>
|
|
<div class="panel-body" id="activityBody">
|
|
<div class="empty-state">
|
|
<i data-lucide="loader"></i>
|
|
<p>Se încarcă...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- Add Issue Modal -->
|
|
<div class="modal-overlay" id="addModal">
|
|
<div class="modal">
|
|
<h2 class="modal-title">Issue nou</h2>
|
|
<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">Deadline</label>
|
|
<input type="date" class="input" id="issueDeadline">
|
|
</div>
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button class="btn btn-secondary" onclick="hideAddModal()">Anulează</button>
|
|
<button class="btn btn-primary" onclick="addIssue()">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 activityData = [];
|
|
let currentFilter = 'all';
|
|
let collapsedPriorities = new Set(['backlog']);
|
|
|
|
// 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() {
|
|
try {
|
|
return JSON.parse(localStorage.getItem('collapsedStatusSections') || '["anafSection"]');
|
|
} catch { return ['anafSection']; }
|
|
}
|
|
|
|
function setCollapsedStatusSections(sections) {
|
|
localStorage.setItem('collapsedStatusSections', JSON.stringify(sections));
|
|
}
|
|
|
|
function initStatusSections() {
|
|
const collapsed = getCollapsedStatusSections();
|
|
collapsed.forEach(id => {
|
|
const el = document.getElementById(id);
|
|
if (el) el.classList.add('collapsed');
|
|
});
|
|
}
|
|
|
|
function toggleStatusSection(id) {
|
|
const el = document.getElementById(id);
|
|
if (!el) return;
|
|
|
|
el.classList.toggle('collapsed');
|
|
|
|
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 gitCommit() {
|
|
if (!confirm('Fac commit și push la toate modificările?')) return;
|
|
showToast('Se execută commit...', 'info');
|
|
try {
|
|
const response = await fetch('/echo/api/git-commit', { method: 'POST' });
|
|
const result = await response.json();
|
|
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() {
|
|
try {
|
|
const response = await fetch('./api/git?' + Date.now());
|
|
if (!response.ok) throw new Error('API error');
|
|
const git = await response.json();
|
|
|
|
// Update badge
|
|
const badge = document.getElementById('gitBadge');
|
|
if (git.clean) {
|
|
badge.textContent = 'curat';
|
|
badge.className = 'status-badge ok';
|
|
} else {
|
|
badge.textContent = git.uncommittedCount + ' modificări';
|
|
badge.className = 'status-badge warning';
|
|
}
|
|
|
|
// Update subtitle
|
|
const subtitle = document.getElementById('gitSubtitle');
|
|
subtitle.textContent = `${git.branch} · ${git.lastCommit.time}`;
|
|
|
|
// Update details
|
|
const details = document.getElementById('gitDetails');
|
|
const GITEA_URL = 'https://gitea.romfast.ro/romfast/clawd';
|
|
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, 40)}${git.lastCommit.message.length > 40 ? '...' : ''} <small>(${git.lastCommit.time})</small></span>
|
|
</div>
|
|
`;
|
|
|
|
if (git.uncommittedCount > 0) {
|
|
const files = git.uncommitted.slice(0, 3).map(f => f.trim().split(' ').pop()).join(', ');
|
|
const more = git.uncommittedCount > 3 ? ` +${git.uncommittedCount - 3}` : '';
|
|
html += `<div class="status-detail-item uncommitted">
|
|
<i data-lucide="alert-circle"></i>
|
|
<span><a href="files.html?git=1" style="color:var(--warning)"><strong>${git.uncommittedCount}</strong> necomise</a>: <small>${files}${more}</small></span>
|
|
</div>`;
|
|
}
|
|
|
|
html += `<div class="status-detail-item">
|
|
<i data-lucide="external-link"></i>
|
|
<a href="${GITEA_URL}" target="_blank" style="color:var(--accent);font-size:var(--text-xs)">gitea.romfast.ro/romfast/clawd</a>
|
|
</div>`;
|
|
|
|
details.innerHTML = html;
|
|
lucide.createIcons();
|
|
|
|
return git;
|
|
} catch (e) {
|
|
console.error('Git status error:', e);
|
|
document.getElementById('gitBadge').textContent = 'eroare';
|
|
document.getElementById('gitBadge').className = 'status-badge error';
|
|
}
|
|
}
|
|
|
|
async function loadAnafStatus() {
|
|
try {
|
|
const response = await fetch('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}`;
|
|
|
|
if (status.anaf.lastCheck) {
|
|
document.getElementById('anafLastCheck').textContent =
|
|
'Ultima verificare: ' + status.anaf.lastCheck;
|
|
}
|
|
}
|
|
|
|
return status;
|
|
} catch (e) {
|
|
console.log('No ANAF status');
|
|
}
|
|
}
|
|
|
|
async function loadCronStatus() {
|
|
try {
|
|
const response = await fetch('./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
|
|
const details = document.getElementById('cronDetails');
|
|
details.innerHTML = 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 ? '' : jobDate.toLocaleDateString('ro-RO', { day: 'numeric', month: 'short' });
|
|
|
|
return `
|
|
<div class="cron-item ${statusClass}">
|
|
<i data-lucide="${icon}" class="cron-icon ${statusClass}"></i>
|
|
<span class="cron-time">${job.time}${dateStr ? ` <span style="color:#6b7280;font-size:11px">(${dateStr})</span>` : ''}</span>
|
|
<span class="cron-name">${job.name}</span>
|
|
<span class="cron-agent" style="color: var(--text-muted); font-size: 0.75rem; margin-left: auto;">${job.agentId || ''}</span>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
lucide.createIcons();
|
|
} catch (e) {
|
|
console.error('Error loading cron status:', e);
|
|
const badge = document.getElementById('cronBadge');
|
|
badge.textContent = 'eroare';
|
|
badge.className = 'status-badge error';
|
|
}
|
|
}
|
|
|
|
async function loadAgentsStatus() {
|
|
try {
|
|
const response = await fetch('./api/agents?' + Date.now());
|
|
if (!response.ok) throw new Error('API error');
|
|
const data = await response.json();
|
|
|
|
const agents = data.agents || [];
|
|
const activeCount = agents.filter(a => a.active).length;
|
|
|
|
// Update badge
|
|
const badge = document.getElementById('agentsBadge');
|
|
badge.textContent = `${activeCount}/${agents.length}`;
|
|
badge.className = 'status-badge ' + (activeCount > 0 ? 'ok' : 'warning');
|
|
|
|
// Update subtitle
|
|
const subtitle = document.getElementById('agentsSubtitle');
|
|
const activeNames = agents.filter(a => a.active).map(a => a.name).join(', ');
|
|
subtitle.textContent = activeCount > 0 ? `Activi: ${activeNames}` : 'Niciun agent activ';
|
|
|
|
// Update grid
|
|
const grid = document.getElementById('agentsGrid');
|
|
grid.innerHTML = agents.map(agent => `
|
|
<div class="agent-chip ${agent.active ? 'active' : ''}">
|
|
<span class="emoji">${agent.emoji || '🤖'}</span>
|
|
<span class="name">${agent.name}</span>
|
|
<span class="status">${agent.active ? '●' : '○'}</span>
|
|
</div>
|
|
`).join('');
|
|
|
|
} catch (e) {
|
|
console.log('Agents status error:', e);
|
|
document.getElementById('agentsBadge').textContent = '-';
|
|
document.getElementById('agentsSubtitle').textContent = 'Nu se poate încărca';
|
|
}
|
|
}
|
|
|
|
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('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}">
|
|
<div class="todo-checkbox ${todo.done ? 'checked' : ''}" onclick="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">${todo.context}</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="${todo.sourceUrl}" class="todo-source-link" target="_blank" onclick="event.stopPropagation()">${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 showAddTodoModal() {
|
|
const text = prompt('Todo (ex: @work Verifică client X)');
|
|
if (text && text.trim()) {
|
|
addTodo(text.trim());
|
|
}
|
|
}
|
|
|
|
async function addTodo(text) {
|
|
// Parse domain from text (@work, @self, @sprijin, @scout)
|
|
let domain = 'work';
|
|
const domainMatch = text.match(/@(work|self|sprijin|scout)/i);
|
|
if (domainMatch) {
|
|
domain = domainMatch[1].toLowerCase();
|
|
text = text.replace(/@(work|self|sprijin|scout)/i, '').trim();
|
|
}
|
|
|
|
const todo = {
|
|
id: 'todo-' + Date.now(),
|
|
text: text,
|
|
domain: domain,
|
|
dueDate: new Date().toISOString().split('T')[0],
|
|
done: false,
|
|
doneAt: null,
|
|
source: 'manual',
|
|
createdAt: new Date().toISOString()
|
|
};
|
|
|
|
todosData.items.push(todo);
|
|
await saveTodos();
|
|
renderTodos();
|
|
showToast('Todo adăugat');
|
|
}
|
|
|
|
// ===== ISSUES =====
|
|
async function loadIssues() {
|
|
try {
|
|
const response = await fetch('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;
|
|
}
|
|
|
|
async function loadActivity() {
|
|
try {
|
|
// Fetch from unified activity API
|
|
const response = await fetch('./api/activity?t=' + Date.now());
|
|
const data = await response.json();
|
|
|
|
if (data.error) throw new Error(data.error);
|
|
|
|
activityData = (data.activities || []).map(a => ({
|
|
type: a.type,
|
|
icon: a.icon || 'activity',
|
|
text: a.text,
|
|
agent: a.agent || 'Echo',
|
|
time: a.time,
|
|
timestamp: a.timestamp,
|
|
status: a.status,
|
|
path: a.path
|
|
}));
|
|
|
|
} catch (e) {
|
|
console.error('Failed to load activity:', e);
|
|
activityData = [];
|
|
}
|
|
|
|
renderActivity();
|
|
document.getElementById('activityCount').textContent = activityData.length;
|
|
}
|
|
|
|
function formatActivityTime(timestamp) {
|
|
if (!timestamp) return '';
|
|
const date = new Date(timestamp);
|
|
if (isNaN(date.getTime())) return timestamp;
|
|
|
|
const now = new Date();
|
|
const isToday = date.toDateString() === now.toDateString();
|
|
|
|
if (isToday) {
|
|
return date.toLocaleTimeString('ro-RO', { hour: '2-digit', minute: '2-digit' });
|
|
} else {
|
|
return date.toLocaleDateString('ro-RO', { day: 'numeric', month: 'short' }) +
|
|
' ' + date.toLocaleTimeString('ro-RO', { hour: '2-digit', minute: '2-digit' });
|
|
}
|
|
}
|
|
|
|
function refreshActivity() {
|
|
loadActivity();
|
|
showToast('Activitate reîmprospătată');
|
|
}
|
|
|
|
function renderActivity() {
|
|
const body = document.getElementById('activityBody');
|
|
|
|
if (activityData.length === 0) {
|
|
body.innerHTML = `
|
|
<div class="empty-state">
|
|
<i data-lucide="inbox"></i>
|
|
<p>Nicio activitate recentă</p>
|
|
</div>
|
|
`;
|
|
lucide.createIcons();
|
|
return;
|
|
}
|
|
|
|
const today = new Date().toLocaleDateString('ro-RO', { day: 'numeric', month: 'short' });
|
|
|
|
// Group by type for better display
|
|
const typeLabels = {
|
|
'cron': '⏰ Cron Jobs',
|
|
'git': '📦 Git Commits',
|
|
'git-file': '🔸 Git Changes',
|
|
'file': '📄 Fișiere',
|
|
'task': '✅ Task-uri'
|
|
};
|
|
|
|
body.innerHTML = `
|
|
<div class="activity-section">
|
|
<div class="activity-section-title">
|
|
<i data-lucide="activity"></i>
|
|
Ultimele 24h
|
|
</div>
|
|
${activityData.map(item => {
|
|
let clickAttr = '';
|
|
if (item.type === 'git-file' && item.path) {
|
|
clickAttr = `onclick="window.open('files.html#${item.path}', '_blank')" style="cursor:pointer"`;
|
|
} else if (item.path) {
|
|
clickAttr = `onclick="window.open('files.html#${item.path}', '_blank')" style="cursor:pointer"`;
|
|
} else if (item.type === 'git' && item.commitHash) {
|
|
clickAttr = `onclick="window.open('https://gitea.romfast.ro/romfast/clawd/commit/${item.commitHash}', '_blank')" style="cursor:pointer"`;
|
|
}
|
|
return `
|
|
<div class="activity-item" ${clickAttr}>
|
|
<div class="activity-icon ${item.type}">
|
|
<i data-lucide="${item.icon || 'activity'}"></i>
|
|
</div>
|
|
<div class="activity-content">
|
|
<div class="activity-text">${item.type === 'git' && item.commitHash ? `<code style="font-size:10px;margin-right:4px">${item.commitHash}</code>` : ''}${item.text}</div>
|
|
<div class="activity-meta">
|
|
<span class="activity-type">${typeLabels[item.type] || item.type}</span>
|
|
<span class="activity-agent">${item.agent}</span>
|
|
<span>${item.time}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`}).join('')}
|
|
</div>
|
|
`;
|
|
lucide.createIcons();
|
|
}
|
|
|
|
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. Adaugă primul!</p>
|
|
</div>
|
|
<button class="add-issue-btn" onclick="showAddModal()">
|
|
<i data-lucide="plus"></i>
|
|
Adaugă issue
|
|
</button>
|
|
`;
|
|
lucide.createIcons();
|
|
return;
|
|
}
|
|
|
|
// Filter issues
|
|
let filtered = issuesData.issues;
|
|
if (currentFilter !== 'all') {
|
|
filtered = filtered.filter(i => i.owner === currentFilter);
|
|
}
|
|
|
|
// Group by priority
|
|
const grouped = {};
|
|
priorityOrder.forEach(p => grouped[p] = []);
|
|
|
|
filtered.forEach(issue => {
|
|
const p = issue.priority || 'backlog';
|
|
if (grouped[p]) grouped[p].push(issue);
|
|
else grouped['backlog'].push(issue);
|
|
});
|
|
|
|
// Sort each group: todo first, then by date
|
|
Object.keys(grouped).forEach(p => {
|
|
grouped[p].sort((a, b) => {
|
|
if (a.status === 'done' && b.status !== 'done') return 1;
|
|
if (a.status !== 'done' && b.status === 'done') return -1;
|
|
return new Date(b.created) - new Date(a.created);
|
|
});
|
|
});
|
|
|
|
let html = '';
|
|
priorityOrder.forEach(priority => {
|
|
const issues = grouped[priority];
|
|
if (issues.length === 0) return;
|
|
|
|
const isCollapsed = collapsedPriorities.has(priority);
|
|
const todoCount = issues.filter(i => i.status !== 'done').length;
|
|
|
|
html += `
|
|
<div class="priority-group">
|
|
<div class="priority-header ${isCollapsed ? 'collapsed' : ''}" onclick="togglePriority('${priority}')">
|
|
<i data-lucide="chevron-down"></i>
|
|
<span class="priority-dot ${priority}"></span>
|
|
<span>${priorityLabels[priority]}</span>
|
|
<span style="margin-left: auto; opacity: 0.7">${todoCount}/${issues.length}</span>
|
|
</div>
|
|
<div class="priority-content ${isCollapsed ? 'hidden' : ''}">
|
|
${issues.map(issue => renderIssueItem(issue)).join('')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
html += `
|
|
<button class="add-issue-btn" onclick="showAddModal()">
|
|
<i data-lucide="plus"></i>
|
|
Adaugă issue
|
|
</button>
|
|
`;
|
|
|
|
body.innerHTML = html;
|
|
lucide.createIcons();
|
|
}
|
|
|
|
function renderIssueItem(issue) {
|
|
const isDone = issue.status === 'done';
|
|
const ownerIcons = { 'clawdbot': '🤖', 'robert': '👷', 'marius': '👤' };
|
|
const ownerIcon = ownerIcons[issue.owner] || '👤';
|
|
const dateStr = new Date(issue.created).toLocaleDateString('ro-RO', { day: 'numeric', month: 'short' });
|
|
|
|
return `
|
|
<div class="issue-item ${isDone ? 'done' : ''}" data-id="${issue.id}">
|
|
<div class="issue-checkbox ${isDone ? 'checked' : ''}" onclick="toggleIssue('${issue.id}')">
|
|
<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">
|
|
${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;
|
|
|
|
issue.status = issue.status === 'done' ? 'todo' : 'done';
|
|
if (issue.status === 'done') {
|
|
issue.completed = new Date().toISOString();
|
|
} else {
|
|
delete issue.completed;
|
|
}
|
|
|
|
renderIssues();
|
|
updateIssuesCount();
|
|
await saveIssues();
|
|
showToast(issue.status === 'done' ? 'Issue finalizat! ✓' : 'Issue redeschis');
|
|
}
|
|
|
|
function editIssue(id) {
|
|
const issue = issuesData.issues.find(i => i.id === id);
|
|
if (issue) {
|
|
alert(`Edit: ${issue.title}\n\n${issue.description || 'Fără descriere'}`);
|
|
}
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
});
|
|
|
|
// Modal
|
|
function showAddModal() {
|
|
document.getElementById('addModal').classList.add('active');
|
|
document.getElementById('issueTitle').focus();
|
|
}
|
|
|
|
function hideAddModal() {
|
|
document.getElementById('addModal').classList.remove('active');
|
|
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('issueDeadline').value = '';
|
|
}
|
|
|
|
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 addIssue() {
|
|
const title = document.getElementById('issueTitle').value.trim();
|
|
if (!title) {
|
|
showToast('Titlul este obligatoriu', 'error');
|
|
return;
|
|
}
|
|
|
|
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: 'todo',
|
|
created: new Date().toISOString(),
|
|
deadline: document.getElementById('issueDeadline').value || null
|
|
};
|
|
|
|
issuesData.issues.unshift(newIssue);
|
|
hideAddModal();
|
|
renderIssues();
|
|
updateIssuesCount();
|
|
await saveIssues();
|
|
showToast('Issue adăugat!');
|
|
}
|
|
|
|
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 modal on outside click
|
|
document.getElementById('addModal').addEventListener('click', (e) => {
|
|
if (e.target.id === 'addModal') hideAddModal();
|
|
});
|
|
|
|
// Init
|
|
initStatusSections();
|
|
loadStatus();
|
|
loadIssues();
|
|
loadTodos();
|
|
loadActivity();
|
|
</script>
|
|
</body>
|
|
</html>
|