diff --git a/TaskTracker.Api/Pages/Analytics.cshtml.cs b/TaskTracker.Api/Pages/Analytics.cshtml.cs index 9e2a01e..8410130 100644 --- a/TaskTracker.Api/Pages/Analytics.cshtml.cs +++ b/TaskTracker.Api/Pages/Analytics.cshtml.cs @@ -7,6 +7,7 @@ using TaskTracker.Core.Interfaces; namespace TaskTracker.Api.Pages; +[IgnoreAntiforgeryToken] public class AnalyticsModel : PageModel { private readonly ITaskRepository _taskRepo; diff --git a/TaskTracker.Api/Pages/Index.cshtml b/TaskTracker.Api/Pages/Index.cshtml new file mode 100644 index 0000000..2bab136 --- /dev/null +++ b/TaskTracker.Api/Pages/Index.cshtml @@ -0,0 +1,2 @@ +@page +@model TaskTracker.Api.Pages.IndexModel diff --git a/TaskTracker.Api/Pages/Index.cshtml.cs b/TaskTracker.Api/Pages/Index.cshtml.cs new file mode 100644 index 0000000..3249a30 --- /dev/null +++ b/TaskTracker.Api/Pages/Index.cshtml.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace TaskTracker.Api.Pages; + +public class IndexModel : PageModel +{ + public IActionResult OnGet() => RedirectToPage("/Board"); +} diff --git a/TaskTracker.Api/Pages/Partials/_MappingRow.cshtml b/TaskTracker.Api/Pages/Partials/_MappingRow.cshtml index 461ef58..14e4ea3 100644 --- a/TaskTracker.Api/Pages/Partials/_MappingRow.cshtml +++ b/TaskTracker.Api/Pages/Partials/_MappingRow.cshtml @@ -28,7 +28,7 @@ - + diff --git a/TaskTracker.Api/wwwroot/css/app.css b/TaskTracker.Api/wwwroot/css/app.css deleted file mode 100644 index 305de4c..0000000 --- a/TaskTracker.Api/wwwroot/css/app.css +++ /dev/null @@ -1,619 +0,0 @@ -/* ── Variables ── */ -:root { - --bg-primary: #1a1b23; - --bg-secondary: #22232e; - --bg-card: #2a2b38; - --bg-input: #32333f; - --bg-hover: #353647; - --border: #3a3b4a; - --text-primary: #e4e4e8; - --text-secondary: #9d9db0; - --text-muted: #6b6b80; - --accent: #6c8cff; - --accent-hover: #8ba3ff; - --success: #4caf7c; - --warning: #e0a040; - --danger: #e05555; - --info: #50b0d0; - --sidebar-width: 220px; - --radius: 8px; - --radius-sm: 4px; -} - -/* ── Reset ── */ -*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } -html, body { height: 100%; } -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - background: var(--bg-primary); - color: var(--text-primary); - line-height: 1.5; -} - -/* ── Layout ── */ -#app { - display: flex; - min-height: 100vh; -} - -#sidebar { - width: var(--sidebar-width); - background: var(--bg-secondary); - border-right: 1px solid var(--border); - display: flex; - flex-direction: column; - position: fixed; - top: 0; - left: 0; - bottom: 0; - z-index: 10; -} - -.sidebar-brand { - padding: 20px 16px; - font-size: 18px; - font-weight: 700; - color: var(--accent); - border-bottom: 1px solid var(--border); - letter-spacing: 0.5px; -} - -.nav-links { - list-style: none; - padding: 8px; -} - -.nav-link { - display: flex; - align-items: center; - gap: 10px; - padding: 10px 12px; - color: var(--text-secondary); - text-decoration: none; - border-radius: var(--radius-sm); - transition: background 0.15s, color 0.15s; - font-size: 14px; -} - -.nav-link:hover { - background: var(--bg-hover); - color: var(--text-primary); -} - -.nav-link.active { - background: var(--accent); - color: #fff; -} - -.nav-icon { - font-size: 12px; - font-weight: 700; - width: 24px; - text-align: center; - opacity: 0.8; -} - -#content { - flex: 1; - margin-left: var(--sidebar-width); - padding: 24px 32px; - max-width: 1200px; -} - -.page { animation: fadeIn 0.15s ease; } -.hidden { display: none !important; } - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(4px); } - to { opacity: 1; transform: translateY(0); } -} - -/* ── Typography ── */ -.page-title { - font-size: 22px; - font-weight: 600; - margin-bottom: 20px; - color: var(--text-primary); -} - -.section-title { - font-size: 14px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; - color: var(--text-muted); - margin-bottom: 12px; -} - -/* ── Cards ── */ -.card { - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 16px 20px; - margin-bottom: 16px; -} - -.card-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 12px; -} - -.card-title { - font-size: 16px; - font-weight: 600; -} - -/* ── Stats Grid ── */ -.stats-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); - gap: 12px; - margin-bottom: 20px; -} - -.stat-card { - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 16px; - text-align: center; -} - -.stat-value { - font-size: 28px; - font-weight: 700; - color: var(--accent); -} - -.stat-label { - font-size: 12px; - color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 0.5px; - margin-top: 4px; -} - -/* ── Status Badges ── */ -.badge { - display: inline-block; - padding: 2px 10px; - border-radius: 12px; - font-size: 12px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.3px; -} - -.badge-pending { background: #3a3b4a; color: var(--text-secondary); } -.badge-active { background: rgba(76, 175, 124, 0.2); color: var(--success); } -.badge-paused { background: rgba(224, 160, 64, 0.2); color: var(--warning); } -.badge-completed { background: rgba(108, 140, 255, 0.2); color: var(--accent); } -.badge-abandoned { background: rgba(224, 85, 85, 0.2); color: var(--danger); } - -/* ── Buttons ── */ -.btn { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 7px 14px; - border: 1px solid var(--border); - border-radius: var(--radius-sm); - background: var(--bg-input); - color: var(--text-primary); - font-size: 13px; - cursor: pointer; - transition: background 0.15s, border-color 0.15s; - font-family: inherit; -} - -.btn:hover { background: var(--bg-hover); border-color: var(--text-muted); } - -.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; } -.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); } - -.btn-success { background: var(--success); border-color: var(--success); color: #fff; } -.btn-success:hover { opacity: 0.9; } - -.btn-warning { background: var(--warning); border-color: var(--warning); color: #1a1b23; } -.btn-warning:hover { opacity: 0.9; } - -.btn-danger { background: var(--danger); border-color: var(--danger); color: #fff; } -.btn-danger:hover { opacity: 0.9; } - -.btn-sm { padding: 4px 10px; font-size: 12px; } - -.btn-group { display: flex; gap: 6px; flex-wrap: wrap; } - -/* ── Forms ── */ -.form-group { - margin-bottom: 14px; -} - -.form-label { - display: block; - font-size: 13px; - color: var(--text-secondary); - margin-bottom: 4px; -} - -.form-input, .form-select, .form-textarea { - width: 100%; - padding: 8px 12px; - background: var(--bg-input); - border: 1px solid var(--border); - border-radius: var(--radius-sm); - color: var(--text-primary); - font-size: 14px; - font-family: inherit; - transition: border-color 0.15s; -} - -.form-input:focus, .form-select:focus, .form-textarea:focus { - outline: none; - border-color: var(--accent); -} - -.form-textarea { resize: vertical; min-height: 60px; } - -.form-inline { - display: flex; - gap: 8px; - align-items: flex-end; -} - -.form-inline .form-group { margin-bottom: 0; flex: 1; } - -/* ── Tables ── */ -.table-wrap { - overflow-x: auto; -} - -table { - width: 100%; - border-collapse: collapse; - font-size: 13px; -} - -th, td { - padding: 10px 12px; - text-align: left; - border-bottom: 1px solid var(--border); -} - -th { - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.5px; - color: var(--text-muted); - font-weight: 600; - background: var(--bg-secondary); - position: sticky; - top: 0; -} - -tr:hover td { background: var(--bg-hover); } - -/* ── Tabs / Filters ── */ -.filter-bar { - display: flex; - gap: 4px; - margin-bottom: 16px; - flex-wrap: wrap; -} - -.filter-btn { - padding: 6px 14px; - border: 1px solid var(--border); - border-radius: 16px; - background: transparent; - color: var(--text-secondary); - font-size: 13px; - cursor: pointer; - transition: all 0.15s; - font-family: inherit; -} - -.filter-btn:hover { border-color: var(--text-muted); color: var(--text-primary); } -.filter-btn.active { background: var(--accent); border-color: var(--accent); color: #fff; } - -/* ── Active Task Card ── */ -.active-task-card { - border-left: 3px solid var(--success); -} - -.active-task-card .card-title { - color: var(--success); -} - -.no-active-task { - color: var(--text-muted); - font-style: italic; - padding: 12px 0; -} - -/* ── Task List ── */ -.task-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: 12px 16px; - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius); - margin-bottom: 8px; - cursor: pointer; - transition: border-color 0.15s; -} - -.task-item:hover { border-color: var(--accent); } - -.task-item-left { - display: flex; - align-items: center; - gap: 12px; - flex: 1; - min-width: 0; -} - -.task-item-title { - font-weight: 500; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.task-item-meta { - font-size: 12px; - color: var(--text-muted); -} - -/* ── Task Detail ── */ -.task-detail { margin-top: 16px; } - -.task-detail-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 16px; - margin-bottom: 16px; -} - -.task-detail-title { - font-size: 20px; - font-weight: 600; -} - -.task-meta-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); - gap: 12px; - margin-bottom: 20px; -} - -.meta-item { - font-size: 13px; -} - -.meta-label { - color: var(--text-muted); - font-size: 11px; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.note-item { - padding: 10px 14px; - background: var(--bg-secondary); - border-radius: var(--radius-sm); - margin-bottom: 8px; - font-size: 13px; -} - -.note-item-header { - display: flex; - justify-content: space-between; - margin-bottom: 4px; -} - -.note-type { - font-size: 11px; - font-weight: 600; - text-transform: uppercase; - color: var(--text-muted); -} - -.note-time { - font-size: 11px; - color: var(--text-muted); -} - -/* ── Context Summary ── */ -.summary-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 10px 0; - border-bottom: 1px solid var(--border); -} - -.summary-item:last-child { border-bottom: none; } - -.summary-app { - font-weight: 500; -} - -.summary-category { - font-size: 12px; - color: var(--text-muted); -} - -.summary-count { - font-size: 18px; - font-weight: 700; - color: var(--accent); - min-width: 50px; - text-align: right; -} - -/* ── Modal ── */ -.modal-overlay { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.6); - display: flex; - align-items: center; - justify-content: center; - z-index: 100; -} - -.modal { - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 24px; - min-width: 400px; - max-width: 90vw; - max-height: 80vh; - overflow-y: auto; - animation: modalIn 0.15s ease; -} - -@keyframes modalIn { - from { opacity: 0; transform: scale(0.95); } - to { opacity: 1; transform: scale(1); } -} - -.modal-title { - font-size: 18px; - font-weight: 600; - margin-bottom: 16px; -} - -.modal-actions { - display: flex; - justify-content: flex-end; - gap: 8px; - margin-top: 20px; -} - -/* ── Utilities ── */ -.text-muted { color: var(--text-muted); } -.text-sm { font-size: 12px; } -.mt-8 { margin-top: 8px; } -.mt-16 { margin-top: 16px; } -.mb-8 { margin-bottom: 8px; } -.mb-16 { margin-bottom: 16px; } -.flex-between { display: flex; justify-content: space-between; align-items: center; } - -.empty-state { - text-align: center; - padding: 40px 20px; - color: var(--text-muted); -} - -.truncate { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 300px; -} - -/* ── Breadcrumbs ── */ -.breadcrumb { - display: flex; - align-items: center; - gap: 4px; - font-size: 13px; - margin-bottom: 12px; - flex-wrap: wrap; -} - -.breadcrumb-link { - color: var(--accent); - text-decoration: none; - transition: color 0.15s; -} - -.breadcrumb-link:hover { - color: var(--accent-hover); - text-decoration: underline; -} - -.breadcrumb-sep { - color: var(--text-muted); - margin: 0 2px; -} - -.breadcrumb-current { - color: var(--text-secondary); -} - -.breadcrumb-parent { - color: var(--text-muted); -} - -/* ── Subtasks ── */ -.subtask-list { - margin-bottom: 8px; -} - -.subtask-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: 8px 12px; - background: var(--bg-secondary); - border: 1px solid var(--border); - border-radius: var(--radius-sm); - margin-bottom: 6px; -} - -.subtask-item:hover { - border-color: var(--accent); -} - -.subtask-item-left { - display: flex; - align-items: center; - gap: 10px; - flex: 1; - min-width: 0; -} - -.subtask-item-title { - color: var(--text-primary); - text-decoration: none; - font-weight: 500; - font-size: 13px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - cursor: pointer; -} - -.subtask-item-title:hover { - color: var(--accent); -} - -.subtask-count { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 20px; - height: 20px; - padding: 0 6px; - border-radius: 10px; - background: var(--bg-hover); - color: var(--text-muted); - font-size: 11px; - font-weight: 600; -} - -/* ── Scrollbar ── */ -::-webkit-scrollbar { width: 8px; } -::-webkit-scrollbar-track { background: var(--bg-primary); } -::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; } -::-webkit-scrollbar-thumb:hover { background: var(--text-muted); } diff --git a/TaskTracker.Api/wwwroot/index.html b/TaskTracker.Api/wwwroot/index.html deleted file mode 100644 index 1921029..0000000 --- a/TaskTracker.Api/wwwroot/index.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - TaskTracker - - - - - - TaskTracker - - - [D] Dashboard - - - [T] Tasks - - - [C] Context - - - [M] Mappings - - - - - - - - - - - - - - diff --git a/TaskTracker.Api/wwwroot/js/api.js b/TaskTracker.Api/wwwroot/js/api.js deleted file mode 100644 index bc8a5e0..0000000 --- a/TaskTracker.Api/wwwroot/js/api.js +++ /dev/null @@ -1,48 +0,0 @@ -const BASE = '/api'; - -async function request(path, options = {}) { - const res = await fetch(`${BASE}${path}`, { - headers: { 'Content-Type': 'application/json', ...options.headers }, - ...options, - }); - const json = await res.json(); - if (!json.success) throw new Error(json.error || 'Request failed'); - return json.data; -} - -export const tasks = { - list: (status, { parentId, includeSubTasks } = {}) => { - const params = new URLSearchParams(); - if (status) params.set('status', status); - if (parentId != null) params.set('parentId', parentId); - if (includeSubTasks) params.set('includeSubTasks', 'true'); - const qs = params.toString(); - return request(`/tasks${qs ? `?${qs}` : ''}`); - }, - subtasks: (parentId) => request(`/tasks?parentId=${parentId}`), - active: () => request('/tasks/active'), - get: (id) => request(`/tasks/${id}`), - create: (body) => request('/tasks', { method: 'POST', body: JSON.stringify(body) }), - start: (id) => request(`/tasks/${id}/start`, { method: 'PUT' }), - pause: (id, note) => request(`/tasks/${id}/pause`, { method: 'PUT', body: JSON.stringify({ note }) }), - resume: (id, note) => request(`/tasks/${id}/resume`, { method: 'PUT', body: JSON.stringify({ note }) }), - complete: (id) => request(`/tasks/${id}/complete`, { method: 'PUT' }), - abandon: (id) => request(`/tasks/${id}`, { method: 'DELETE' }), -}; - -export const notes = { - list: (taskId) => request(`/tasks/${taskId}/notes`), - create: (taskId, body) => request(`/tasks/${taskId}/notes`, { method: 'POST', body: JSON.stringify(body) }), -}; - -export const context = { - recent: (minutes = 30) => request(`/context/recent?minutes=${minutes}`), - summary: () => request('/context/summary'), -}; - -export const mappings = { - list: () => request('/mappings'), - create: (body) => request('/mappings', { method: 'POST', body: JSON.stringify(body) }), - update: (id, body) => request(`/mappings/${id}`, { method: 'PUT', body: JSON.stringify(body) }), - remove: (id) => request(`/mappings/${id}`, { method: 'DELETE' }), -}; diff --git a/TaskTracker.Api/wwwroot/js/components/modal.js b/TaskTracker.Api/wwwroot/js/components/modal.js deleted file mode 100644 index 880b3d3..0000000 --- a/TaskTracker.Api/wwwroot/js/components/modal.js +++ /dev/null @@ -1,33 +0,0 @@ -const overlay = document.getElementById('modal-overlay'); - -export function showModal(title, contentHtml, actions = []) { - overlay.innerHTML = ` - - ${title} - ${contentHtml} - - `; - const actionsEl = document.getElementById('modal-actions'); - actions.forEach(({ label, cls, onClick }) => { - const btn = document.createElement('button'); - btn.className = `btn ${cls || ''}`; - btn.textContent = label; - btn.addEventListener('click', async () => { - await onClick(overlay.querySelector('.modal')); - closeModal(); - }); - actionsEl.appendChild(btn); - }); - overlay.classList.remove('hidden'); - overlay.addEventListener('click', onOverlayClick); -} - -export function closeModal() { - overlay.classList.add('hidden'); - overlay.innerHTML = ''; - overlay.removeEventListener('click', onOverlayClick); -} - -function onOverlayClick(e) { - if (e.target === overlay) closeModal(); -} diff --git a/TaskTracker.Api/wwwroot/js/pages/context.js b/TaskTracker.Api/wwwroot/js/pages/context.js deleted file mode 100644 index 71579ea..0000000 --- a/TaskTracker.Api/wwwroot/js/pages/context.js +++ /dev/null @@ -1,91 +0,0 @@ -import * as api from '../api.js'; - -const el = () => document.getElementById('page-context'); - -export async function initContext() { - el().innerHTML = ` - Context - App Summary (8 hours) - - - Recent Events - - Last 15 min - Last 30 min - Last hour - Last 2 hours - Last 8 hours - - - `; - - document.getElementById('ctx-minutes').addEventListener('change', loadEvents); - await Promise.all([loadSummary(), loadEvents()]); -} - -async function loadSummary() { - try { - const summary = await api.context.summary(); - const container = document.getElementById('ctx-summary'); - if (!summary || !summary.length) { - container.innerHTML = `No activity recorded`; - return; - } - container.innerHTML = ` - - ApplicationCategoryEventsFirst SeenLast Seen - - ${summary.map(s => ` - - ${esc(s.appName)} - ${esc(s.category)} - ${s.eventCount} - ${formatTime(s.firstSeen)} - ${formatTime(s.lastSeen)} - `).join('')} - - `; - } catch (e) { - document.getElementById('ctx-summary').innerHTML = `Failed to load summary`; - } -} - -async function loadEvents() { - const minutes = parseInt(document.getElementById('ctx-minutes').value); - try { - const events = await api.context.recent(minutes); - const container = document.getElementById('ctx-events'); - if (!events || !events.length) { - container.innerHTML = `No recent events`; - return; - } - container.innerHTML = ` - - SourceAppWindow TitleURLTime - - ${events.map(e => ` - - ${esc(e.source)} - ${esc(e.appName)} - ${esc(e.windowTitle)} - ${e.url ? esc(e.url) : '-'} - ${formatTime(e.timestamp)} - `).join('')} - - `; - } catch (e) { - document.getElementById('ctx-events').innerHTML = `Failed to load events`; - } -} - -function formatTime(iso) { - const d = new Date(iso); - return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); -} - -function esc(str) { - if (!str) return ''; - const d = document.createElement('div'); - d.textContent = str; - return d.innerHTML; -} diff --git a/TaskTracker.Api/wwwroot/js/pages/dashboard.js b/TaskTracker.Api/wwwroot/js/pages/dashboard.js deleted file mode 100644 index ade244b..0000000 --- a/TaskTracker.Api/wwwroot/js/pages/dashboard.js +++ /dev/null @@ -1,127 +0,0 @@ -import * as api from '../api.js'; - -const el = () => document.getElementById('page-dashboard'); - -export function initDashboard() { - el().innerHTML = ` - Dashboard - - Task Summary - - Recent Activity (8 hours) - `; -} - -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 = `No active task`; - return; - } - - const parentTrail = await buildParentTrail(task); - const breadcrumbHtml = parentTrail.length > 0 - ? `${parentTrail.map(p => `${esc(p.title)}/`).join('')}${esc(task.title)}` - : ''; - - const elapsed = task.startedAt ? formatElapsed(new Date(task.startedAt)) : ''; - container.innerHTML = ` - - - - ${esc(task.title)} - ${breadcrumbHtml} - ${task.description ? `${esc(task.description)}` : ''} - ${task.category ? `Category: ${esc(task.category)}` : ''} - ${elapsed ? `Active for ${elapsed}` : ''} - - - Pause - Complete - - - `; - 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]) => ` - - ${count} - ${status} - `).join(''); -} - -function renderContextSummary(summary) { - const container = document.getElementById('dash-context'); - if (!summary || summary.length === 0) { - container.innerHTML = `No recent activity`; - return; - } - container.innerHTML = summary.slice(0, 10).map(item => ` - - - ${esc(item.appName)} - ${esc(item.category)} - - ${item.eventCount} - `).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; -} diff --git a/TaskTracker.Api/wwwroot/js/pages/mappings.js b/TaskTracker.Api/wwwroot/js/pages/mappings.js deleted file mode 100644 index 86e15eb..0000000 --- a/TaskTracker.Api/wwwroot/js/pages/mappings.js +++ /dev/null @@ -1,119 +0,0 @@ -import * as api from '../api.js'; -import { showModal } from '../components/modal.js'; - -const el = () => document.getElementById('page-mappings'); - -export async function initMappings() { - el().innerHTML = ` - App Mappings - - Map process names, window titles, or URLs to categories - + New Mapping - - `; - - document.getElementById('btn-new-mapping').addEventListener('click', () => showMappingForm()); - await loadMappings(); -} - -async function loadMappings() { - try { - const mappings = await api.mappings.list(); - const container = document.getElementById('mapping-list'); - if (!mappings || !mappings.length) { - container.innerHTML = `No mappings configured`; - return; - } - container.innerHTML = ` - - PatternMatch TypeCategoryFriendly NameActions - - ${mappings.map(m => ` - - ${esc(m.pattern)} - ${m.matchType} - ${esc(m.category)} - ${esc(m.friendlyName) || '-'} - - - Edit - Delete - - - `).join('')} - - `; - container.querySelectorAll('[data-edit]').forEach(btn => { - btn.addEventListener('click', () => { - const m = mappings.find(x => x.id === parseInt(btn.dataset.edit)); - if (m) showMappingForm(m); - }); - }); - container.querySelectorAll('[data-delete]').forEach(btn => { - btn.addEventListener('click', () => confirmDelete(parseInt(btn.dataset.delete))); - }); - } catch (e) { - document.getElementById('mapping-list').innerHTML = `Failed to load mappings`; - } -} - -function showMappingForm(existing = null) { - const title = existing ? 'Edit Mapping' : 'New Mapping'; - showModal(title, ` - - Pattern * - - - - Match Type * - - Process Name - Title Contains - URL Contains - - - - Category * - - - - Friendly Name - - `, - [ - { label: 'Cancel', onClick: () => {} }, - { - label: existing ? 'Save' : 'Create', cls: 'btn-primary', onClick: async (modal) => { - const pattern = modal.querySelector('#map-pattern').value.trim(); - const matchType = modal.querySelector('#map-match-type').value; - const category = modal.querySelector('#map-category').value.trim(); - const friendlyName = modal.querySelector('#map-friendly').value.trim() || null; - if (!pattern || !category) { alert('Pattern and Category are required'); throw new Error('cancel'); } - const body = { pattern, matchType, category, friendlyName }; - if (existing) await api.mappings.update(existing.id, body); - else await api.mappings.create(body); - loadMappings(); - }, - }, - ]); - setTimeout(() => document.getElementById('map-pattern')?.focus(), 100); -} - -function confirmDelete(id) { - showModal('Delete Mapping', `Are you sure you want to delete this mapping?`, [ - { label: 'Cancel', onClick: () => {} }, - { - label: 'Delete', cls: 'btn-danger', onClick: async () => { - await api.mappings.remove(id); - loadMappings(); - }, - }, - ]); -} - -function esc(str) { - if (!str) return ''; - const d = document.createElement('div'); - d.textContent = str; - return d.innerHTML; -} diff --git a/TaskTracker.Api/wwwroot/js/pages/tasks.js b/TaskTracker.Api/wwwroot/js/pages/tasks.js deleted file mode 100644 index c015247..0000000 --- a/TaskTracker.Api/wwwroot/js/pages/tasks.js +++ /dev/null @@ -1,357 +0,0 @@ -import * as api from '../api.js'; -import { showModal, closeModal } from '../components/modal.js'; - -const el = () => document.getElementById('page-tasks'); -let currentFilter = null; -let selectedTaskId = null; - -export function initTasks() { - el().innerHTML = ` - Tasks - - - + New Task - - - `; - - renderFilters(); - document.getElementById('btn-new-task').addEventListener('click', () => showNewTaskModal()); - loadTasks(); -} - -function renderFilters() { - const statuses = [null, 'Pending', 'Active', 'Paused', 'Completed', 'Abandoned']; - const labels = ['All', 'Pending', 'Active', 'Paused', 'Completed', 'Abandoned']; - const container = document.getElementById('task-filters'); - container.innerHTML = statuses.map((s, i) => ` - ${labels[i]}`).join(''); - container.querySelectorAll('.filter-btn').forEach(btn => { - btn.addEventListener('click', () => { - currentFilter = btn.dataset.status || null; - renderFilters(); - loadTasks(); - }); - }); -} - -async function loadTasks() { - try { - const tasks = await api.tasks.list(currentFilter); - renderTaskList(tasks); - } catch (e) { - document.getElementById('task-list').innerHTML = `Failed to load tasks`; - } -} - -function renderTaskList(tasks) { - const container = document.getElementById('task-list'); - document.getElementById('task-detail').classList.add('hidden'); - container.classList.remove('hidden'); - if (!tasks.length) { - container.innerHTML = `No tasks found`; - return; - } - container.innerHTML = tasks.map(t => { - const subCount = t.subTasks ? t.subTasks.length : 0; - return ` - - - ${t.status} - ${esc(t.title)} - ${subCount > 0 ? `${subCount} subtask${subCount !== 1 ? 's' : ''}` : ''} - - ${t.category ? esc(t.category) + ' · ' : ''}${formatDate(t.createdAt)} - `; - }).join(''); - container.querySelectorAll('.task-item').forEach(item => { - item.addEventListener('click', () => showTaskDetail(parseInt(item.dataset.id))); - }); -} - -async function buildBreadcrumbs(task) { - const trail = [{ id: task.id, title: task.title }]; - 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 showTaskDetail(id) { - selectedTaskId = id; - try { - const task = await api.tasks.get(id); - const container = document.getElementById('task-detail'); - document.getElementById('task-list').classList.add('hidden'); - container.classList.remove('hidden'); - - // Build breadcrumb trail - const breadcrumbs = await buildBreadcrumbs(task); - const breadcrumbHtml = breadcrumbs.length > 1 - ? `${breadcrumbs.map((b, i) => - i < breadcrumbs.length - 1 - ? `${esc(b.title)}/` - : `${esc(b.title)}` - ).join('')}` - : ''; - - container.innerHTML = ` - ← ${task.parentTaskId ? 'Back to parent' : 'Back to list'} - ${breadcrumbHtml} - - - - ${esc(task.title)} - ${task.status} - - - - ${task.description ? `${esc(task.description)}` : ''} - - Category${esc(task.category) || 'None'} - Created${formatDateTime(task.createdAt)} - Started${task.startedAt ? formatDateTime(task.startedAt) : 'Not started'} - Completed${task.completedAt ? formatDateTime(task.completedAt) : '-'} - - Subtasks - - ${task.status !== 'Completed' && task.status !== 'Abandoned' ? `+ Add Subtask` : ''} - Notes - - - - - - Add - - ${task.contextEvents && task.contextEvents.length ? ` - Linked Context Events - - - AppTitleTime - - ${task.contextEvents.slice(0, 50).map(e => ` - - ${esc(e.appName)} - ${esc(e.windowTitle)} - ${formatTime(e.timestamp)} - `).join('')} - - - ` : ''} - `; - - // Render action buttons based on status - renderActions(task); - - // Render subtasks - renderSubTasks(task.subTasks || []); - - // Render notes - renderNotes(task.notes || []); - - // Breadcrumb navigation - container.querySelectorAll('.breadcrumb-link').forEach(link => { - link.addEventListener('click', (e) => { - e.preventDefault(); - showTaskDetail(parseInt(link.dataset.id)); - }); - }); - - // Back button - document.getElementById('btn-back-tasks').addEventListener('click', () => { - if (task.parentTaskId) { - showTaskDetail(task.parentTaskId); - } else { - container.classList.add('hidden'); - document.getElementById('task-list').classList.remove('hidden'); - loadTasks(); - } - }); - - // Add subtask button - const addSubBtn = document.getElementById('btn-add-subtask'); - if (addSubBtn) { - addSubBtn.addEventListener('click', () => showNewTaskModal(task.id)); - } - - // Add note - const noteInput = document.getElementById('note-input'); - document.getElementById('btn-add-note').addEventListener('click', () => addNote(task.id, noteInput)); - noteInput.addEventListener('keydown', (e) => { - if (e.key === 'Enter') addNote(task.id, noteInput); - }); - } catch (e) { - alert('Failed to load task: ' + e.message); - } -} - -function renderSubTasks(subTasks) { - const container = document.getElementById('task-subtasks'); - if (!subTasks.length) { - container.innerHTML = `No subtasks`; - return; - } - container.innerHTML = subTasks.map(st => { - const subCount = st.subTasks ? st.subTasks.length : 0; - const canStart = st.status === 'Pending' || st.status === 'Paused'; - const canComplete = st.status === 'Active' || st.status === 'Paused'; - return ` - - - ${st.status} - ${esc(st.title)} - ${subCount > 0 ? `${subCount}` : ''} - - - ${canStart ? `${st.status === 'Paused' ? 'Resume' : 'Start'}` : ''} - ${canComplete ? `Complete` : ''} - - `; - }).join(''); - - // Navigate to subtask detail - container.querySelectorAll('.subtask-item-title').forEach(link => { - link.addEventListener('click', (e) => { - e.preventDefault(); - showTaskDetail(parseInt(link.dataset.id)); - }); - }); - - // Inline subtask actions - container.querySelectorAll('.subtask-action').forEach(btn => { - btn.addEventListener('click', async (e) => { - e.stopPropagation(); - const action = btn.dataset.action; - const stId = parseInt(btn.dataset.id); - try { - if (action === 'start') await api.tasks.start(stId); - else if (action === 'complete') await api.tasks.complete(stId); - showTaskDetail(selectedTaskId); - } catch (err) { alert(err.message); } - }); - }); -} - -function renderActions(task) { - const container = document.getElementById('task-actions'); - const actions = []; - switch (task.status) { - case 'Pending': - actions.push({ label: 'Start', cls: 'btn-success', action: () => api.tasks.start(task.id) }); - actions.push({ label: 'Abandon', cls: 'btn-danger', action: () => api.tasks.abandon(task.id) }); - break; - case 'Active': - actions.push({ label: 'Pause', cls: 'btn-warning', action: () => api.tasks.pause(task.id) }); - actions.push({ label: 'Complete', cls: 'btn-success', action: () => api.tasks.complete(task.id) }); - actions.push({ label: 'Abandon', cls: 'btn-danger', action: () => api.tasks.abandon(task.id) }); - break; - case 'Paused': - actions.push({ label: 'Resume', cls: 'btn-success', action: () => api.tasks.resume(task.id) }); - actions.push({ label: 'Complete', cls: 'btn-success', action: () => api.tasks.complete(task.id) }); - actions.push({ label: 'Abandon', cls: 'btn-danger', action: () => api.tasks.abandon(task.id) }); - break; - } - container.innerHTML = ''; - actions.forEach(({ label, cls, action }) => { - const btn = document.createElement('button'); - btn.className = `btn btn-sm ${cls}`; - btn.textContent = label; - btn.addEventListener('click', async () => { - try { - await action(); - showTaskDetail(task.id); - } catch (e) { alert(e.message); } - }); - container.appendChild(btn); - }); -} - -function renderNotes(notes) { - const container = document.getElementById('task-notes'); - if (!notes.length) { - container.innerHTML = `No notes yet`; - return; - } - container.innerHTML = notes.map(n => ` - - - ${n.type} - ${formatDateTime(n.createdAt)} - - ${esc(n.content)} - `).join(''); -} - -async function addNote(taskId, input) { - const content = input.value.trim(); - if (!content) return; - try { - await api.notes.create(taskId, { content, type: 'General' }); - input.value = ''; - showTaskDetail(taskId); - } catch (e) { alert(e.message); } -} - -function showNewTaskModal(parentTaskId = null) { - const title = parentTaskId ? 'New Subtask' : 'New Task'; - showModal(title, ` - - Title * - - - - Description - - - - Category - - `, - [ - { label: 'Cancel', onClick: () => {} }, - { - label: 'Create', cls: 'btn-primary', onClick: async (modal) => { - const taskTitle = modal.querySelector('#new-task-title').value.trim(); - if (!taskTitle) { alert('Title is required'); throw new Error('cancel'); } - const description = modal.querySelector('#new-task-desc').value.trim() || null; - const category = modal.querySelector('#new-task-cat').value.trim() || null; - const body = { title: taskTitle, description, category }; - if (parentTaskId) body.parentTaskId = parentTaskId; - await api.tasks.create(body); - if (parentTaskId) { - showTaskDetail(parentTaskId); - } else { - loadTasks(); - } - }, - }, - ]); - setTimeout(() => document.getElementById('new-task-title')?.focus(), 100); -} - -function formatDate(iso) { - return new Date(iso).toLocaleDateString(); -} - -function formatDateTime(iso) { - const d = new Date(iso); - return d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); -} - -function formatTime(iso) { - return new Date(iso).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); -} - -function esc(str) { - if (!str) return ''; - const d = document.createElement('div'); - d.textContent = str; - return d.innerHTML; -}
${esc(m.pattern)}
Are you sure you want to delete this mapping?
${esc(task.description)}