Existing ASP.NET API with vanilla JS SPA, WindowWatcher, Chrome extension, and MCP server. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
128 lines
4.8 KiB
JavaScript
128 lines
4.8 KiB
JavaScript
import * as api from '../api.js';
|
|
|
|
const el = () => document.getElementById('page-dashboard');
|
|
|
|
export function initDashboard() {
|
|
el().innerHTML = `
|
|
<h1 class="page-title">Dashboard</h1>
|
|
<div id="dash-active-task"></div>
|
|
<div class="section-title mt-16">Task Summary</div>
|
|
<div id="dash-stats" class="stats-grid"></div>
|
|
<div class="section-title mt-16">Recent Activity (8 hours)</div>
|
|
<div id="dash-context" class="card"></div>`;
|
|
}
|
|
|
|
export async function refreshDashboard() {
|
|
try {
|
|
const [active, allTasks, summary] = await Promise.all([
|
|
api.tasks.active(),
|
|
api.tasks.list(null, { includeSubTasks: true }),
|
|
api.context.summary(),
|
|
]);
|
|
await renderActiveTask(active);
|
|
renderStats(allTasks);
|
|
renderContextSummary(summary);
|
|
} catch (e) {
|
|
console.error('Dashboard refresh failed:', e);
|
|
}
|
|
}
|
|
|
|
async function buildParentTrail(task) {
|
|
const trail = [];
|
|
let current = task;
|
|
while (current.parentTaskId) {
|
|
try {
|
|
current = await api.tasks.get(current.parentTaskId);
|
|
trail.unshift({ id: current.id, title: current.title });
|
|
} catch {
|
|
break;
|
|
}
|
|
}
|
|
return trail;
|
|
}
|
|
|
|
async function renderActiveTask(task) {
|
|
const container = document.getElementById('dash-active-task');
|
|
if (!task) {
|
|
container.innerHTML = `<div class="card"><div class="no-active-task">No active task</div></div>`;
|
|
return;
|
|
}
|
|
|
|
const parentTrail = await buildParentTrail(task);
|
|
const breadcrumbHtml = parentTrail.length > 0
|
|
? `<div class="breadcrumb text-sm mt-8">${parentTrail.map(p => `<span class="breadcrumb-parent">${esc(p.title)}</span><span class="breadcrumb-sep">/</span>`).join('')}<span class="breadcrumb-current">${esc(task.title)}</span></div>`
|
|
: '';
|
|
|
|
const elapsed = task.startedAt ? formatElapsed(new Date(task.startedAt)) : '';
|
|
container.innerHTML = `
|
|
<div class="card active-task-card">
|
|
<div class="card-header">
|
|
<div>
|
|
<div class="card-title">${esc(task.title)}</div>
|
|
${breadcrumbHtml}
|
|
${task.description ? `<div class="text-sm text-muted mt-8">${esc(task.description)}</div>` : ''}
|
|
${task.category ? `<div class="text-sm text-muted">Category: ${esc(task.category)}</div>` : ''}
|
|
${elapsed ? `<div class="text-sm text-muted">Active for ${elapsed}</div>` : ''}
|
|
</div>
|
|
<div class="btn-group">
|
|
<button class="btn btn-warning btn-sm" data-action="pause" data-id="${task.id}">Pause</button>
|
|
<button class="btn btn-success btn-sm" data-action="complete" data-id="${task.id}">Complete</button>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
container.querySelectorAll('[data-action]').forEach(btn => {
|
|
btn.addEventListener('click', async () => {
|
|
const action = btn.dataset.action;
|
|
const id = btn.dataset.id;
|
|
try {
|
|
if (action === 'pause') await api.tasks.pause(id);
|
|
else if (action === 'complete') await api.tasks.complete(id);
|
|
refreshDashboard();
|
|
} catch (e) { alert(e.message); }
|
|
});
|
|
});
|
|
}
|
|
|
|
function renderStats(allTasks) {
|
|
const counts = { Pending: 0, Active: 0, Paused: 0, Completed: 0, Abandoned: 0 };
|
|
allTasks.forEach(t => counts[t.status] = (counts[t.status] || 0) + 1);
|
|
const container = document.getElementById('dash-stats');
|
|
container.innerHTML = Object.entries(counts).map(([status, count]) => `
|
|
<div class="stat-card">
|
|
<div class="stat-value">${count}</div>
|
|
<div class="stat-label">${status}</div>
|
|
</div>`).join('');
|
|
}
|
|
|
|
function renderContextSummary(summary) {
|
|
const container = document.getElementById('dash-context');
|
|
if (!summary || summary.length === 0) {
|
|
container.innerHTML = `<div class="empty-state">No recent activity</div>`;
|
|
return;
|
|
}
|
|
container.innerHTML = summary.slice(0, 10).map(item => `
|
|
<div class="summary-item">
|
|
<div>
|
|
<div class="summary-app">${esc(item.appName)}</div>
|
|
<div class="summary-category">${esc(item.category)}</div>
|
|
</div>
|
|
<div class="summary-count">${item.eventCount}</div>
|
|
</div>`).join('');
|
|
}
|
|
|
|
function formatElapsed(since) {
|
|
const diff = Math.floor((Date.now() - since.getTime()) / 1000);
|
|
if (diff < 60) return `${diff}s`;
|
|
if (diff < 3600) return `${Math.floor(diff / 60)}m`;
|
|
const h = Math.floor(diff / 3600);
|
|
const m = Math.floor((diff % 3600) / 60);
|
|
return `${h}h ${m}m`;
|
|
}
|
|
|
|
function esc(str) {
|
|
if (!str) return '';
|
|
const d = document.createElement('div');
|
|
d.textContent = str;
|
|
return d.innerHTML;
|
|
}
|