chore: initial commit of TaskTracker project
Existing ASP.NET API with vanilla JS SPA, WindowWatcher, Chrome extension, and MCP server. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
127
TaskTracker.Api/wwwroot/js/pages/dashboard.js
Normal file
127
TaskTracker.Api/wwwroot/js/pages/dashboard.js
Normal file
@@ -0,0 +1,127 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user