diff --git a/server/src/dashboard.js b/server/src/dashboard.js index 03dade1..2cc8059 100644 --- a/server/src/dashboard.js +++ b/server/src/dashboard.js @@ -110,21 +110,36 @@ function showInspector(nodeId) { const titleEl = document.getElementById("inspector-title"); if (titleEl) titleEl.innerText = entity.name; - const typeEl = document.getElementById("inspector-type"); - if (typeEl) - typeEl.innerText = entity.entity_type; - const nsEl = document.getElementById("inspector-namespace"); - if (nsEl) - nsEl.innerText = entity.namespace || "global"; - const obsHtml = (entity.observations || []).map((o) => `
  • ${o}
  • `).join(""); + const metaEl = document.querySelector(".inspector-meta"); + if (metaEl) { + const createdStr = entity.created_at ? new Date(entity.created_at * 1000).toISOString().replace("T", " ").substring(0, 19) : ""; + const updatedStr = entity.updated_at ? new Date(entity.updated_at * 1000).toISOString().replace("T", " ").substring(0, 19) : ""; + metaEl.innerHTML = ` +
    Type: ${escapeHtml(entity.entity_type)}
    +
    Namespace: ${escapeHtml(entity.namespace || "global")}
    + ${entity.repo_name ? `
    Repository: ${escapeHtml(entity.repo_name)}
    ` : ""} + ${entity.file_path ? `
    File: ${escapeHtml(entity.file_path)}
    ` : ""} + ${entity.git_branch ? `
    Branch: ${escapeHtml(entity.git_branch)}
    ` : ""} + ${createdStr ? `
    Created: ${escapeHtml(createdStr)}
    ` : ""} + ${updatedStr ? `
    Updated: ${escapeHtml(updatedStr)}
    ` : ""} + `; + } else { + const typeEl = document.getElementById("inspector-type"); + if (typeEl) + typeEl.innerText = entity.entity_type; + const nsEl = document.getElementById("inspector-namespace"); + if (nsEl) + nsEl.innerText = entity.namespace || "global"; + } + const obsHtml = (entity.observations || []).map((o) => `
  • ${escapeHtml(o)}
  • `).join(""); const obsEl = document.getElementById("inspector-observations"); if (obsEl) obsEl.innerHTML = obsHtml || "
  • No observations recorded.
  • "; const rels = rawRelations.filter((r) => r.from === nodeId || r.to === nodeId); const relHtml = rels.map((r) => { if (r.from === nodeId) - return `
  • → ${r.relation_type} → ${r.to}
  • `; - return `
  • ← ${r.relation_type} ← ${r.from}
  • `; + return `
  • → ${escapeHtml(r.relation_type)} → ${escapeHtml(r.to)}
  • `; + return `
  • ← ${escapeHtml(r.relation_type)} ← ${escapeHtml(r.from)}
  • `; }).join(""); const relEl = document.getElementById("inspector-relations"); if (relEl) @@ -142,10 +157,10 @@ function toggleFilter(type) { updateGraphData(); } function renderFilters() { - const container = document.getElementById("graph-filters"); - if (!container) + const container2 = document.getElementById("graph-filters"); + if (!container2) return; - container.innerHTML = Array.from(allTypes).sort().map((type) => { + container2.innerHTML = Array.from(allTypes).sort().map((type) => { const isActive = activeFilters.has(type); const color = getColorForType(type).bg || getColorForType(type).background; return ``; + } + html += ``; + if (isBlocked && blockers.length > 0 && !isCompleted && !isCancelled) { + html += `
    Waiting on: ${escapeHtml(blockers.join(", "))}
    `; } html += `${escapeHtml(t.title)}`; if (t.description) { html += `
    ${escapeHtml(t.description)}
    `; } + if (t.verification_command) { + html += `
    \uD83E\uDDEA Test: ${escapeHtml(t.verification_command)}
    `; + } const criteria = t.acceptance_criteria || []; if (criteria.length > 0) { html += ``; if (unmetCriteria && !isCompleted && !isCancelled) isBlocked = true; } - html += progressHtml; - if (!isCompleted && !isCancelled && !isBlocked) { - html += ``; + if (t.git_branch || t.repo_name) { + html += `
    `; + if (t.repo_name) + html += `\uD83D\uDCE6 ${escapeHtml(t.repo_name)}`; + if (t.git_branch) + html += `\uD83C\uDF3F ${escapeHtml(t.git_branch)}`; + html += `
    `; } + html += progressHtml; if (allChildren.length > 0) { html += `
    `; html += buildTaskTreeHTML(tasks, t.id, 0); @@ -416,14 +462,14 @@ async function handleSearch(e) { signal: activeSearchAbortController.signal }); const data = await res.json(); - const container = document.getElementById("search-results"); - if (!container) + const container2 = document.getElementById("search-results"); + if (!container2) return; if (!data.results || data.results.length === 0) { - container.innerHTML = '
    No results found.
    '; + container2.innerHTML = '
    No results found.
    '; return; } - container.innerHTML = data.results.map((r) => ` + container2.innerHTML = data.results.map((r) => `
    @@ -730,23 +776,23 @@ function setupWS() { }; } async function loadGenericList(endpoint, containerId, formatter) { - const container = document.getElementById(containerId); try { const res = await fetch(endpoint); if (!res.ok) { - if (container) { - container.innerHTML = `
    ⚠️ Failed to load ${endpoint} (${res.status} ${res.statusText})
    `; + if (container2) { + container2.innerHTML = `
    ⚠️ Failed to load ${endpoint} (${res.status} ${res.statusText})
    `; } return; } const data = await res.json(); - if (!container) + const container2 = document.getElementById(containerId); + if (!container2) return; if (!data || data.length === 0) { - container.innerHTML = '
    No items recorded.
    '; + container2.innerHTML = '
    No items recorded.
    '; return; } - container.innerHTML = data.map((item) => { + container2.innerHTML = data.map((item) => { const date = item.timestamp ? new Date(item.timestamp * 1000).toLocaleString() : ""; return `
    ${date}
    @@ -805,46 +851,172 @@ function loadLedger() { }); } function loadTerminal() { - loadGenericList("/api/terminal/history", "terminal-container", (item) => ` - ${item.command} - Exit Code: ${item.exit_code} -
    - OS: ${item.os || "Unknown"} - CWD: ${item.cwd || "Unknown"} + loadGenericList("/api/terminal/history", "terminal-container", (item) => { + const isSuccess = item.exit_code === 0; + const statusColor = isSuccess ? "var(--success-color)" : "var(--error-color)"; + const statusBg = isSuccess ? "rgba(46, 204, 113, 0.12)" : "rgba(231, 76, 60, 0.12)"; + const durationStr = item.duration_ms != null ? item.duration_ms < 1000 ? `${item.duration_ms}ms` : `${(item.duration_ms / 1000).toFixed(2)}s` : ""; + return ` +
    + $ ${escapeHtml(item.command)} +
    + ${durationStr ? `⏱️ ${escapeHtml(durationStr)}` : ""} + Exit ${item.exit_code} +
    - `); +
    + ${item.shell ? `\uD83D\uDC1A ${escapeHtml(item.shell)}` : ""} + OS: ${escapeHtml(item.os || "Unknown")} + \uD83D\uDCC2 ${escapeHtml(item.cwd || "Unknown")} + ${item.repo_name ? `\uD83D\uDCE6 ${escapeHtml(item.repo_name)}` : ""} + ${item.invoker ? `\uD83D\uDC64 ${escapeHtml(item.invoker)}` : ""} +
    + ${item.status_reason ? `
    Reason: ${escapeHtml(item.status_reason)}
    ` : ""} + ${item.stdout_summary ? `
    Output: ${escapeHtml(item.stdout_summary)}
    ` : ""} + ${item.error_output ? ` +
    + Error Output +
    ${escapeHtml(item.error_output)}
    +
    + ` : ""} + `; + }); } function loadTechDebt() { - loadGenericList("/api/tech_debts", "techdebt-container", (item) => ` - ${item.id} ${item.is_resolved ? '(Resolved)' : '(Open)'} -
    Description: ${item.description}
    -
    Ideal Solution: ${item.ideal_solution}
    -
    - Git Commit: ${item.git_commit || "None"} - Branch: ${item.git_branch || "None"} + loadGenericList("/api/tech_debts", "techdebt-container", (item) => { + const sev = (item.severity || "medium").toLowerCase(); + let sevBg = "rgba(241, 196, 15, 0.15)"; + let sevColor = "#f1c40f"; + let sevBorder = "rgba(241, 196, 15, 0.4)"; + if (sev === "critical" || sev === "blocker") { + sevBg = "rgba(231, 76, 60, 0.18)"; + sevColor = "#e74c3c"; + sevBorder = "rgba(231, 76, 60, 0.5)"; + } else if (sev === "high") { + sevBg = "rgba(230, 126, 34, 0.18)"; + sevColor = "#e67e22"; + sevBorder = "rgba(230, 126, 34, 0.5)"; + } else if (sev === "low" || sev === "trivial") { + sevBg = "rgba(52, 152, 219, 0.15)"; + sevColor = "#3498db"; + sevBorder = "rgba(52, 152, 219, 0.4)"; + } + const statusBadge = item.is_resolved ? '✓ Resolved' : '● Open'; + const fileLoc = item.file_path ? `${item.file_path}${item.line_range ? `:${item.line_range}` : ""}` : ""; + return ` +
    +
    + ${escapeHtml(item.id)} + ${escapeHtml(item.severity || "medium")} + ${item.effort_estimate ? `⏳ ${escapeHtml(item.effort_estimate)}` : ""} +
    +
    ${statusBadge}
    - `); + ${fileLoc ? `
    \uD83D\uDCC4 ${escapeHtml(fileLoc)}
    ` : ""} +
    Description: ${escapeHtml(item.description || "")}
    + ${item.workaround ? `
    Workaround: ${escapeHtml(item.workaround)}
    ` : ""} +
    Ideal Solution: ${escapeHtml(item.ideal_solution || "")}
    +
    + ${item.repo_name ? `\uD83D\uDCE6 ${escapeHtml(item.repo_name)}` : ""} + Commit: ${item.git_commit ? escapeHtml(item.git_commit.substring(0, 8)) : "None"} + \uD83C\uDF3F ${escapeHtml(item.git_branch || "None")} + \uD83C\uDFF7️ ${escapeHtml(item.namespace || "global")} +
    + `; + }); loadGenericList("/api/error_fixes", "errorfixes-container", (item) => ` - ${item.signature || "Error"} (Resolved) -
    Solution: ${item.solution || ""}
    -
    Commit: ${item.git_commit || "None"}
    +
    +
    + ${escapeHtml(item.signature || "Error")} + ${item.error_category ? `${escapeHtml(item.error_category)}` : ""} + ${item.toolchain ? `\uD83D\uDD27 ${escapeHtml(item.toolchain)}` : ""} +
    + ✓ Resolved +
    +
    Solution: ${escapeHtml(item.solution || "")}
    + ${item.stack_trace ? ` +
    + Stack Trace +
    ${escapeHtml(item.stack_trace)}
    +
    + ` : ""} +
    + ${item.repo_name ? `\uD83D\uDCE6 ${escapeHtml(item.repo_name)}` : ""} + Commit: ${item.git_commit ? escapeHtml(item.git_commit.substring(0, 8)) : "None"} + ${item.git_branch ? `\uD83C\uDF3F ${escapeHtml(item.git_branch)}` : ""} +
    `); } function loadADRs() { - loadGenericList("/api/adrs", "adrs-container", (item) => ` - ${item.id} | ${item.title} - ${item.status} -
    Context: ${item.context || ""}
    -
    Decision: ${item.decision || ""}
    -
    Consequence: ${item.consequence || ""}
    - ${item.supersedes ? `
    Supersedes: ${item.supersedes}
    ` : ""} - `); + loadGenericList("/api/adrs", "adrs-container", (item) => { + const statusLower = (item.status || "").toLowerCase(); + let badgeStyle = "color:var(--accent-color); border-color:rgba(88, 166, 255, 0.4); background:rgba(88, 166, 255, 0.12);"; + let badgeText = `● ${escapeHtml(item.status || "accepted")}`; + if (statusLower === "implemented" || statusLower === "resolved") { + badgeStyle = "color:var(--success-color); border-color:rgba(46, 160, 67, 0.4); background:rgba(46, 160, 67, 0.15); font-weight:600;"; + badgeText = `✓ ${escapeHtml(item.status)}`; + } else if (statusLower === "superseded") { + badgeStyle = "text-decoration: line-through; color:var(--error-color); border-color:rgba(248, 81, 73, 0.4); background:rgba(248, 81, 73, 0.12);"; + badgeText = `✕ ${escapeHtml(item.status)}`; + } else if (statusLower === "proposed" || statusLower === "draft") { + badgeStyle = "color:var(--warning-color); border-color:rgba(210, 153, 34, 0.4); background:rgba(210, 153, 34, 0.12);"; + badgeText = `○ ${escapeHtml(item.status)}`; + } + const resolvedDate = item.resolved_at ? new Date(item.resolved_at * 1000).toISOString().replace("T", " ").substring(0, 19) : ""; + return ` +
    + ${escapeHtml(item.id)} | ${escapeHtml(item.title)} + ${badgeText} +
    + ${item.git_commit || item.git_branch || item.task_id || item.resolved_at ? ` +
    + ${item.git_branch || item.git_commit ? `\uD83D\uDCE6 ${escapeHtml(item.git_branch || "repo")} @ ${escapeHtml((item.git_commit || "").substring(0, 8))}` : ""} + ${item.task_id ? `\uD83C\uDFAF Task: ${escapeHtml(item.task_id)}` : ""} + ${resolvedDate ? `✓ Resolved: ${escapeHtml(resolvedDate)}` : ""} +
    + ` : ""} +
    Context: ${escapeHtml(item.context || "")}
    +
    Decision: ${escapeHtml(item.decision || "")}
    +
    Consequence: ${escapeHtml(item.consequence || "")}
    + ${item.supersedes ? `
    Supersedes: ${escapeHtml(item.supersedes)}
    ` : ""} + `; + }); } function loadMemos() { - loadGenericList("/api/handoff_memos", "memos-container", (item) => ` - Memo from ${item.author || "System"} -
    ${item.content || item.summary || ""}
    - `); + loadGenericList("/api/handoff_memos", "memos-container", (item) => { + const blockers = Array.isArray(item.blockers) && item.blockers.length > 0 ? item.blockers : []; + const actionItems = Array.isArray(item.action_items) && item.action_items.length > 0 ? item.action_items : []; + return ` +
    + \uD83D\uDC64 Memo from ${escapeHtml(item.author || "System")} + ${escapeHtml(item.namespace || "global")} +
    + ${item.git_branch || item.vcs_revision || item.session_id ? ` +
    + ${item.git_branch ? `\uD83C\uDF3F ${escapeHtml(item.git_branch)}` : ""} + ${item.vcs_revision ? `Rev: ${escapeHtml(item.vcs_revision.substring(0, 8))}` : ""} + ${item.session_id ? `Session: ${escapeHtml(item.session_id)}` : ""} +
    + ` : ""} +
    ${escapeHtml(item.content || item.summary || "")}
    + ${blockers.length > 0 ? ` +
    +
    \uD83D\uDEAB Blockers:
    +
      + ${blockers.map((b) => `
    • ${escapeHtml(b)}
    • `).join("")} +
    +
    + ` : ""} + ${actionItems.length > 0 ? ` +
    +
    \uD83D\uDCCB Action Items:
    +
      + ${actionItems.map((a) => `
    • ${escapeHtml(a)}
    • `).join("")} +
    +
    + ` : ""} + `; + }); loadGenericList("/api/session_summaries", "summaries-container", (item) => `
    Session ${item.session_id ? escapeHtml(item.session_id) : "Summary"} @@ -855,17 +1027,75 @@ function loadMemos() { ${item.tasks_completed && item.tasks_completed.length > 0 ? `
    Tasks Completed:
      ${item.tasks_completed.map((t) => `
    • ${escapeHtml(t)}
    • `).join("")}
    ` : ""} ${item.next_steps && item.next_steps.length > 0 ? `
    Next Steps:
      ${item.next_steps.map((s) => `
    • ${escapeHtml(s)}
    • `).join("")}
    ` : ""} `); - loadGenericList("/api/milestones", "milestones-container", (item) => ` - ${item.title || item.name} -
    ${item.description || ""}
    - `); + loadGenericList("/api/milestones", "milestones-container", (item) => { + const statusLower = (item.status || "pending").toLowerCase(); + let statusBg = "rgba(52, 152, 219, 0.15)"; + let statusColor = "var(--accent-color)"; + let statusLabel = item.status || "Pending"; + if (statusLower === "completed" || statusLower === "done") { + statusBg = "rgba(46, 204, 113, 0.15)"; + statusColor = "var(--success-color)"; + statusLabel = "✓ Completed"; + } else if (statusLower === "in_progress" || statusLower === "active") { + statusBg = "rgba(230, 126, 34, 0.15)"; + statusColor = "var(--open-color)"; + statusLabel = "● In Progress"; + } + const targetDate = item.target_date ? new Date(item.target_date * 1000).toLocaleDateString() : ""; + const completedDate = item.completed_at ? new Date(item.completed_at * 1000).toLocaleDateString() : ""; + const deliverables = Array.isArray(item.deliverables) && item.deliverables.length > 0 ? item.deliverables : []; + const taskIds = Array.isArray(item.task_ids) && item.task_ids.length > 0 ? item.task_ids : []; + return ` +
    + ${escapeHtml(item.title || item.name || "Milestone")} + ${escapeHtml(statusLabel)} +
    + ${targetDate || completedDate ? ` +
    + ${targetDate ? `\uD83C\uDFAF Target: ${escapeHtml(targetDate)}` : ""} + ${completedDate ? `✓ Finished: ${escapeHtml(completedDate)}` : ""} +
    + ` : ""} +
    ${escapeHtml(item.description || "")}
    + ${deliverables.length > 0 ? ` +
    + Deliverables: +
      + ${deliverables.map((d) => `
    • ${escapeHtml(d)}
    • `).join("")} +
    +
    + ` : ""} + ${taskIds.length > 0 ? ` +
    + Linked Tasks: + ${taskIds.map((tid) => `${escapeHtml(tid)}`).join("")} +
    + ` : ""} + `; + }); } function loadSnippets() { - loadGenericList("/api/snippets", "snippets-container", (item) => ` - ${item.description || "Snippet"} -
    Language: ${item.language || "txt"} | Tags: ${(item.tags || []).join(", ")}
    -
    ${item.content || item.code || ""}
    - `); + loadGenericList("/api/snippets", "snippets-container", (item) => { + const code = item.code || item.content || ""; + const tags = Array.isArray(item.tags) ? item.tags : []; + const origin = item.origin_file ? `${item.origin_file}${item.line_range ? `:${item.line_range}` : ""}` : ""; + return ` +
    +
    + ${escapeHtml(item.name || item.description || "Snippet")} + ${item.language ? `${escapeHtml(item.language)}` : ""} +
    + +
    + ${item.description && item.description !== item.name ? `
    ${escapeHtml(item.description)}
    ` : ""} + ${origin ? `
    \uD83D\uDCC4 ${escapeHtml(origin)}
    ` : ""} +
    ${escapeHtml(code)}
    +
    + ${item.repo_name ? `\uD83D\uDCE6 ${escapeHtml(item.repo_name)}` : ""} + ${tags.map((t) => `#${escapeHtml(t)}`).join(" ")} +
    + `; + }); } function loadAllExtras() { loadTerminal(); diff --git a/server/src/dashboard.ts b/server/src/dashboard.ts index 0871a77..f75099d 100644 --- a/server/src/dashboard.ts +++ b/server/src/dashboard.ts @@ -40,6 +40,11 @@ interface Entity { entity_type: string; namespace?: string; observations?: string[]; + git_branch?: string; + repo_name?: string; + file_path?: string; + created_at?: number; + updated_at?: number; } interface Relation { @@ -56,7 +61,18 @@ interface TaskItem { parent_id?: string; parentId?: string; dependencies?: string[]; - acceptance_criteria?: { description: string; is_met: boolean }[]; + acceptance_criteria?: { + id?: string; + description: string; + is_met: boolean; + verification_command?: string; + verification_output?: string; + }[]; + priority?: string; + assigned_agent?: string; + verification_command?: string; + repo_name?: string; + git_branch?: string; } interface ActivityItem { @@ -217,14 +233,33 @@ function showInspector(nodeId: string): void { const titleEl = document.getElementById("inspector-title"); if (titleEl) titleEl.innerText = entity.name; - const typeEl = document.getElementById("inspector-type"); - if (typeEl) typeEl.innerText = entity.entity_type; + const metaEl = document.querySelector(".inspector-meta"); + if (metaEl) { + const createdStr = entity.created_at + ? new Date(entity.created_at * 1000).toISOString().replace("T", " ").substring(0, 19) + : ""; + const updatedStr = entity.updated_at + ? new Date(entity.updated_at * 1000).toISOString().replace("T", " ").substring(0, 19) + : ""; + metaEl.innerHTML = ` +
    Type: ${escapeHtml(entity.entity_type)}
    +
    Namespace: ${escapeHtml(entity.namespace || "global")}
    + ${entity.repo_name ? `
    Repository: ${escapeHtml(entity.repo_name)}
    ` : ""} + ${entity.file_path ? `
    File: ${escapeHtml(entity.file_path)}
    ` : ""} + ${entity.git_branch ? `
    Branch: ${escapeHtml(entity.git_branch)}
    ` : ""} + ${createdStr ? `
    Created: ${escapeHtml(createdStr)}
    ` : ""} + ${updatedStr ? `
    Updated: ${escapeHtml(updatedStr)}
    ` : ""} + `; + } else { + const typeEl = document.getElementById("inspector-type"); + if (typeEl) typeEl.innerText = entity.entity_type; - const nsEl = document.getElementById("inspector-namespace"); - if (nsEl) nsEl.innerText = entity.namespace || "global"; + const nsEl = document.getElementById("inspector-namespace"); + if (nsEl) nsEl.innerText = entity.namespace || "global"; + } const obsHtml = (entity.observations || []) - .map((o) => `
  • ${o}
  • `) + .map((o) => `
  • ${escapeHtml(o)}
  • `) .join(""); const obsEl = document.getElementById("inspector-observations"); if (obsEl) @@ -234,8 +269,8 @@ function showInspector(nodeId: string): void { const relHtml = rels .map((r) => { if (r.from === nodeId) - return `
  • → ${r.relation_type} → ${r.to}
  • `; - return `
  • ← ${r.relation_type} ← ${r.from}
  • `; + return `
  • → ${escapeHtml(r.relation_type)} → ${escapeHtml(r.to)}
  • `; + return `
  • ← ${escapeHtml(r.relation_type)} ← ${escapeHtml(r.from)}
  • `; }) .join(""); @@ -486,16 +521,41 @@ function buildTaskTreeHTML( } } + const prio = (t.priority || "").toLowerCase(); + let prioBadge = ""; + if (prio === "critical" || prio === "urgent") { + prioBadge = `CRITICAL`; + } else if (prio === "high") { + prioBadge = `HIGH`; + } else if (prio === "medium" || prio === "med") { + prioBadge = `MEDIUM`; + } else if (prio === "low") { + prioBadge = `LOW`; + } + html += `
    `; + html += `
    `; + html += `
    `; + if (prioBadge) html += prioBadge; + if (t.assigned_agent) { + html += `🤖 ${escapeHtml(t.assigned_agent)}`; + } if (isBlocked && !isCompleted && !isCancelled) { - html += `
    [BLOCKED]
    `; - if (blockers.length > 0) { - html += `
    Waiting on: ${blockers.join(", ")}
    `; - } + html += `BLOCKED`; } if (isCancelled) { - html += `
    [CANCELLED]
    `; + html += `CANCELLED`; + } + html += `
    `; + + if (!isCompleted && !isCancelled && !isBlocked) { + html += ``; + } + html += `
    `; + + if (isBlocked && blockers.length > 0 && !isCompleted && !isCancelled) { + html += `
    Waiting on: ${escapeHtml(blockers.join(", "))}
    `; } html += `${escapeHtml(t.title)}`; @@ -503,6 +563,10 @@ function buildTaskTreeHTML( html += `
    ${escapeHtml(t.description)}
    `; } + if (t.verification_command) { + html += `
    🧪 Test: ${escapeHtml(t.verification_command)}
    `; + } + const criteria = t.acceptance_criteria || []; if (criteria.length > 0) { html += `
      `; @@ -512,18 +576,21 @@ function buildTaskTreeHTML( if (!isMet) unmetCriteria = true; const check = isMet ? "☑" : "☐"; const strike = isMet ? "text-decoration: line-through;" : ""; - html += `
    • ${check} ${c.description}
    • `; + html += `
    • ${check} ${escapeHtml(c.description)}${c.verification_command ? ` (${escapeHtml(c.verification_command)})` : ""}
    • `; }); html += `
    `; if (unmetCriteria && !isCompleted && !isCancelled) isBlocked = true; } - html += progressHtml; - - if (!isCompleted && !isCancelled && !isBlocked) { - html += ``; + if (t.git_branch || t.repo_name) { + html += `
    `; + if (t.repo_name) html += `📦 ${escapeHtml(t.repo_name)}`; + if (t.git_branch) html += `🌿 ${escapeHtml(t.git_branch)}`; + html += `
    `; } + html += progressHtml; + if (allChildren.length > 0) { html += `
    `; html += buildTaskTreeHTML(tasks, t.id, 0); @@ -969,7 +1036,6 @@ async function loadGenericList( containerId: string, formatter: (item: any) => string, ): Promise { - const container = document.getElementById(containerId); try { const res = await fetch(endpoint); if (!res.ok) { @@ -979,6 +1045,7 @@ async function loadGenericList( return; } const data = await res.json(); + const container = document.getElementById(containerId); if (!container) return; if (!data || data.length === 0) { @@ -1073,14 +1140,37 @@ function loadTerminal(): void { loadGenericList( "/api/terminal/history", "terminal-container", - (item) => ` - ${item.command} - Exit Code: ${item.exit_code} -
    - OS: ${item.os || "Unknown"} - CWD: ${item.cwd || "Unknown"} + (item) => { + const isSuccess = item.exit_code === 0; + const statusColor = isSuccess ? "var(--success-color)" : "var(--error-color)"; + const statusBg = isSuccess ? "rgba(46, 204, 113, 0.12)" : "rgba(231, 76, 60, 0.12)"; + const durationStr = item.duration_ms != null ? (item.duration_ms < 1000 ? `${item.duration_ms}ms` : `${(item.duration_ms / 1000).toFixed(2)}s`) : ""; + + return ` +
    + $ ${escapeHtml(item.command)} +
    + ${durationStr ? `⏱️ ${escapeHtml(durationStr)}` : ""} + Exit ${item.exit_code} +
    - `, +
    + ${item.shell ? `🐚 ${escapeHtml(item.shell)}` : ""} + OS: ${escapeHtml(item.os || "Unknown")} + 📂 ${escapeHtml(item.cwd || "Unknown")} + ${item.repo_name ? `📦 ${escapeHtml(item.repo_name)}` : ""} + ${item.invoker ? `👤 ${escapeHtml(item.invoker)}` : ""} +
    + ${item.status_reason ? `
    Reason: ${escapeHtml(item.status_reason)}
    ` : ""} + ${item.stdout_summary ? `
    Output: ${escapeHtml(item.stdout_summary)}
    ` : ""} + ${item.error_output ? ` +
    + Error Output +
    ${escapeHtml(item.error_output)}
    +
    + ` : ""} + `; + }, ); } @@ -1088,40 +1178,123 @@ function loadTechDebt(): void { loadGenericList( "/api/tech_debts", "techdebt-container", - (item) => ` - ${item.id} ${item.is_resolved ? '(Resolved)' : '(Open)'} -
    Description: ${item.description}
    -
    Ideal Solution: ${item.ideal_solution}
    -
    - Git Commit: ${item.git_commit || "None"} - Branch: ${item.git_branch || "None"} + (item) => { + const sev = (item.severity || "medium").toLowerCase(); + let sevBg = "rgba(241, 196, 15, 0.15)"; + let sevColor = "#f1c40f"; + let sevBorder = "rgba(241, 196, 15, 0.4)"; + if (sev === "critical" || sev === "blocker") { + sevBg = "rgba(231, 76, 60, 0.18)"; + sevColor = "#e74c3c"; + sevBorder = "rgba(231, 76, 60, 0.5)"; + } else if (sev === "high") { + sevBg = "rgba(230, 126, 34, 0.18)"; + sevColor = "#e67e22"; + sevBorder = "rgba(230, 126, 34, 0.5)"; + } else if (sev === "low" || sev === "trivial") { + sevBg = "rgba(52, 152, 219, 0.15)"; + sevColor = "#3498db"; + sevBorder = "rgba(52, 152, 219, 0.4)"; + } + + const statusBadge = item.is_resolved + ? '✓ Resolved' + : '● Open'; + + const fileLoc = item.file_path + ? `${item.file_path}${item.line_range ? `:${item.line_range}` : ""}` + : ""; + + return ` +
    +
    + ${escapeHtml(item.id)} + ${escapeHtml(item.severity || "medium")} + ${item.effort_estimate ? `⏳ ${escapeHtml(item.effort_estimate)}` : ""} +
    +
    ${statusBadge}
    - `, + ${fileLoc ? `
    📄 ${escapeHtml(fileLoc)}
    ` : ""} +
    Description: ${escapeHtml(item.description || "")}
    + ${item.workaround ? `
    Workaround: ${escapeHtml(item.workaround)}
    ` : ""} +
    Ideal Solution: ${escapeHtml(item.ideal_solution || "")}
    +
    + ${item.repo_name ? `📦 ${escapeHtml(item.repo_name)}` : ""} + Commit: ${item.git_commit ? escapeHtml(item.git_commit.substring(0, 8)) : "None"} + 🌿 ${escapeHtml(item.git_branch || "None")} + 🏷️ ${escapeHtml(item.namespace || "global")} +
    + `; + }, ); loadGenericList( "/api/error_fixes", "errorfixes-container", (item) => ` - ${item.signature || "Error"} (Resolved) -
    Solution: ${item.solution || ""}
    -
    Commit: ${item.git_commit || "None"}
    +
    +
    + ${escapeHtml(item.signature || "Error")} + ${item.error_category ? `${escapeHtml(item.error_category)}` : ""} + ${item.toolchain ? `🔧 ${escapeHtml(item.toolchain)}` : ""} +
    + ✓ Resolved +
    +
    Solution: ${escapeHtml(item.solution || "")}
    + ${item.stack_trace ? ` +
    + Stack Trace +
    ${escapeHtml(item.stack_trace)}
    +
    + ` : ""} +
    + ${item.repo_name ? `📦 ${escapeHtml(item.repo_name)}` : ""} + Commit: ${item.git_commit ? escapeHtml(item.git_commit.substring(0, 8)) : "None"} + ${item.git_branch ? `🌿 ${escapeHtml(item.git_branch)}` : ""} +
    `, ); } - function loadADRs(): void { loadGenericList( "/api/adrs", "adrs-container", - (item) => ` - ${item.id} | ${item.title} - ${item.status} -
    Context: ${item.context || ""}
    -
    Decision: ${item.decision || ""}
    -
    Consequence: ${item.consequence || ""}
    - ${item.supersedes ? `
    Supersedes: ${item.supersedes}
    ` : ""} - `, + (item) => { + const statusLower = (item.status || "").toLowerCase(); + let badgeStyle = "color:var(--accent-color); border-color:rgba(88, 166, 255, 0.4); background:rgba(88, 166, 255, 0.12);"; + let badgeText = `● ${escapeHtml(item.status || "accepted")}`; + + if (statusLower === "implemented" || statusLower === "resolved") { + badgeStyle = "color:var(--success-color); border-color:rgba(46, 160, 67, 0.4); background:rgba(46, 160, 67, 0.15); font-weight:600;"; + badgeText = `✓ ${escapeHtml(item.status)}`; + } else if (statusLower === "superseded") { + badgeStyle = "text-decoration: line-through; color:var(--error-color); border-color:rgba(248, 81, 73, 0.4); background:rgba(248, 81, 73, 0.12);"; + badgeText = `✕ ${escapeHtml(item.status)}`; + } else if (statusLower === "proposed" || statusLower === "draft") { + badgeStyle = "color:var(--warning-color); border-color:rgba(210, 153, 34, 0.4); background:rgba(210, 153, 34, 0.12);"; + badgeText = `○ ${escapeHtml(item.status)}`; + } + + const resolvedDate = item.resolved_at ? new Date(item.resolved_at * 1000).toISOString().replace("T", " ").substring(0, 19) : ""; + + return ` +
    + ${escapeHtml(item.id)} | ${escapeHtml(item.title)} + ${badgeText} +
    + ${(item.git_commit || item.git_branch || item.task_id || item.resolved_at) ? ` +
    + ${item.git_branch || item.git_commit ? `📦 ${escapeHtml(item.git_branch || "repo")} @ ${escapeHtml((item.git_commit || "").substring(0, 8))}` : ""} + ${item.task_id ? `🎯 Task: ${escapeHtml(item.task_id)}` : ""} + ${resolvedDate ? `✓ Resolved: ${escapeHtml(resolvedDate)}` : ""} +
    + ` : ""} +
    Context: ${escapeHtml(item.context || "")}
    +
    Decision: ${escapeHtml(item.decision || "")}
    +
    Consequence: ${escapeHtml(item.consequence || "")}
    + ${item.supersedes ? `
    Supersedes: ${escapeHtml(item.supersedes)}
    ` : ""} + `; + }, ); } @@ -1129,10 +1302,41 @@ function loadMemos(): void { loadGenericList( "/api/handoff_memos", "memos-container", - (item) => ` - Memo from ${item.author || "System"} -
    ${item.content || item.summary || ""}
    - `, + (item) => { + const blockers = Array.isArray(item.blockers) && item.blockers.length > 0 ? item.blockers : []; + const actionItems = Array.isArray(item.action_items) && item.action_items.length > 0 ? item.action_items : []; + + return ` +
    + 👤 Memo from ${escapeHtml(item.author || "System")} + ${escapeHtml(item.namespace || "global")} +
    + ${(item.git_branch || item.vcs_revision || item.session_id) ? ` +
    + ${item.git_branch ? `🌿 ${escapeHtml(item.git_branch)}` : ""} + ${item.vcs_revision ? `Rev: ${escapeHtml(item.vcs_revision.substring(0, 8))}` : ""} + ${item.session_id ? `Session: ${escapeHtml(item.session_id)}` : ""} +
    + ` : ""} +
    ${escapeHtml(item.content || item.summary || "")}
    + ${blockers.length > 0 ? ` +
    +
    🚫 Blockers:
    +
      + ${blockers.map((b: string) => `
    • ${escapeHtml(b)}
    • `).join("")} +
    +
    + ` : ""} + ${actionItems.length > 0 ? ` +
    +
    📋 Action Items:
    +
      + ${actionItems.map((a: string) => `
    • ${escapeHtml(a)}
    • `).join("")} +
    +
    + ` : ""} + `; + }, ); loadGenericList( @@ -1153,10 +1357,54 @@ function loadMemos(): void { loadGenericList( "/api/milestones", "milestones-container", - (item) => ` - ${item.title || item.name} -
    ${item.description || ""}
    - `, + (item) => { + const statusLower = (item.status || "pending").toLowerCase(); + let statusBg = "rgba(52, 152, 219, 0.15)"; + let statusColor = "var(--accent-color)"; + let statusLabel = item.status || "Pending"; + if (statusLower === "completed" || statusLower === "done") { + statusBg = "rgba(46, 204, 113, 0.15)"; + statusColor = "var(--success-color)"; + statusLabel = "✓ Completed"; + } else if (statusLower === "in_progress" || statusLower === "active") { + statusBg = "rgba(230, 126, 34, 0.15)"; + statusColor = "var(--open-color)"; + statusLabel = "● In Progress"; + } + + const targetDate = item.target_date ? new Date(item.target_date * 1000).toLocaleDateString() : ""; + const completedDate = item.completed_at ? new Date(item.completed_at * 1000).toLocaleDateString() : ""; + const deliverables = Array.isArray(item.deliverables) && item.deliverables.length > 0 ? item.deliverables : []; + const taskIds = Array.isArray(item.task_ids) && item.task_ids.length > 0 ? item.task_ids : []; + + return ` +
    + ${escapeHtml(item.title || item.name || "Milestone")} + ${escapeHtml(statusLabel)} +
    + ${targetDate || completedDate ? ` +
    + ${targetDate ? `🎯 Target: ${escapeHtml(targetDate)}` : ""} + ${completedDate ? `✓ Finished: ${escapeHtml(completedDate)}` : ""} +
    + ` : ""} +
    ${escapeHtml(item.description || "")}
    + ${deliverables.length > 0 ? ` +
    + Deliverables: +
      + ${deliverables.map((d: string) => `
    • ${escapeHtml(d)}
    • `).join("")} +
    +
    + ` : ""} + ${taskIds.length > 0 ? ` +
    + Linked Tasks: + ${taskIds.map((tid: string) => `${escapeHtml(tid)}`).join("")} +
    + ` : ""} + `; + }, ); } @@ -1164,14 +1412,30 @@ function loadSnippets(): void { loadGenericList( "/api/snippets", "snippets-container", - (item) => ` - ${item.description || "Snippet"} -
    Language: ${item.language || "txt"} | Tags: ${(item.tags || []).join(", ")}
    -
    ${item.content || item.code || ""}
    - `, + (item) => { + const code = item.code || item.content || ""; + const tags = Array.isArray(item.tags) ? item.tags : []; + const origin = item.origin_file ? `${item.origin_file}${item.line_range ? `:${item.line_range}` : ""}` : ""; + + return ` +
    +
    + ${escapeHtml(item.name || item.description || "Snippet")} + ${item.language ? `${escapeHtml(item.language)}` : ""} +
    + +
    + ${item.description && item.description !== item.name ? `
    ${escapeHtml(item.description)}
    ` : ""} + ${origin ? `
    📄 ${escapeHtml(origin)}
    ` : ""} +
    ${escapeHtml(code)}
    +
    + ${item.repo_name ? `📦 ${escapeHtml(item.repo_name)}` : ""} + ${tags.map((t: string) => `#${escapeHtml(t)}`).join(" ")} +
    + `; + }, ); } - function loadAllExtras(): void { loadTerminal(); loadTechDebt(); diff --git a/server/src/handlers/meta.rs b/server/src/handlers/meta.rs index 52ebf4b..2ce8aa2 100644 --- a/server/src/handlers/meta.rs +++ b/server/src/handlers/meta.rs @@ -378,7 +378,7 @@ impl McpTool for DecisionsHandler { fn schema(&self) -> Value { crate::mcp::tool_def::( "decisions", - "Consolidated Architectural Decision Records (ADRs) management (log, query, delete)", + "Consolidated Architectural Decision Records (ADRs) management (log, query, update, delete)", ) } @@ -396,6 +396,12 @@ impl McpTool for DecisionsHandler { let context = req.context.unwrap_or_default(); let decision = req.decision.unwrap_or_default(); let consequence = req.consequences.unwrap_or_default(); + let status_lower = status.to_ascii_lowercase(); + let resolved_at = if status_lower == "implemented" || status_lower == "resolved" { + Some(crate::handlers::utils::now_secs()) + } else { + None + }; let idx = state.get_search_index().await; let mut final_id = String::new(); @@ -404,7 +410,7 @@ impl McpTool for DecisionsHandler { state.code.adrs.modify(|adrs| { if let Some(superseded_id) = &req.supersedes { for old_adr in adrs.iter_mut() { - if old_adr.id == *superseded_id { + if old_adr.id.eq_ignore_ascii_case(superseded_id) { old_adr.status = "superseded".to_string(); adrs_to_index.push(old_adr.clone()); break; @@ -427,6 +433,10 @@ impl McpTool for DecisionsHandler { alternatives_considered: req.alternatives_considered.unwrap_or_default(), affected_components: req.affected_components.unwrap_or_default(), author: req.author, + git_commit: req.git_commit, + git_branch: req.git_branch, + resolved_at, + task_id: req.task_id, }; adrs_to_index.push(a.clone()); @@ -444,6 +454,94 @@ impl McpTool for DecisionsHandler { ); Ok(format!("Logged decision {}: {}", final_id, title)) } + DecisionAction::Update => { + let id = req.id.ok_or_else(|| { + crate::error::AppError::Internal("Missing required parameter 'id' for action 'update'. Next step: Provide ADR 'id' string in request and retry.".to_string()) + })?; + let mut updated_adr = None; + let mut adrs_to_index = Vec::new(); + + state.code.adrs.modify(|adrs| { + let target_pos = adrs.iter().position(|a| a.id.eq_ignore_ascii_case(&id)); + if let Some(pos) = target_pos { + if let Some(superseded_id) = &req.supersedes { + if let Some(s_pos) = adrs.iter().position(|a| a.id.eq_ignore_ascii_case(superseded_id)) { + if s_pos != pos { + adrs[s_pos].status = "superseded".to_string(); + adrs_to_index.push(adrs[s_pos].clone()); + } + } + } + + let a = &mut adrs[pos]; + if let Some(t) = req.title { + a.title = t; + } + if let Some(c) = req.context { + a.context = c; + } + if let Some(d) = req.decision { + a.decision = d; + } + if let Some(cons) = req.consequences { + a.consequence = cons; + } + if let Some(s) = req.status { + let s_lower = s.to_ascii_lowercase(); + if (s_lower == "implemented" || s_lower == "resolved") && a.resolved_at.is_none() { + a.resolved_at = Some(crate::handlers::utils::now_secs()); + } else if s_lower != "implemented" && s_lower != "resolved" { + a.resolved_at = None; + } + a.status = s; + } + if req.supersedes.is_some() { + a.supersedes = req.supersedes; + } + if req.repo_name.is_some() { + a.repo_name = req.repo_name; + } + if let Some(alts) = req.alternatives_considered { + a.alternatives_considered = alts; + } + if let Some(aff) = req.affected_components { + a.affected_components = aff; + } + if req.author.is_some() { + a.author = req.author; + } + if req.git_commit.is_some() { + a.git_commit = req.git_commit; + } + if req.git_branch.is_some() { + a.git_branch = req.git_branch; + } + if req.task_id.is_some() { + a.task_id = req.task_id; + } + + adrs_to_index.push(a.clone()); + updated_adr = Some(a.clone()); + } + }); + + if let Some(adr) = updated_adr { + let idx = state.get_search_index().await; + for a in &adrs_to_index { + drop(idx.index_adr(a)); + } + state.record_activity( + "decision", + &format!("Updated {}: {}", adr.id, adr.title), + Some(&adr.status), + ); + Ok(format!("Updated decision {}: {} (status: {})", adr.id, adr.title, adr.status)) + } else { + Err(crate::error::AppError::Internal( + format!("Decision with id '{}' not found", id), + )) + } + } DecisionAction::Query => { let limit = req.limit.unwrap_or(20); let include_body = req.include_body.unwrap_or(true); @@ -458,6 +556,7 @@ impl McpTool for DecisionsHandler { crate::handlers::utils::contains_ignore_ascii_case(&a.title, q) || crate::handlers::utils::contains_ignore_ascii_case(&a.context, q) || crate::handlers::utils::contains_ignore_ascii_case(&a.decision, q) + || crate::handlers::utils::contains_ignore_ascii_case(&a.consequence, q) } else { true } @@ -476,6 +575,10 @@ impl McpTool for DecisionsHandler { "title": a.title, "status": a.status, "timestamp": a.timestamp, + "git_commit": a.git_commit, + "git_branch": a.git_branch, + "resolved_at": a.resolved_at, + "task_id": a.task_id, }) }) .collect(); @@ -1787,6 +1890,11 @@ mod tests { .map_err(|e| crate::error::AppError::Internal(e.to_string())) .unwrap(); assert_eq!(res1, "Logged decision ADR-0001: Architecture"); + let q_dec = decision_handler + .execute(json!({"action": "query"}), state.clone()) + .await + .unwrap(); + assert!(q_dec.contains("Simple")); let debt_handler = TechDebtHandler; let args_debt = json!({ @@ -2000,6 +2108,40 @@ mod tests { .await .unwrap(); assert!(q_dec_res.contains("Use Axum")); + assert!(q_dec_res.contains("Fast async API routing")); + + let q_by_consequence = handler_dec + .execute(serde_json::json!({"action": "query", "query": "Fast async"}), state.clone()) + .await + .unwrap(); + assert!(q_by_consequence.contains("Use Axum")); + + let update_res = handler_dec + .execute( + serde_json::json!({ + "action": "update", + "id": "ADR-0001", + "status": "implemented", + "git_commit": "abc1234", + "git_branch": "master", + "task_id": "TASK-123" + }), + state.clone(), + ) + .await + .unwrap(); + assert!(update_res.contains("Updated decision ADR-0001")); + assert!(update_res.contains("implemented")); + + let q_after_update = handler_dec + .execute(serde_json::json!({"action": "query", "include_body": false}), state.clone()) + .await + .unwrap(); + assert!(q_after_update.contains("implemented")); + assert!(q_after_update.contains("abc1234")); + assert!(q_after_update.contains("master")); + assert!(q_after_update.contains("TASK-123")); + assert!(q_after_update.contains("resolved_at")); let del_dec_res = handler_dec .execute(serde_json::json!({"action": "delete", "id": "ADR-0001"}), state.clone()) diff --git a/server/src/models.rs b/server/src/models.rs index 9226fd2..8f0def5 100644 --- a/server/src/models.rs +++ b/server/src/models.rs @@ -367,6 +367,7 @@ pub struct Adr { pub title: String, pub context: String, pub decision: String, + #[serde(alias = "consequences")] pub consequence: String, #[serde(default = "default_adr_status")] pub status: String, @@ -383,6 +384,14 @@ pub struct Adr { pub affected_components: Vec, #[serde(default)] pub author: Option, + #[serde(default)] + pub git_commit: Option, + #[serde(default)] + pub git_branch: Option, + #[serde(default)] + pub resolved_at: Option, + #[serde(default)] + pub task_id: Option, } fn default_adr_status() -> String { diff --git a/server/src/tools.rs b/server/src/tools.rs index 35a983e..951e416 100644 --- a/server/src/tools.rs +++ b/server/src/tools.rs @@ -792,7 +792,7 @@ pub struct SnippetsTool { pub line_range: Option, } -#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)] +#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq, Clone)] #[serde(rename_all = "snake_case")] pub enum DecisionAction { #[serde(alias = "log", alias = "LOG", alias = "Log")] @@ -801,15 +801,18 @@ pub enum DecisionAction { Query, #[serde(alias = "delete", alias = "DELETE", alias = "Delete")] Delete, + #[serde(alias = "update", alias = "UPDATE", alias = "Update")] + Update, } -/// Architectural Decision Records (ADRs) log (log, query, delete). +/// Architectural Decision Records (ADRs) log (log, query, update, delete). /// Actions: /// - 'log': Record ADR. Required: title. Optional: status, context, decision, consequences, namespace. /// - 'query': Search ADRs. Optional: query, namespace. +/// - 'update': Update ADR status or implementation info. Required: id. Optional: status, git_commit, git_branch, task_id, context, decision, consequences, title. /// - 'delete': Remove ADR. Required: id. -/// Next steps on error: Provide title for 'log' or id for 'delete'. -#[derive(Debug, Deserialize, Serialize, JsonSchema)] +/// Next steps on error: Provide title for 'log' or id for 'update'/'delete'. +#[derive(Deserialize, Serialize, JsonSchema, Debug, Clone)] pub struct DecisionsTool { /// Action to perform: 'log', 'query', or 'delete'. pub action: DecisionAction, @@ -824,6 +827,7 @@ pub struct DecisionsTool { /// Decision made (for 'log'). pub decision: Option, /// Consequences & tradeoffs (for 'log'). + #[serde(alias = "consequence")] pub consequences: Option, /// Search query string (for 'query'). pub query: Option, @@ -843,6 +847,12 @@ pub struct DecisionsTool { pub limit: Option, /// Include the full body of the matched decisions. Defaults to true. pub include_body: Option, + /// Optional Git commit where decision was implemented. + pub git_commit: Option, + /// Optional Git branch where decision was implemented. + pub git_branch: Option, + /// Optional task ID linked to this decision. + pub task_id: Option, } #[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]