Files
clawd/kanban/index.html
Echo 4083d615ff Dashboard v2: Activity + Issues panels
- New index.html with two-panel dashboard
  - Activity panel: shows Clawdbot activity log
  - Issues panel: Eisenhower priority groups, owner filter
- issues.json: new data structure for ROA work items
- kanban.html: preserved old kanban board
- First issue: D101 RD49→RD50 fix
2026-01-30 15:26:38 +00:00

1005 lines
32 KiB
HTML

<!DOCTYPE html>
<html lang="ro">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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-5);
}
.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);
}
/* Two-column dashboard */
.dashboard-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-5);
}
@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);
}
.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-actions {
display: flex;
gap: var(--space-2);
}
.panel-body {
padding: var(--space-3);
max-height: 600px;
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 {
background: rgba(34, 197, 94, 0.2);
color: #22c55e;
}
.activity-icon.running {
background: rgba(59, 130, 246, 0.2);
color: #3b82f6;
}
.activity-icon svg {
width: 14px;
height: 14px;
}
.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(34, 197, 94, 0.15), rgba(22, 163, 74, 0.1));
}
.issues-panel .panel-title svg {
color: #22c55e;
}
.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-4);
}
.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;
}
.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.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-surface);
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;
}
.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;
}
/* Old kanban link */
.legacy-link {
font-size: var(--text-xs);
color: var(--text-muted);
text-decoration: none;
display: flex;
align-items: center;
gap: var(--space-1);
}
.legacy-link:hover {
color: var(--accent);
}
.legacy-link svg {
width: 12px;
height: 12px;
}
</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="kanban.html" class="nav-item">
<i data-lucide="columns"></i>
<span>Kanban</span>
</a>
<a href="notes.html" class="nav-item">
<i data-lucide="file-text"></i>
<span>Notes</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" id="lastUpdated">Echo Work · Productivitate și proiecte</p>
</div>
<div class="dashboard-grid">
<!-- Activity Panel -->
<div class="panel activity-panel">
<div class="panel-header">
<div class="panel-title">
<i data-lucide="bot"></i>
<span>Clawdbot Activity</span>
</div>
<div class="panel-actions">
<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 -->
<div class="panel issues-panel">
<div class="panel-header">
<div class="panel-title">
<i data-lucide="check-square"></i>
<span>Issues</span>
</div>
<div class="panel-actions">
<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="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="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();
// 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'];
// Load data
async function loadIssues() {
try {
const response = await fetch('issues.json?' + Date.now());
issuesData = await response.json();
populateProgramSelect();
renderIssues();
} 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();
}
}
async function loadActivity() {
// For now, show static data. Later we can fetch from API.
activityData = [
{ type: 'done', text: 'Răspuns întrebare D101', agent: 'Echo Work', time: '15:10' },
{ type: 'done', text: 'Salut', agent: 'Echo Work', time: '12:55' },
{ type: 'done', text: 'Identificare agent', agent: 'Echo Work', time: '12:48' }
];
renderActivity();
}
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' });
body.innerHTML = `
<div class="activity-section">
<div class="activity-section-title">
<i data-lucide="calendar"></i>
Azi (${today})
</div>
${activityData.map(item => `
<div class="activity-item">
<div class="activity-icon ${item.type}">
<i data-lucide="${item.type === 'running' ? 'loader' : 'check'}"></i>
</div>
<div class="activity-content">
<div class="activity-text">${item.text}</div>
<div class="activity-meta">
<span class="activity-agent">${item.agent}</span>
<span>${item.time}</span>
</div>
${item.progress ? `
<div class="progress-bar">
<div class="progress-fill" style="width: ${item.progress}%"></div>
</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 ownerIcon = issue.owner === 'clawdbot' ? '🤖' : '👤';
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' : '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();
await saveIssues();
showToast(issue.status === 'done' ? 'Issue finalizat! ✓' : 'Issue redeschis');
}
function editIssue(id) {
// TODO: implement edit modal
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');
// Clear form
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();
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: 'kanban/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
loadIssues();
loadActivity();
</script>
</body>
</html>