Files
clawd/dashboard/index.html
2026-02-02 10:45:03 +00:00

2004 lines
70 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: #1f2937;
border-radius: 6px;
margin-bottom: 6px;
transition: all 0.2s;
}
.todo-item:hover { background: #374151; }
.todo-item.done { opacity: 0.6; }
.todo-item.done .todo-text { text-decoration: line-through; }
.todo-checkbox {
width: 20px;
height: 20px;
border: 2px solid #6b7280;
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: #f3f4f6; margin-bottom: 4px; }
.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: #9ca3af;
}
.todo-due.overdue { color: #ef4444; font-weight: 500; }
.todo-source { font-size: 11px; color: #6b7280; }
.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>
<p class="page-subtitle">Echo Work · Productivitate și proiecte</p>
</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-secondary btn-sm" 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">
<!-- Activity Panel -->
<div class="panel activity-panel" id="activityPanel">
<div class="panel-header" onclick="toggleSection('activityPanel')">
<div class="panel-header-left">
<i data-lucide="chevron-down" class="panel-toggle"></i>
<div class="panel-title">
<i data-lucide="bot"></i>
<span>Activity</span>
</div>
<span class="panel-count" id="activityCount">0</span>
</div>
<div class="panel-actions" onclick="event.stopPropagation()">
<button class="btn btn-secondary btn-sm" onclick="refreshActivity()" title="Refresh">
<i data-lucide="refresh-cw"></i>
</button>
</div>
</div>
<div class="panel-body" id="activityBody">
<div class="empty-state">
<i data-lucide="loader"></i>
<p>Se încarcă...</p>
</div>
</div>
</div>
<!-- Issues Panel -->
<!-- Todo's Panel -->
<div class="panel todos-panel" id="todosPanel">
<div class="panel-header" onclick="toggleSection('todosPanel')">
<div class="panel-header-left">
<i data-lucide="chevron-down" class="panel-toggle"></i>
<div class="panel-title">
<i data-lucide="list-todo"></i>
<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-primary btn-sm" onclick="showAddTodoModal()">
<i data-lucide="plus"></i>
<span>Nou</span>
</button>
</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 class="panel issues-panel" id="issuesPanel">
<div class="panel-header" onclick="toggleSection('issuesPanel')">
<div class="panel-header-left">
<i data-lucide="chevron-down" class="panel-toggle"></i>
<div class="panel-title">
<i data-lucide="check-square"></i>
<span>Issues</span>
</div>
<span class="panel-count" id="issuesCount">0</span>
</div>
<div class="panel-actions" onclick="event.stopPropagation()">
<button class="btn btn-primary btn-sm" onclick="showAddModal()">
<i data-lucide="plus"></i>
<span>Nou</span>
</button>
</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>
</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 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';
// 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');
return `
<div class="cron-item ${statusClass}">
<i data-lucide="${icon}" class="cron-icon ${statusClass}"></i>
<span class="cron-time">${job.time}</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>
<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 ? `<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>