diff --git a/src/aggregator/web/index.html b/src/aggregator/web/index.html
index ca1688e..e912e56 100644
--- a/src/aggregator/web/index.html
+++ b/src/aggregator/web/index.html
@@ -71,7 +71,7 @@
.board {
padding: 2rem;
display: grid;
- grid-template-columns: repeat(3, 1fr);
+ grid-template-columns: repeat(4, 1fr);
gap: 1.5rem;
min-height: calc(100vh - 180px);
}
@@ -157,6 +157,65 @@
display: none;
}
.task-card.expanded .task-description { display: block; }
+ .task-card { position: relative; }
+ .task-actions {
+ position: absolute;
+ top: -8px;
+ right: -8px;
+ display: none;
+ gap: 4px;
+ z-index: 10;
+ }
+ .task-card:hover .task-actions { display: flex; }
+ .task-action-btn {
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+ border: none;
+ color: white;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 14px;
+ font-weight: bold;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.3);
+ }
+ .task-action-btn.archive { background: #3b82f6; font-size: 12px; }
+ .task-action-btn.delete { background: #ef4444; }
+ .task-action-btn:hover { transform: scale(1.1); }
+ /* Inline editing styles */
+ .edit-input {
+ background: #1e293b;
+ border: 1px solid #3b82f6;
+ border-radius: 0.25rem;
+ padding: 0.25rem 0.5rem;
+ color: #e2e8f0;
+ font-size: inherit;
+ width: 100%;
+ outline: none;
+ }
+ .inline-select {
+ background: #1e293b;
+ border: 1px solid #334155;
+ border-radius: 0.25rem;
+ padding: 0.125rem 0.25rem;
+ color: #e2e8f0;
+ font-size: 0.625rem;
+ cursor: pointer;
+ outline: none;
+ }
+ .task-title.editable:hover {
+ background: rgba(59, 130, 246, 0.1);
+ border-radius: 0.25rem;
+ cursor: text;
+ }
+ .task-controls {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin-top: 0.25rem;
+ }
/* Modal styles */
.modal-overlay {
@@ -281,6 +340,13 @@
@@ -396,7 +463,7 @@
oldBtns.forEach(btn => btn.remove());
const newTaskBtn = document.getElementById('new-task-btn');
- projects.forEach(p => {
+ projects.filter(p => tasks.some(t => t.projectName === p.name)).forEach(p => {
const btn = document.createElement('button');
btn.className = 'project-btn';
btn.innerHTML = `
${p.name}`;
@@ -410,7 +477,125 @@
});
document.getElementById('stats').textContent =
- `${projects.length} projects | ${tasks.length} tasks | Updated ${new Date().toLocaleTimeString()}`;
+ `${projects.filter(p => tasks.some(t => t.projectName === p.name)).length} active projects | ${tasks.length} tasks | Updated ${new Date().toLocaleTimeString()}`;
+ }
+
+ async function archiveTask(projectPath, taskId) {
+ try {
+ const response = await fetch('/api/tasks/archive', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ projectPath, taskId })
+ });
+ if (!response.ok) {
+ const data = await response.json();
+ throw new Error(data.error || 'Failed to archive task');
+ }
+ showToast('Task archived', 'success');
+ } catch (err) {
+ console.error('Archive error:', err);
+ showToast(err.message || 'Failed to archive task', 'error');
+ }
+ }
+
+ async function deleteTask(projectPath, taskId, title) {
+ if (!confirm(`Are you sure you want to delete "${title}"? This cannot be undone.`)) return;
+ try {
+ const response = await fetch('/api/tasks/delete', {
+ method: 'DELETE',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ projectPath, taskId })
+ });
+ if (!response.ok) {
+ const data = await response.json();
+ throw new Error(data.error || 'Failed to delete task');
+ }
+ showToast('Task deleted', 'success');
+ } catch (err) {
+ console.error('Delete error:', err);
+ showToast(err.message || 'Failed to delete task', 'error');
+ }
+ }
+
+ async function updateTask(projectPath, taskId, updates) {
+ try {
+ const response = await fetch('/api/tasks/update', {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ projectPath, taskId, ...updates })
+ });
+ if (!response.ok) {
+ const data = await response.json();
+ throw new Error(data.error || 'Failed to update task');
+ }
+ showToast('Task updated', 'success');
+ } catch (err) {
+ console.error('Update error:', err);
+ showToast(err.message || 'Failed to update task', 'error');
+ }
+ }
+
+ function startInlineEdit(card, task, field) {
+ const element = card.querySelector(`[data-field="${field}"]`);
+ const currentValue = field === 'title' ? task.title : (task.description || '');
+ const isTextarea = field === 'description';
+
+ if (isTextarea) {
+ element.innerHTML = `
+
+
+ Ctrl+Enter to save
+
+
+
+ `;
+ } else {
+ element.innerHTML = `
+
+
+
+
+
+ `;
+ }
+
+ const input = element.querySelector(isTextarea ? 'textarea' : 'input');
+ input.focus();
+ if (!isTextarea) input.select();
+
+ const saveEdit = async () => {
+ const newValue = input.value;
+ if (newValue !== currentValue) {
+ await updateTask(task.projectPath, task.id, { [field]: newValue });
+ }
+ // Re-render will happen via WebSocket update
+ };
+
+ const cancelEdit = () => {
+ renderTasks(); // Re-render to restore original
+ };
+
+ element.querySelector('[data-save]').addEventListener('click', (e) => {
+ e.stopPropagation();
+ saveEdit();
+ });
+ element.querySelector('[data-cancel]').addEventListener('click', (e) => {
+ e.stopPropagation();
+ cancelEdit();
+ });
+
+ input.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape') {
+ e.stopPropagation();
+ cancelEdit();
+ } else if (e.key === 'Enter' && (!isTextarea || e.ctrlKey)) {
+ e.stopPropagation();
+ e.preventDefault();
+ saveEdit();
+ }
+ });
+
+ input.addEventListener('click', (e) => e.stopPropagation());
}
function renderTasks() {
@@ -418,14 +603,15 @@
const statusMap = {
'to do': 'todo',
'in progress': 'progress',
- 'done': 'done'
+ 'done': 'done',
+ "won't do": 'wontdo'
};
- ['todo', 'progress', 'done'].forEach(id => {
+ ['todo', 'progress', 'done', 'wontdo'].forEach(id => {
document.getElementById(`tasks-${id}`).innerHTML = '';
});
- let counts = { todo: 0, progress: 0, done: 0 };
+ let counts = { todo: 0, progress: 0, done: 0, wontdo: 0 };
tasks.forEach(task => {
if (selectedProject && task.projectName !== selectedProject) return;
@@ -457,13 +643,35 @@
priorityClass = `priority-${task.priority.toLowerCase()}`;
}
+ const priorityBgColors = { high: 'rgba(239,68,68,0.2)', medium: 'rgba(245,158,11,0.2)', low: 'rgba(34,197,94,0.2)' };
+ const priorityTextColors = { high: '#ef4444', medium: '#f59e0b', low: '#22c55e' };
+ const currentPriority = task.priority ? task.priority.toLowerCase() : '';
+
card.innerHTML = `
+
+
+
+