feat(dashboard,adrs): enrich dashboard tabs with domain model parity and resolve consequence schema handling

This commit is contained in:
Riz Ashraf committed 2026-10-07 14:38:02 +01:00
1 parent 73c3d41c87
commit f546579596
5 files changed
+798 -143

No files matched your search

+307 -77
View File
@@ -110,21 +110,36 @@ function showInspector(nodeId) {
const titleEl = document.getElementById("inspector-title"); const titleEl = document.getElementById("inspector-title");
if (titleEl) if (titleEl)
titleEl.innerText = entity.name; titleEl.innerText = entity.name;
const typeEl = document.getElementById("inspector-type"); const metaEl = document.querySelector(".inspector-meta");
if (typeEl) if (metaEl) {
typeEl.innerText = entity.entity_type; const createdStr = entity.created_at ? new Date(entity.created_at * 1000).toISOString().replace("T", " ").substring(0, 19) : "";
const nsEl = document.getElementById("inspector-namespace"); const updatedStr = entity.updated_at ? new Date(entity.updated_at * 1000).toISOString().replace("T", " ").substring(0, 19) : "";
if (nsEl) metaEl.innerHTML = `
nsEl.innerText = entity.namespace || "global"; <div><strong>Type:</strong> <span id="inspector-type">${escapeHtml(entity.entity_type)}</span></div>
const obsHtml = (entity.observations || []).map((o) => `<li>${o}</li>`).join(""); <div><strong>Namespace:</strong> <span id="inspector-namespace">${escapeHtml(entity.namespace || "global")}</span></div>
${entity.repo_name ? `<div><strong>Repository:</strong> <span style="font-family:monospace;">${escapeHtml(entity.repo_name)}</span></div>` : ""}
${entity.file_path ? `<div><strong>File:</strong> <span style="font-family:monospace; word-break:break-all;">${escapeHtml(entity.file_path)}</span></div>` : ""}
${entity.git_branch ? `<div><strong>Branch:</strong> <span style="font-family:monospace;">${escapeHtml(entity.git_branch)}</span></div>` : ""}
${createdStr ? `<div><strong>Created:</strong> <span>${escapeHtml(createdStr)}</span></div>` : ""}
${updatedStr ? `<div><strong>Updated:</strong> <span>${escapeHtml(updatedStr)}</span></div>` : ""}
`;
} 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) => `<li>${escapeHtml(o)}</li>`).join("");
const obsEl = document.getElementById("inspector-observations"); const obsEl = document.getElementById("inspector-observations");
if (obsEl) if (obsEl)
obsEl.innerHTML = obsHtml || "<li><em>No observations recorded.</em></li>"; obsEl.innerHTML = obsHtml || "<li><em>No observations recorded.</em></li>";
const rels = rawRelations.filter((r) => r.from === nodeId || r.to === nodeId); const rels = rawRelations.filter((r) => r.from === nodeId || r.to === nodeId);
const relHtml = rels.map((r) => { const relHtml = rels.map((r) => {
if (r.from === nodeId) if (r.from === nodeId)
return `<li>→ <em>${r.relation_type}</em> → <strong>${r.to}</strong></li>`; return `<li>→ <em>${escapeHtml(r.relation_type)}</em> → <strong>${escapeHtml(r.to)}</strong></li>`;
return `<li>← <em>${r.relation_type}</em> ← <strong>${r.from}</strong></li>`; return `<li>← <em>${escapeHtml(r.relation_type)}</em> ← <strong>${escapeHtml(r.from)}</strong></li>`;
}).join(""); }).join("");
const relEl = document.getElementById("inspector-relations"); const relEl = document.getElementById("inspector-relations");
if (relEl) if (relEl)
@@ -142,10 +157,10 @@ function toggleFilter(type) {
updateGraphData(); updateGraphData();
} }
function renderFilters() { function renderFilters() {
const container = document.getElementById("graph-filters"); const container2 = document.getElementById("graph-filters");
if (!container) if (!container2)
return; return;
container.innerHTML = Array.from(allTypes).sort().map((type) => { container2.innerHTML = Array.from(allTypes).sort().map((type) => {
const isActive = activeFilters.has(type); const isActive = activeFilters.has(type);
const color = getColorForType(type).bg || getColorForType(type).background; const color = getColorForType(type).bg || getColorForType(type).background;
return `<button class="filter-chip ${isActive ? "active" : ""}" onclick="toggleFilter('${type}')"> return `<button class="filter-chip ${isActive ? "active" : ""}" onclick="toggleFilter('${type}')">
@@ -212,8 +227,8 @@ async function loadGraph() {
rawRelations = data.relations || []; rawRelations = data.relations || [];
updateGraphData(); updateGraphData();
if (!network) { if (!network) {
const container = document.getElementById("network-container"); const container2 = document.getElementById("network-container");
if (!container) if (!container2)
return; return;
const options = { const options = {
nodes: { shape: "dot", size: 16, font: { size: 12 } }, nodes: { shape: "dot", size: 16, font: { size: 12 } },
@@ -242,7 +257,7 @@ async function loadGraph() {
}, },
interaction: { hover: true, tooltipDelay: 100, zoomView: false } interaction: { hover: true, tooltipDelay: 100, zoomView: false }
}; };
network = new vis.Network(container, { nodes: nodesData, edges: edgesData }, options); network = new vis.Network(container2, { nodes: nodesData, edges: edgesData }, options);
network.once("stabilizationIterationsDone", function() { network.once("stabilizationIterationsDone", function() {
network.setOptions({ physics: { enabled: false } }); network.setOptions({ physics: { enabled: false } });
}); });
@@ -252,7 +267,7 @@ async function loadGraph() {
network.on("dragEnd", function() { network.on("dragEnd", function() {
network.setOptions({ physics: { enabled: false } }); network.setOptions({ physics: { enabled: false } });
}); });
container.addEventListener("wheel", function(event) { container2.addEventListener("wheel", function(event) {
event.preventDefault(); event.preventDefault();
const direction = event.deltaY > 0 ? -0.15 : 0.15; const direction = event.deltaY > 0 ? -0.15 : 0.15;
zoomGraph(direction); zoomGraph(direction);
@@ -268,9 +283,9 @@ async function loadGraph() {
} }
} catch (err) { } catch (err) {
console.error("Failed to load graph", err); console.error("Failed to load graph", err);
const container = document.getElementById("network-container"); const container2 = document.getElementById("network-container");
if (container) { if (container2) {
container.innerHTML = '<div style="color:#e74c3c;padding:20px;font-weight:bold;">Error loading graph: ' + err.message + "</div>"; container2.innerHTML = '<div style="color:#e74c3c;padding:20px;font-weight:bold;">Error loading graph: ' + err.message + "</div>";
} }
} }
} }
@@ -325,20 +340,46 @@ function buildTaskTreeHTML(tasks, parentId, depth = 0) {
isBlocked = true; isBlocked = true;
} }
} }
const prio = (t.priority || "").toLowerCase();
let prioBadge = "";
if (prio === "critical" || prio === "urgent") {
prioBadge = `<span style="background:rgba(231,76,60,0.18); color:#e74c3c; border:1px solid rgba(231,76,60,0.5); font-size:0.7em; font-weight:bold; padding:2px 6px; border-radius:4px; text-transform:uppercase;">CRITICAL</span>`;
} else if (prio === "high") {
prioBadge = `<span style="background:rgba(230,126,34,0.18); color:#e67e22; border:1px solid rgba(230,126,34,0.5); font-size:0.7em; font-weight:bold; padding:2px 6px; border-radius:4px; text-transform:uppercase;">HIGH</span>`;
} else if (prio === "medium" || prio === "med") {
prioBadge = `<span style="background:rgba(52,152,219,0.15); color:#3498db; border:1px solid rgba(52,152,219,0.4); font-size:0.7em; font-weight:bold; padding:2px 6px; border-radius:4px; text-transform:uppercase;">MEDIUM</span>`;
} else if (prio === "low") {
prioBadge = `<span style="background:var(--canvas-bg); color:var(--text-secondary); border:1px solid var(--border-color); font-size:0.7em; padding:2px 6px; border-radius:4px; text-transform:uppercase;">LOW</span>`;
}
html += `<div class="${cardClass}" style="margin-left: ${depth * 20}px; margin-bottom: 10px;">`; html += `<div class="${cardClass}" style="margin-left: ${depth * 20}px; margin-bottom: 10px;">`;
html += `<div style="display:flex; justify-content:space-between; align-items:flex-start; gap:8px; margin-bottom:4px;">`;
html += `<div style="display:flex; align-items:center; gap:6px; flex-wrap:wrap;">`;
if (prioBadge)
html += prioBadge;
if (t.assigned_agent) {
html += `<span style="background:var(--canvas-bg); color:var(--text-secondary); border:1px solid var(--border-color); font-size:0.7em; padding:2px 6px; border-radius:4px;">\uD83E\uDD16 ${escapeHtml(t.assigned_agent)}</span>`;
}
if (isBlocked && !isCompleted && !isCancelled) { if (isBlocked && !isCompleted && !isCancelled) {
html += `<div style="background:#e74c3c; color:white; font-size:0.75em; padding:2px 6px; border-radius:3px; display:inline-block; margin-bottom:6px; font-weight:bold;">[BLOCKED]</div>`; html += `<span style="background:#e74c3c; color:white; font-size:0.7em; padding:2px 6px; border-radius:3px; font-weight:bold;">BLOCKED</span>`;
if (blockers.length > 0) {
html += `<div style="font-size:0.8em; color:#e74c3c; margin-bottom:6px;">Waiting on: ${blockers.join(", ")}</div>`;
}
} }
if (isCancelled) { if (isCancelled) {
html += `<div style="background:#95a5a6; color:white; font-size:0.75em; padding:2px 6px; border-radius:3px; display:inline-block; margin-bottom:6px; font-weight:bold;">[CANCELLED]</div>`; html += `<span style="background:#95a5a6; color:white; font-size:0.7em; padding:2px 6px; border-radius:3px; font-weight:bold;">CANCELLED</span>`;
}
html += `</div>`;
if (!isCompleted && !isCancelled && !isBlocked) {
html += `<button class="complete-btn" style="position:static;" onclick="completeTask('${escapeHtml(t.id)}')" title="Mark Completed">✓</button>`;
}
html += `</div>`;
if (isBlocked && blockers.length > 0 && !isCompleted && !isCancelled) {
html += `<div style="font-size:0.8em; color:#e74c3c; margin-bottom:6px;">Waiting on: ${escapeHtml(blockers.join(", "))}</div>`;
} }
html += `<strong>${escapeHtml(t.title)}</strong>`; html += `<strong>${escapeHtml(t.title)}</strong>`;
if (t.description) { if (t.description) {
html += `<div style="margin-top:5px; font-size:0.9em; color:var(--text-secondary); line-height:1.4;">${escapeHtml(t.description)}</div>`; html += `<div style="margin-top:5px; font-size:0.9em; color:var(--text-secondary); line-height:1.4;">${escapeHtml(t.description)}</div>`;
} }
if (t.verification_command) {
html += `<div style="margin-top:6px; font-size:0.8em; font-family:monospace; background:var(--canvas-bg); padding:3px 8px; border-radius:4px; border:1px solid var(--border-color); color:var(--accent-color);">\uD83E\uDDEA Test: ${escapeHtml(t.verification_command)}</div>`;
}
const criteria = t.acceptance_criteria || []; const criteria = t.acceptance_criteria || [];
if (criteria.length > 0) { if (criteria.length > 0) {
html += `<ul style="margin:8px 0 0 0; padding-left:20px; font-size: 0.9em; color: var(--text-secondary);">`; html += `<ul style="margin:8px 0 0 0; padding-left:20px; font-size: 0.9em; color: var(--text-secondary);">`;
@@ -349,16 +390,21 @@ function buildTaskTreeHTML(tasks, parentId, depth = 0) {
unmetCriteria = true; unmetCriteria = true;
const check = isMet ? "☑" : "☐"; const check = isMet ? "☑" : "☐";
const strike = isMet ? "text-decoration: line-through;" : ""; const strike = isMet ? "text-decoration: line-through;" : "";
html += `<li style="${strike}">${check} ${c.description}</li>`; html += `<li style="${strike}">${check} ${escapeHtml(c.description)}${c.verification_command ? ` <code style="font-size:0.8em; background:var(--canvas-bg); padding:1px 4px; border-radius:3px;">(${escapeHtml(c.verification_command)})</code>` : ""}</li>`;
}); });
html += `</ul>`; html += `</ul>`;
if (unmetCriteria && !isCompleted && !isCancelled) if (unmetCriteria && !isCompleted && !isCancelled)
isBlocked = true; isBlocked = true;
} }
html += progressHtml; if (t.git_branch || t.repo_name) {
if (!isCompleted && !isCancelled && !isBlocked) { html += `<div style="margin-top:8px; font-size:0.75em; display:flex; gap:6px; flex-wrap:wrap; color:var(--text-secondary);">`;
html += `<button class="complete-btn" onclick="completeTask('${t.id}')" title="Mark Completed">✓</button>`; if (t.repo_name)
html += `<span style="background:var(--canvas-bg); padding:1px 6px; border-radius:3px; border:1px solid var(--border-color);">\uD83D\uDCE6 ${escapeHtml(t.repo_name)}</span>`;
if (t.git_branch)
html += `<span style="background:var(--canvas-bg); padding:1px 6px; border-radius:3px; border:1px solid var(--border-color);">\uD83C\uDF3F ${escapeHtml(t.git_branch)}</span>`;
html += `</div>`;
} }
html += progressHtml;
if (allChildren.length > 0) { if (allChildren.length > 0) {
html += `<div style="margin-top: 15px; border-left: 2px solid var(--border-color); padding-left: 10px;">`; html += `<div style="margin-top: 15px; border-left: 2px solid var(--border-color); padding-left: 10px;">`;
html += buildTaskTreeHTML(tasks, t.id, 0); html += buildTaskTreeHTML(tasks, t.id, 0);
@@ -416,14 +462,14 @@ async function handleSearch(e) {
signal: activeSearchAbortController.signal signal: activeSearchAbortController.signal
}); });
const data = await res.json(); const data = await res.json();
const container = document.getElementById("search-results"); const container2 = document.getElementById("search-results");
if (!container) if (!container2)
return; return;
if (!data.results || data.results.length === 0) { if (!data.results || data.results.length === 0) {
container.innerHTML = '<div style="padding:15px;color:var(--text-secondary);">No results found.</div>'; container2.innerHTML = '<div style="padding:15px;color:var(--text-secondary);">No results found.</div>';
return; return;
} }
container.innerHTML = data.results.map((r) => ` container2.innerHTML = data.results.map((r) => `
<div class="search-result-item" onclick="switchTab('graph-tab'); if(rawEntities['${r.id}']){ showInspector('${r.id}'); if(network){ network.selectNodes(['${r.id}']); network.focus('${r.id}', {scale:1.2, animation:true}); } }" style="cursor:pointer;"> <div class="search-result-item" onclick="switchTab('graph-tab'); if(rawEntities['${r.id}']){ showInspector('${r.id}'); if(network){ network.selectNodes(['${r.id}']); network.focus('${r.id}', {scale:1.2, animation:true}); } }" style="cursor:pointer;">
<div style="display:flex; justify-content:space-between; align-items:flex-start;"> <div style="display:flex; justify-content:space-between; align-items:flex-start;">
<div> <div>
@@ -730,23 +776,23 @@ function setupWS() {
}; };
} }
async function loadGenericList(endpoint, containerId, formatter) { async function loadGenericList(endpoint, containerId, formatter) {
const container = document.getElementById(containerId);
try { try {
const res = await fetch(endpoint); const res = await fetch(endpoint);
if (!res.ok) { if (!res.ok) {
if (container) { if (container2) {
container.innerHTML = `<div style="color:var(--error-color); padding:20px; text-align:center;">⚠️ Failed to load ${endpoint} (${res.status} ${res.statusText})</div>`; container2.innerHTML = `<div style="color:var(--error-color); padding:20px; text-align:center;">⚠️ Failed to load ${endpoint} (${res.status} ${res.statusText})</div>`;
} }
return; return;
} }
const data = await res.json(); const data = await res.json();
if (!container) const container2 = document.getElementById(containerId);
if (!container2)
return; return;
if (!data || data.length === 0) { if (!data || data.length === 0) {
container.innerHTML = '<div style="color:var(--text-secondary); padding:20px; text-align:center;">No items recorded.</div>'; container2.innerHTML = '<div style="color:var(--text-secondary); padding:20px; text-align:center;">No items recorded.</div>';
return; return;
} }
container.innerHTML = data.map((item) => { container2.innerHTML = data.map((item) => {
const date = item.timestamp ? new Date(item.timestamp * 1000).toLocaleString() : ""; const date = item.timestamp ? new Date(item.timestamp * 1000).toLocaleString() : "";
return `<div class="task-card" style="border-left-color: #9b59b6;"> return `<div class="task-card" style="border-left-color: #9b59b6;">
<div style="font-size:0.75em; color:var(--text-secondary); float:right;">${date}</div> <div style="font-size:0.75em; color:var(--text-secondary); float:right;">${date}</div>
@@ -805,46 +851,172 @@ function loadLedger() {
}); });
} }
function loadTerminal() { function loadTerminal() {
loadGenericList("/api/terminal/history", "terminal-container", (item) => ` loadGenericList("/api/terminal/history", "terminal-container", (item) => {
<span style="font-weight:bold; font-size:1.05em; color:var(--text-primary);">${item.command}</span> const isSuccess = item.exit_code === 0;
<span style="margin-left:15px; font-size:0.85em;"><span style="font-weight:bold; color:var(--text-secondary);">Exit Code:</span> <span style="color:${item.exit_code === 0 ? "var(--success-color)" : "var(--error-color)"}; font-weight:bold;">${item.exit_code}</span></span> const statusColor = isSuccess ? "var(--success-color)" : "var(--error-color)";
<div style="margin-top:10px; font-size:0.85em;"> const statusBg = isSuccess ? "rgba(46, 204, 113, 0.12)" : "rgba(231, 76, 60, 0.12)";
<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; margin-right:8px; border:1px solid var(--border-color);">OS: ${item.os || "Unknown"}</span> const durationStr = item.duration_ms != null ? item.duration_ms < 1000 ? `${item.duration_ms}ms` : `${(item.duration_ms / 1000).toFixed(2)}s` : "";
<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; font-family:monospace; border:1px solid var(--border-color);">CWD: ${item.cwd || "Unknown"}</span> return `
<div style="display:flex; justify-content:space-between; align-items:flex-start; gap:10px;">
<span style="font-family:monospace; font-weight:bold; font-size:1.05em; color:var(--text-primary); word-break:break-all;">$ ${escapeHtml(item.command)}</span>
<div style="display:flex; gap:6px; align-items:center; flex-shrink:0;">
${durationStr ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; font-size:0.75em; border:1px solid var(--border-color); color:var(--text-secondary); font-family:monospace;">⏱️ ${escapeHtml(durationStr)}</span>` : ""}
<span style="background:${statusBg}; color:${statusColor}; border:1px solid ${statusColor}; font-size:0.75em; font-weight:bold; padding:2px 6px; border-radius:4px; font-family:monospace;">Exit ${item.exit_code}</span>
</div>
</div> </div>
`); <div style="margin-top:8px; font-size:0.85em; display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
${item.shell ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color); font-weight:600;">\uD83D\uDC1A ${escapeHtml(item.shell)}</span>` : ""}
<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">OS: ${escapeHtml(item.os || "Unknown")}</span>
<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; font-family:monospace; border:1px solid var(--border-color); word-break:break-all;">\uD83D\uDCC2 ${escapeHtml(item.cwd || "Unknown")}</span>
${item.repo_name ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">\uD83D\uDCE6 ${escapeHtml(item.repo_name)}</span>` : ""}
${item.invoker ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color); color:var(--text-secondary);">\uD83D\uDC64 ${escapeHtml(item.invoker)}</span>` : ""}
</div>
${item.status_reason ? `<div style="margin-top:6px; font-size:0.85em; color:var(--error-color);"><strong>Reason:</strong> ${escapeHtml(item.status_reason)}</div>` : ""}
${item.stdout_summary ? `<div style="margin-top:6px; font-size:0.85em; color:var(--text-secondary);"><strong>Output:</strong> ${escapeHtml(item.stdout_summary)}</div>` : ""}
${item.error_output ? `
<details style="margin-top:8px;" ${!isSuccess ? "open" : ""}>
<summary style="font-size:0.8em; color:var(--error-color); cursor:pointer; font-weight:600;">Error Output</summary>
<pre style="background:#1a0f0f; color:#ff6b6b; padding:8px; border-radius:4px; border:1px solid rgba(231,76,60,0.3); font-size:0.8em; overflow-x:auto; margin-top:4px; white-space:pre-wrap;">${escapeHtml(item.error_output)}</pre>
</details>
` : ""}
`;
});
} }
function loadTechDebt() { function loadTechDebt() {
loadGenericList("/api/tech_debts", "techdebt-container", (item) => ` loadGenericList("/api/tech_debts", "techdebt-container", (item) => {
<strong>${item.id}</strong> ${item.is_resolved ? '<span style="color:var(--success-color); font-weight:bold; background: rgba(46, 204, 113, 0.1); padding: 2px 6px; border-radius: 4px; border: 1px solid var(--success-color); margin-left: 8px;">(Resolved)</span>' : '<span style="color:var(--open-color); font-weight:bold; background: rgba(230, 126, 34, 0.1); padding: 2px 6px; border-radius: 4px; border: 1px solid var(--open-color); margin-left: 8px;">(Open)</span>'} const sev = (item.severity || "medium").toLowerCase();
<div style="margin-top:5px;"><strong>Description:</strong> ${item.description}</div> let sevBg = "rgba(241, 196, 15, 0.15)";
<div style="margin-top:5px;"><strong>Ideal Solution:</strong> ${item.ideal_solution}</div> let sevColor = "#f1c40f";
<div style="margin-top:10px;"> let sevBorder = "rgba(241, 196, 15, 0.4)";
<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; font-size:0.8em; margin-right:8px; border:1px solid var(--border-color);">Git Commit: ${item.git_commit || "None"}</span> if (sev === "critical" || sev === "blocker") {
<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; font-size:0.8em; border:1px solid var(--border-color);">Branch: ${item.git_branch || "None"}</span> 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 ? '<span style="color:var(--success-color); font-weight:bold; background: rgba(46, 204, 113, 0.15); padding: 2px 6px; border-radius: 4px; border: 1px solid var(--success-color);">✓ Resolved</span>' : '<span style="color:var(--open-color); font-weight:bold; background: rgba(230, 126, 34, 0.15); padding: 2px 6px; border-radius: 4px; border: 1px solid var(--open-color);">● Open</span>';
const fileLoc = item.file_path ? `${item.file_path}${item.line_range ? `:${item.line_range}` : ""}` : "";
return `
<div style="display:flex; justify-content:space-between; align-items:center; gap:8px;">
<div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap;">
<strong style="font-family:monospace; font-size:1em;">${escapeHtml(item.id)}</strong>
<span style="background:${sevBg}; color:${sevColor}; border:1px solid ${sevBorder}; font-size:0.75em; font-weight:bold; padding:2px 6px; border-radius:4px; text-transform:uppercase;">${escapeHtml(item.severity || "medium")}</span>
${item.effort_estimate ? `<span style="background:var(--canvas-bg); font-size:0.75em; padding:2px 6px; border-radius:4px; border:1px solid var(--border-color); color:var(--text-secondary);">⏳ ${escapeHtml(item.effort_estimate)}</span>` : ""}
</div>
<div>${statusBadge}</div>
</div> </div>
`); ${fileLoc ? `<div style="margin-top:6px; font-family:monospace; font-size:0.85em; color:var(--accent-color);">\uD83D\uDCC4 ${escapeHtml(fileLoc)}</div>` : ""}
<div style="margin-top:6px; font-size:0.9em; line-height:1.4;"><strong>Description:</strong> ${escapeHtml(item.description || "")}</div>
${item.workaround ? `<div style="margin-top:4px; font-size:0.85em; color:var(--text-secondary); background:var(--canvas-bg); padding:4px 8px; border-radius:4px; border-left:3px solid #f39c12;"><strong>Workaround:</strong> ${escapeHtml(item.workaround)}</div>` : ""}
<div style="margin-top:6px; font-size:0.9em; line-height:1.4; color:var(--text-secondary);"><strong>Ideal Solution:</strong> ${escapeHtml(item.ideal_solution || "")}</div>
<div style="margin-top:8px; font-size:0.8em; display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
${item.repo_name ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">\uD83D\uDCE6 ${escapeHtml(item.repo_name)}</span>` : ""}
<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; font-family:monospace; border:1px solid var(--border-color);">Commit: ${item.git_commit ? escapeHtml(item.git_commit.substring(0, 8)) : "None"}</span>
<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">\uD83C\uDF3F ${escapeHtml(item.git_branch || "None")}</span>
<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color); color:var(--text-secondary);">\uD83C\uDFF7️ ${escapeHtml(item.namespace || "global")}</span>
</div>
`;
});
loadGenericList("/api/error_fixes", "errorfixes-container", (item) => ` loadGenericList("/api/error_fixes", "errorfixes-container", (item) => `
<strong>${item.signature || "Error"}</strong> <span style="color:var(--success-color); font-weight:bold; background: rgba(46, 204, 113, 0.1); padding: 2px 6px; border-radius: 4px; border: 1px solid var(--success-color); margin-left: 8px;">(Resolved)</span> <div style="display:flex; justify-content:space-between; align-items:flex-start; gap:8px;">
<div style="font-size:0.9em; margin-bottom:8px; margin-top:5px;"><em>Solution:</em> ${item.solution || ""}</div> <div>
<div style="font-family:monospace; font-size:0.85em; color:#e74c3c;">Commit: ${item.git_commit || "None"}</div> <strong style="color:var(--text-primary); font-family:monospace; font-size:1em;">${escapeHtml(item.signature || "Error")}</strong>
${item.error_category ? `<span style="background:rgba(231,76,60,0.15); color:var(--error-color); border:1px solid rgba(231,76,60,0.4); font-size:0.75em; font-weight:bold; padding:2px 6px; border-radius:4px; margin-left:6px;">${escapeHtml(item.error_category)}</span>` : ""}
${item.toolchain ? `<span style="background:var(--canvas-bg); color:var(--text-secondary); border:1px solid var(--border-color); font-size:0.75em; padding:2px 6px; border-radius:4px; margin-left:6px; font-family:monospace;">\uD83D\uDD27 ${escapeHtml(item.toolchain)}</span>` : ""}
</div>
<span style="color:var(--success-color); font-weight:bold; background: rgba(46, 204, 113, 0.15); padding: 2px 6px; border-radius: 4px; border: 1px solid var(--success-color); flex-shrink:0;">✓ Resolved</span>
</div>
<div style="margin-top:8px; font-size:0.9em; line-height:1.4;"><strong>Solution:</strong> ${escapeHtml(item.solution || "")}</div>
${item.stack_trace ? `
<details style="margin-top:8px;">
<summary style="font-size:0.8em; color:var(--text-secondary); cursor:pointer; font-weight:600;">Stack Trace</summary>
<pre style="background:#111417; color:#ff7b72; padding:8px; border-radius:4px; border:1px solid var(--border-color); font-size:0.8em; overflow-x:auto; margin-top:4px; white-space:pre-wrap;">${escapeHtml(item.stack_trace)}</pre>
</details>
` : ""}
<div style="margin-top:8px; font-size:0.8em; display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
${item.repo_name ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">\uD83D\uDCE6 ${escapeHtml(item.repo_name)}</span>` : ""}
<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; font-family:monospace; border:1px solid var(--border-color);">Commit: ${item.git_commit ? escapeHtml(item.git_commit.substring(0, 8)) : "None"}</span>
${item.git_branch ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">\uD83C\uDF3F ${escapeHtml(item.git_branch)}</span>` : ""}
</div>
`); `);
} }
function loadADRs() { function loadADRs() {
loadGenericList("/api/adrs", "adrs-container", (item) => ` loadGenericList("/api/adrs", "adrs-container", (item) => {
<strong>${item.id} | ${item.title}</strong> const statusLower = (item.status || "").toLowerCase();
<span style="float:right; background:var(--canvas-bg); padding:2px 6px; border-radius:4px; font-size:0.8em; border:1px solid var(--border-color); ${item.status === "superseded" ? "text-decoration: line-through; color:var(--error-color);" : "color:var(--success-color);"}">${item.status}</span> let badgeStyle = "color:var(--accent-color); border-color:rgba(88, 166, 255, 0.4); background:rgba(88, 166, 255, 0.12);";
<div style="font-size:0.9em; margin-bottom:8px; margin-top:8px;"><em>Context:</em> ${item.context || ""}</div> let badgeText = `● ${escapeHtml(item.status || "accepted")}`;
<div style="font-size:0.9em; margin-bottom:8px;"><em>Decision:</em> ${item.decision || ""}</div> if (statusLower === "implemented" || statusLower === "resolved") {
<div style="font-size:0.9em; margin-bottom:8px;"><em>Consequence:</em> ${item.consequence || ""}</div> badgeStyle = "color:var(--success-color); border-color:rgba(46, 160, 67, 0.4); background:rgba(46, 160, 67, 0.15); font-weight:600;";
${item.supersedes ? `<div style="font-size:0.9em; font-weight:bold; color:var(--warning-color);">Supersedes: ${item.supersedes}</div>` : ""} 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 `
<div style="display:flex; justify-content:space-between; align-items:center;">
<strong>${escapeHtml(item.id)} | ${escapeHtml(item.title)}</strong>
<span style="background:var(--canvas-bg); padding:2px 8px; border-radius:4px; font-size:0.8em; border:1px solid var(--border-color); ${badgeStyle}">${badgeText}</span>
</div>
${item.git_commit || item.git_branch || item.task_id || item.resolved_at ? `
<div style="font-size:0.8em; color:var(--text-secondary); margin-top:4px; font-family:monospace; display:flex; gap:12px; flex-wrap:wrap;">
${item.git_branch || item.git_commit ? `<span>\uD83D\uDCE6 ${escapeHtml(item.git_branch || "repo")} @ ${escapeHtml((item.git_commit || "").substring(0, 8))}</span>` : ""}
${item.task_id ? `<span>\uD83C\uDFAF Task: ${escapeHtml(item.task_id)}</span>` : ""}
${resolvedDate ? `<span>✓ Resolved: ${escapeHtml(resolvedDate)}</span>` : ""}
</div>
` : ""}
<div style="font-size:0.9em; margin-bottom:8px; margin-top:8px;"><em>Context:</em> ${escapeHtml(item.context || "")}</div>
<div style="font-size:0.9em; margin-bottom:8px;"><em>Decision:</em> ${escapeHtml(item.decision || "")}</div>
<div style="font-size:0.9em; margin-bottom:8px;"><em>Consequence:</em> ${escapeHtml(item.consequence || "")}</div>
${item.supersedes ? `<div style="font-size:0.9em; font-weight:bold; color:var(--warning-color);">Supersedes: ${escapeHtml(item.supersedes)}</div>` : ""}
`;
});
} }
function loadMemos() { function loadMemos() {
loadGenericList("/api/handoff_memos", "memos-container", (item) => ` loadGenericList("/api/handoff_memos", "memos-container", (item) => {
<strong>Memo from ${item.author || "System"}</strong> const blockers = Array.isArray(item.blockers) && item.blockers.length > 0 ? item.blockers : [];
<div style="white-space:pre-wrap; font-size:0.9em; line-height:1.4;">${item.content || item.summary || ""}</div> const actionItems = Array.isArray(item.action_items) && item.action_items.length > 0 ? item.action_items : [];
`); return `
<div style="display:flex; justify-content:space-between; align-items:center; gap:8px;">
<strong style="color:var(--text-primary); font-size:1em;">\uD83D\uDC64 Memo from ${escapeHtml(item.author || "System")}</strong>
<span style="font-size:0.75em; color:var(--text-secondary); background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">${escapeHtml(item.namespace || "global")}</span>
</div>
${item.git_branch || item.vcs_revision || item.session_id ? `
<div style="font-size:0.8em; color:var(--text-secondary); margin-top:3px; font-family:monospace; display:flex; gap:10px; flex-wrap:wrap;">
${item.git_branch ? `<span>\uD83C\uDF3F ${escapeHtml(item.git_branch)}</span>` : ""}
${item.vcs_revision ? `<span>Rev: ${escapeHtml(item.vcs_revision.substring(0, 8))}</span>` : ""}
${item.session_id ? `<span>Session: ${escapeHtml(item.session_id)}</span>` : ""}
</div>
` : ""}
<div style="white-space:pre-wrap; font-size:0.9em; line-height:1.4; margin-top:8px;">${escapeHtml(item.content || item.summary || "")}</div>
${blockers.length > 0 ? `
<div style="margin-top:8px; background:rgba(231,76,60,0.1); border:1px solid rgba(231,76,60,0.3); border-radius:4px; padding:6px 10px;">
<div style="color:var(--error-color); font-weight:bold; font-size:0.85em;">\uD83D\uDEAB Blockers:</div>
<ul style="margin:4px 0 0 16px; padding:0; font-size:0.85em; color:var(--text-primary);">
${blockers.map((b) => `<li>${escapeHtml(b)}</li>`).join("")}
</ul>
</div>
` : ""}
${actionItems.length > 0 ? `
<div style="margin-top:8px; background:rgba(52,152,219,0.1); border:1px solid rgba(52,152,219,0.3); border-radius:4px; padding:6px 10px;">
<div style="color:var(--accent-color); font-weight:bold; font-size:0.85em;">\uD83D\uDCCB Action Items:</div>
<ul style="margin:4px 0 0 16px; padding:0; font-size:0.85em; color:var(--text-primary);">
${actionItems.map((a) => `<li>${escapeHtml(a)}</li>`).join("")}
</ul>
</div>
` : ""}
`;
});
loadGenericList("/api/session_summaries", "summaries-container", (item) => ` loadGenericList("/api/session_summaries", "summaries-container", (item) => `
<div style="display:flex; justify-content:space-between; align-items:center;"> <div style="display:flex; justify-content:space-between; align-items:center;">
<strong>Session ${item.session_id ? escapeHtml(item.session_id) : "Summary"}</strong> <strong>Session ${item.session_id ? escapeHtml(item.session_id) : "Summary"}</strong>
@@ -855,17 +1027,75 @@ function loadMemos() {
${item.tasks_completed && item.tasks_completed.length > 0 ? `<div style="margin-top:8px; font-size:0.85em;"><strong>Tasks Completed:</strong><ul style="margin:4px 0 0 16px; padding:0;">${item.tasks_completed.map((t) => `<li>${escapeHtml(t)}</li>`).join("")}</ul></div>` : ""} ${item.tasks_completed && item.tasks_completed.length > 0 ? `<div style="margin-top:8px; font-size:0.85em;"><strong>Tasks Completed:</strong><ul style="margin:4px 0 0 16px; padding:0;">${item.tasks_completed.map((t) => `<li>${escapeHtml(t)}</li>`).join("")}</ul></div>` : ""}
${item.next_steps && item.next_steps.length > 0 ? `<div style="margin-top:6px; font-size:0.85em;"><strong>Next Steps:</strong><ul style="margin:4px 0 0 16px; padding:0;">${item.next_steps.map((s) => `<li>${escapeHtml(s)}</li>`).join("")}</ul></div>` : ""} ${item.next_steps && item.next_steps.length > 0 ? `<div style="margin-top:6px; font-size:0.85em;"><strong>Next Steps:</strong><ul style="margin:4px 0 0 16px; padding:0;">${item.next_steps.map((s) => `<li>${escapeHtml(s)}</li>`).join("")}</ul></div>` : ""}
`); `);
loadGenericList("/api/milestones", "milestones-container", (item) => ` loadGenericList("/api/milestones", "milestones-container", (item) => {
<strong>${item.title || item.name}</strong> const statusLower = (item.status || "pending").toLowerCase();
<div style="margin-top:5px; font-size:0.85em;">${item.description || ""}</div> 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 `
<div style="display:flex; justify-content:space-between; align-items:center; gap:8px;">
<strong style="font-size:1.05em; color:var(--text-primary);">${escapeHtml(item.title || item.name || "Milestone")}</strong>
<span style="background:${statusBg}; color:${statusColor}; border:1px solid ${statusColor}; font-size:0.75em; font-weight:bold; padding:2px 8px; border-radius:4px;">${escapeHtml(statusLabel)}</span>
</div>
${targetDate || completedDate ? `
<div style="font-size:0.8em; color:var(--text-secondary); margin-top:3px; display:flex; gap:10px;">
${targetDate ? `<span>\uD83C\uDFAF Target: ${escapeHtml(targetDate)}</span>` : ""}
${completedDate ? `<span>✓ Finished: ${escapeHtml(completedDate)}</span>` : ""}
</div>
` : ""}
<div style="margin-top:6px; font-size:0.9em; line-height:1.4; color:var(--text-primary);">${escapeHtml(item.description || "")}</div>
${deliverables.length > 0 ? `
<div style="margin-top:8px; font-size:0.85em;">
<strong>Deliverables:</strong>
<ul style="margin:4px 0 0 16px; padding:0;">
${deliverables.map((d) => `<li>${escapeHtml(d)}</li>`).join("")}
</ul>
</div>
` : ""}
${taskIds.length > 0 ? `
<div style="margin-top:8px; display:flex; gap:6px; flex-wrap:wrap; align-items:center;">
<span style="font-size:0.8em; color:var(--text-secondary);">Linked Tasks:</span>
${taskIds.map((tid) => `<span style="background:var(--canvas-bg); padding:1px 6px; border-radius:3px; font-size:0.75em; font-family:monospace; border:1px solid var(--border-color);">${escapeHtml(tid)}</span>`).join("")}
</div>
` : ""}
`;
});
} }
function loadSnippets() { function loadSnippets() {
loadGenericList("/api/snippets", "snippets-container", (item) => ` loadGenericList("/api/snippets", "snippets-container", (item) => {
<strong>${item.description || "Snippet"}</strong> const code = item.code || item.content || "";
<div style="margin-bottom:8px; font-size:0.8em;">Language: ${item.language || "txt"} | Tags: ${(item.tags || []).join(", ")}</div> const tags = Array.isArray(item.tags) ? item.tags : [];
<pre style="background:#111417; color:#d1d8e0; padding:10px; border-radius:4px; overflow-x:auto; font-size:0.85em;">${item.content || item.code || ""}</pre> const origin = item.origin_file ? `${item.origin_file}${item.line_range ? `:${item.line_range}` : ""}` : "";
`); return `
<div style="display:flex; justify-content:space-between; align-items:flex-start; gap:8px;">
<div>
<strong style="font-size:1.05em; color:var(--text-primary);">${escapeHtml(item.name || item.description || "Snippet")}</strong>
${item.language ? `<span style="background:var(--canvas-bg); color:var(--accent-color); border:1px solid var(--border-color); font-size:0.75em; font-weight:bold; padding:2px 6px; border-radius:4px; margin-left:6px; font-family:monospace;">${escapeHtml(item.language)}</span>` : ""}
</div>
<button class="complete-btn" style="position:static;" onclick='navigator.clipboard.writeText(${JSON.stringify(code)}).then(() => { this.innerText="✓ Copied"; setTimeout(() => { this.innerText="\uD83D\uDCCB Copy"; }, 1500); })' title="Copy to Clipboard">\uD83D\uDCCB Copy</button>
</div>
${item.description && item.description !== item.name ? `<div style="margin-top:4px; font-size:0.85em; color:var(--text-secondary);">${escapeHtml(item.description)}</div>` : ""}
${origin ? `<div style="margin-top:6px; font-family:monospace; font-size:0.8em; color:var(--accent-color);">\uD83D\uDCC4 ${escapeHtml(origin)}</div>` : ""}
<pre style="background:#111417; color:#d1d8e0; padding:10px; border-radius:4px; overflow-x:auto; font-size:0.85em; margin-top:8px; border:1px solid var(--border-color);">${escapeHtml(code)}</pre>
<div style="margin-top:8px; font-size:0.8em; display:flex; gap:6px; flex-wrap:wrap; align-items:center;">
${item.repo_name ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">\uD83D\uDCE6 ${escapeHtml(item.repo_name)}</span>` : ""}
${tags.map((t) => `<span style="background:var(--card-bg); padding:1px 6px; border-radius:3px; font-size:0.75em; border:1px solid var(--border-color); color:var(--text-secondary);">#${escapeHtml(t)}</span>`).join(" ")}
</div>
`;
});
} }
function loadAllExtras() { function loadAllExtras() {
loadTerminal(); loadTerminal();
+324 -60
View File
@@ -40,6 +40,11 @@ interface Entity {
entity_type: string; entity_type: string;
namespace?: string; namespace?: string;
observations?: string[]; observations?: string[];
git_branch?: string;
repo_name?: string;
file_path?: string;
created_at?: number;
updated_at?: number;
} }
interface Relation { interface Relation {
@@ -56,7 +61,18 @@ interface TaskItem {
parent_id?: string; parent_id?: string;
parentId?: string; parentId?: string;
dependencies?: 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 { interface ActivityItem {
@@ -217,14 +233,33 @@ function showInspector(nodeId: string): void {
const titleEl = document.getElementById("inspector-title"); const titleEl = document.getElementById("inspector-title");
if (titleEl) titleEl.innerText = entity.name; if (titleEl) titleEl.innerText = entity.name;
const typeEl = document.getElementById("inspector-type"); const metaEl = document.querySelector<HTMLElement>(".inspector-meta");
if (typeEl) typeEl.innerText = entity.entity_type; 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 = `
<div><strong>Type:</strong> <span id="inspector-type">${escapeHtml(entity.entity_type)}</span></div>
<div><strong>Namespace:</strong> <span id="inspector-namespace">${escapeHtml(entity.namespace || "global")}</span></div>
${entity.repo_name ? `<div><strong>Repository:</strong> <span style="font-family:monospace;">${escapeHtml(entity.repo_name)}</span></div>` : ""}
${entity.file_path ? `<div><strong>File:</strong> <span style="font-family:monospace; word-break:break-all;">${escapeHtml(entity.file_path)}</span></div>` : ""}
${entity.git_branch ? `<div><strong>Branch:</strong> <span style="font-family:monospace;">${escapeHtml(entity.git_branch)}</span></div>` : ""}
${createdStr ? `<div><strong>Created:</strong> <span>${escapeHtml(createdStr)}</span></div>` : ""}
${updatedStr ? `<div><strong>Updated:</strong> <span>${escapeHtml(updatedStr)}</span></div>` : ""}
`;
} else {
const typeEl = document.getElementById("inspector-type");
if (typeEl) typeEl.innerText = entity.entity_type;
const nsEl = document.getElementById("inspector-namespace"); const nsEl = document.getElementById("inspector-namespace");
if (nsEl) nsEl.innerText = entity.namespace || "global"; if (nsEl) nsEl.innerText = entity.namespace || "global";
}
const obsHtml = (entity.observations || []) const obsHtml = (entity.observations || [])
.map((o) => `<li>${o}</li>`) .map((o) => `<li>${escapeHtml(o)}</li>`)
.join(""); .join("");
const obsEl = document.getElementById("inspector-observations"); const obsEl = document.getElementById("inspector-observations");
if (obsEl) if (obsEl)
@@ -234,8 +269,8 @@ function showInspector(nodeId: string): void {
const relHtml = rels const relHtml = rels
.map((r) => { .map((r) => {
if (r.from === nodeId) if (r.from === nodeId)
return `<li>→ <em>${r.relation_type}</em> → <strong>${r.to}</strong></li>`; return `<li>→ <em>${escapeHtml(r.relation_type)}</em> → <strong>${escapeHtml(r.to)}</strong></li>`;
return `<li>← <em>${r.relation_type}</em> ← <strong>${r.from}</strong></li>`; return `<li>← <em>${escapeHtml(r.relation_type)}</em> ← <strong>${escapeHtml(r.from)}</strong></li>`;
}) })
.join(""); .join("");
@@ -486,16 +521,41 @@ function buildTaskTreeHTML(
} }
} }
const prio = (t.priority || "").toLowerCase();
let prioBadge = "";
if (prio === "critical" || prio === "urgent") {
prioBadge = `<span style="background:rgba(231,76,60,0.18); color:#e74c3c; border:1px solid rgba(231,76,60,0.5); font-size:0.7em; font-weight:bold; padding:2px 6px; border-radius:4px; text-transform:uppercase;">CRITICAL</span>`;
} else if (prio === "high") {
prioBadge = `<span style="background:rgba(230,126,34,0.18); color:#e67e22; border:1px solid rgba(230,126,34,0.5); font-size:0.7em; font-weight:bold; padding:2px 6px; border-radius:4px; text-transform:uppercase;">HIGH</span>`;
} else if (prio === "medium" || prio === "med") {
prioBadge = `<span style="background:rgba(52,152,219,0.15); color:#3498db; border:1px solid rgba(52,152,219,0.4); font-size:0.7em; font-weight:bold; padding:2px 6px; border-radius:4px; text-transform:uppercase;">MEDIUM</span>`;
} else if (prio === "low") {
prioBadge = `<span style="background:var(--canvas-bg); color:var(--text-secondary); border:1px solid var(--border-color); font-size:0.7em; padding:2px 6px; border-radius:4px; text-transform:uppercase;">LOW</span>`;
}
html += `<div class="${cardClass}" style="margin-left: ${depth * 20}px; margin-bottom: 10px;">`; html += `<div class="${cardClass}" style="margin-left: ${depth * 20}px; margin-bottom: 10px;">`;
html += `<div style="display:flex; justify-content:space-between; align-items:flex-start; gap:8px; margin-bottom:4px;">`;
html += `<div style="display:flex; align-items:center; gap:6px; flex-wrap:wrap;">`;
if (prioBadge) html += prioBadge;
if (t.assigned_agent) {
html += `<span style="background:var(--canvas-bg); color:var(--text-secondary); border:1px solid var(--border-color); font-size:0.7em; padding:2px 6px; border-radius:4px;">🤖 ${escapeHtml(t.assigned_agent)}</span>`;
}
if (isBlocked && !isCompleted && !isCancelled) { if (isBlocked && !isCompleted && !isCancelled) {
html += `<div style="background:#e74c3c; color:white; font-size:0.75em; padding:2px 6px; border-radius:3px; display:inline-block; margin-bottom:6px; font-weight:bold;">[BLOCKED]</div>`; html += `<span style="background:#e74c3c; color:white; font-size:0.7em; padding:2px 6px; border-radius:3px; font-weight:bold;">BLOCKED</span>`;
if (blockers.length > 0) {
html += `<div style="font-size:0.8em; color:#e74c3c; margin-bottom:6px;">Waiting on: ${blockers.join(", ")}</div>`;
}
} }
if (isCancelled) { if (isCancelled) {
html += `<div style="background:#95a5a6; color:white; font-size:0.75em; padding:2px 6px; border-radius:3px; display:inline-block; margin-bottom:6px; font-weight:bold;">[CANCELLED]</div>`; html += `<span style="background:#95a5a6; color:white; font-size:0.7em; padding:2px 6px; border-radius:3px; font-weight:bold;">CANCELLED</span>`;
}
html += `</div>`;
if (!isCompleted && !isCancelled && !isBlocked) {
html += `<button class="complete-btn" style="position:static;" onclick="completeTask('${escapeHtml(t.id)}')" title="Mark Completed">✓</button>`;
}
html += `</div>`;
if (isBlocked && blockers.length > 0 && !isCompleted && !isCancelled) {
html += `<div style="font-size:0.8em; color:#e74c3c; margin-bottom:6px;">Waiting on: ${escapeHtml(blockers.join(", "))}</div>`;
} }
html += `<strong>${escapeHtml(t.title)}</strong>`; html += `<strong>${escapeHtml(t.title)}</strong>`;
@@ -503,6 +563,10 @@ function buildTaskTreeHTML(
html += `<div style="margin-top:5px; font-size:0.9em; color:var(--text-secondary); line-height:1.4;">${escapeHtml(t.description)}</div>`; html += `<div style="margin-top:5px; font-size:0.9em; color:var(--text-secondary); line-height:1.4;">${escapeHtml(t.description)}</div>`;
} }
if (t.verification_command) {
html += `<div style="margin-top:6px; font-size:0.8em; font-family:monospace; background:var(--canvas-bg); padding:3px 8px; border-radius:4px; border:1px solid var(--border-color); color:var(--accent-color);">🧪 Test: ${escapeHtml(t.verification_command)}</div>`;
}
const criteria = t.acceptance_criteria || []; const criteria = t.acceptance_criteria || [];
if (criteria.length > 0) { if (criteria.length > 0) {
html += `<ul style="margin:8px 0 0 0; padding-left:20px; font-size: 0.9em; color: var(--text-secondary);">`; html += `<ul style="margin:8px 0 0 0; padding-left:20px; font-size: 0.9em; color: var(--text-secondary);">`;
@@ -512,18 +576,21 @@ function buildTaskTreeHTML(
if (!isMet) unmetCriteria = true; if (!isMet) unmetCriteria = true;
const check = isMet ? "☑" : "☐"; const check = isMet ? "☑" : "☐";
const strike = isMet ? "text-decoration: line-through;" : ""; const strike = isMet ? "text-decoration: line-through;" : "";
html += `<li style="${strike}">${check} ${c.description}</li>`; html += `<li style="${strike}">${check} ${escapeHtml(c.description)}${c.verification_command ? ` <code style="font-size:0.8em; background:var(--canvas-bg); padding:1px 4px; border-radius:3px;">(${escapeHtml(c.verification_command)})</code>` : ""}</li>`;
}); });
html += `</ul>`; html += `</ul>`;
if (unmetCriteria && !isCompleted && !isCancelled) isBlocked = true; if (unmetCriteria && !isCompleted && !isCancelled) isBlocked = true;
} }
html += progressHtml; if (t.git_branch || t.repo_name) {
html += `<div style="margin-top:8px; font-size:0.75em; display:flex; gap:6px; flex-wrap:wrap; color:var(--text-secondary);">`;
if (!isCompleted && !isCancelled && !isBlocked) { if (t.repo_name) html += `<span style="background:var(--canvas-bg); padding:1px 6px; border-radius:3px; border:1px solid var(--border-color);">📦 ${escapeHtml(t.repo_name)}</span>`;
html += `<button class="complete-btn" onclick="completeTask('${t.id}')" title="Mark Completed">✓</button>`; if (t.git_branch) html += `<span style="background:var(--canvas-bg); padding:1px 6px; border-radius:3px; border:1px solid var(--border-color);">🌿 ${escapeHtml(t.git_branch)}</span>`;
html += `</div>`;
} }
html += progressHtml;
if (allChildren.length > 0) { if (allChildren.length > 0) {
html += `<div style="margin-top: 15px; border-left: 2px solid var(--border-color); padding-left: 10px;">`; html += `<div style="margin-top: 15px; border-left: 2px solid var(--border-color); padding-left: 10px;">`;
html += buildTaskTreeHTML(tasks, t.id, 0); html += buildTaskTreeHTML(tasks, t.id, 0);
@@ -969,7 +1036,6 @@ async function loadGenericList(
containerId: string, containerId: string,
formatter: (item: any) => string, formatter: (item: any) => string,
): Promise<void> { ): Promise<void> {
const container = document.getElementById(containerId);
try { try {
const res = await fetch(endpoint); const res = await fetch(endpoint);
if (!res.ok) { if (!res.ok) {
@@ -979,6 +1045,7 @@ async function loadGenericList(
return; return;
} }
const data = await res.json(); const data = await res.json();
const container = document.getElementById(containerId);
if (!container) return; if (!container) return;
if (!data || data.length === 0) { if (!data || data.length === 0) {
@@ -1073,14 +1140,37 @@ function loadTerminal(): void {
loadGenericList( loadGenericList(
"/api/terminal/history", "/api/terminal/history",
"terminal-container", "terminal-container",
(item) => ` (item) => {
<span style="font-weight:bold; font-size:1.05em; color:var(--text-primary);">${item.command}</span> const isSuccess = item.exit_code === 0;
<span style="margin-left:15px; font-size:0.85em;"><span style="font-weight:bold; color:var(--text-secondary);">Exit Code:</span> <span style="color:${item.exit_code === 0 ? "var(--success-color)" : "var(--error-color)"}; font-weight:bold;">${item.exit_code}</span></span> const statusColor = isSuccess ? "var(--success-color)" : "var(--error-color)";
<div style="margin-top:10px; font-size:0.85em;"> const statusBg = isSuccess ? "rgba(46, 204, 113, 0.12)" : "rgba(231, 76, 60, 0.12)";
<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; margin-right:8px; border:1px solid var(--border-color);">OS: ${item.os || "Unknown"}</span> const durationStr = item.duration_ms != null ? (item.duration_ms < 1000 ? `${item.duration_ms}ms` : `${(item.duration_ms / 1000).toFixed(2)}s`) : "";
<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; font-family:monospace; border:1px solid var(--border-color);">CWD: ${item.cwd || "Unknown"}</span>
return `
<div style="display:flex; justify-content:space-between; align-items:flex-start; gap:10px;">
<span style="font-family:monospace; font-weight:bold; font-size:1.05em; color:var(--text-primary); word-break:break-all;">$ ${escapeHtml(item.command)}</span>
<div style="display:flex; gap:6px; align-items:center; flex-shrink:0;">
${durationStr ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; font-size:0.75em; border:1px solid var(--border-color); color:var(--text-secondary); font-family:monospace;">⏱️ ${escapeHtml(durationStr)}</span>` : ""}
<span style="background:${statusBg}; color:${statusColor}; border:1px solid ${statusColor}; font-size:0.75em; font-weight:bold; padding:2px 6px; border-radius:4px; font-family:monospace;">Exit ${item.exit_code}</span>
</div>
</div> </div>
`, <div style="margin-top:8px; font-size:0.85em; display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
${item.shell ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color); font-weight:600;">🐚 ${escapeHtml(item.shell)}</span>` : ""}
<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">OS: ${escapeHtml(item.os || "Unknown")}</span>
<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; font-family:monospace; border:1px solid var(--border-color); word-break:break-all;">📂 ${escapeHtml(item.cwd || "Unknown")}</span>
${item.repo_name ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">📦 ${escapeHtml(item.repo_name)}</span>` : ""}
${item.invoker ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color); color:var(--text-secondary);">👤 ${escapeHtml(item.invoker)}</span>` : ""}
</div>
${item.status_reason ? `<div style="margin-top:6px; font-size:0.85em; color:var(--error-color);"><strong>Reason:</strong> ${escapeHtml(item.status_reason)}</div>` : ""}
${item.stdout_summary ? `<div style="margin-top:6px; font-size:0.85em; color:var(--text-secondary);"><strong>Output:</strong> ${escapeHtml(item.stdout_summary)}</div>` : ""}
${item.error_output ? `
<details style="margin-top:8px;" ${!isSuccess ? "open" : ""}>
<summary style="font-size:0.8em; color:var(--error-color); cursor:pointer; font-weight:600;">Error Output</summary>
<pre style="background:#1a0f0f; color:#ff6b6b; padding:8px; border-radius:4px; border:1px solid rgba(231,76,60,0.3); font-size:0.8em; overflow-x:auto; margin-top:4px; white-space:pre-wrap;">${escapeHtml(item.error_output)}</pre>
</details>
` : ""}
`;
},
); );
} }
@@ -1088,40 +1178,123 @@ function loadTechDebt(): void {
loadGenericList( loadGenericList(
"/api/tech_debts", "/api/tech_debts",
"techdebt-container", "techdebt-container",
(item) => ` (item) => {
<strong>${item.id}</strong> ${item.is_resolved ? '<span style="color:var(--success-color); font-weight:bold; background: rgba(46, 204, 113, 0.1); padding: 2px 6px; border-radius: 4px; border: 1px solid var(--success-color); margin-left: 8px;">(Resolved)</span>' : '<span style="color:var(--open-color); font-weight:bold; background: rgba(230, 126, 34, 0.1); padding: 2px 6px; border-radius: 4px; border: 1px solid var(--open-color); margin-left: 8px;">(Open)</span>'} const sev = (item.severity || "medium").toLowerCase();
<div style="margin-top:5px;"><strong>Description:</strong> ${item.description}</div> let sevBg = "rgba(241, 196, 15, 0.15)";
<div style="margin-top:5px;"><strong>Ideal Solution:</strong> ${item.ideal_solution}</div> let sevColor = "#f1c40f";
<div style="margin-top:10px;"> let sevBorder = "rgba(241, 196, 15, 0.4)";
<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; font-size:0.8em; margin-right:8px; border:1px solid var(--border-color);">Git Commit: ${item.git_commit || "None"}</span> if (sev === "critical" || sev === "blocker") {
<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; font-size:0.8em; border:1px solid var(--border-color);">Branch: ${item.git_branch || "None"}</span> 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
? '<span style="color:var(--success-color); font-weight:bold; background: rgba(46, 204, 113, 0.15); padding: 2px 6px; border-radius: 4px; border: 1px solid var(--success-color);">✓ Resolved</span>'
: '<span style="color:var(--open-color); font-weight:bold; background: rgba(230, 126, 34, 0.15); padding: 2px 6px; border-radius: 4px; border: 1px solid var(--open-color);">● Open</span>';
const fileLoc = item.file_path
? `${item.file_path}${item.line_range ? `:${item.line_range}` : ""}`
: "";
return `
<div style="display:flex; justify-content:space-between; align-items:center; gap:8px;">
<div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap;">
<strong style="font-family:monospace; font-size:1em;">${escapeHtml(item.id)}</strong>
<span style="background:${sevBg}; color:${sevColor}; border:1px solid ${sevBorder}; font-size:0.75em; font-weight:bold; padding:2px 6px; border-radius:4px; text-transform:uppercase;">${escapeHtml(item.severity || "medium")}</span>
${item.effort_estimate ? `<span style="background:var(--canvas-bg); font-size:0.75em; padding:2px 6px; border-radius:4px; border:1px solid var(--border-color); color:var(--text-secondary);">⏳ ${escapeHtml(item.effort_estimate)}</span>` : ""}
</div>
<div>${statusBadge}</div>
</div> </div>
`, ${fileLoc ? `<div style="margin-top:6px; font-family:monospace; font-size:0.85em; color:var(--accent-color);">📄 ${escapeHtml(fileLoc)}</div>` : ""}
<div style="margin-top:6px; font-size:0.9em; line-height:1.4;"><strong>Description:</strong> ${escapeHtml(item.description || "")}</div>
${item.workaround ? `<div style="margin-top:4px; font-size:0.85em; color:var(--text-secondary); background:var(--canvas-bg); padding:4px 8px; border-radius:4px; border-left:3px solid #f39c12;"><strong>Workaround:</strong> ${escapeHtml(item.workaround)}</div>` : ""}
<div style="margin-top:6px; font-size:0.9em; line-height:1.4; color:var(--text-secondary);"><strong>Ideal Solution:</strong> ${escapeHtml(item.ideal_solution || "")}</div>
<div style="margin-top:8px; font-size:0.8em; display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
${item.repo_name ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">📦 ${escapeHtml(item.repo_name)}</span>` : ""}
<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; font-family:monospace; border:1px solid var(--border-color);">Commit: ${item.git_commit ? escapeHtml(item.git_commit.substring(0, 8)) : "None"}</span>
<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">🌿 ${escapeHtml(item.git_branch || "None")}</span>
<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color); color:var(--text-secondary);">🏷️ ${escapeHtml(item.namespace || "global")}</span>
</div>
`;
},
); );
loadGenericList( loadGenericList(
"/api/error_fixes", "/api/error_fixes",
"errorfixes-container", "errorfixes-container",
(item) => ` (item) => `
<strong>${item.signature || "Error"}</strong> <span style="color:var(--success-color); font-weight:bold; background: rgba(46, 204, 113, 0.1); padding: 2px 6px; border-radius: 4px; border: 1px solid var(--success-color); margin-left: 8px;">(Resolved)</span> <div style="display:flex; justify-content:space-between; align-items:flex-start; gap:8px;">
<div style="font-size:0.9em; margin-bottom:8px; margin-top:5px;"><em>Solution:</em> ${item.solution || ""}</div> <div>
<div style="font-family:monospace; font-size:0.85em; color:#e74c3c;">Commit: ${item.git_commit || "None"}</div> <strong style="color:var(--text-primary); font-family:monospace; font-size:1em;">${escapeHtml(item.signature || "Error")}</strong>
${item.error_category ? `<span style="background:rgba(231,76,60,0.15); color:var(--error-color); border:1px solid rgba(231,76,60,0.4); font-size:0.75em; font-weight:bold; padding:2px 6px; border-radius:4px; margin-left:6px;">${escapeHtml(item.error_category)}</span>` : ""}
${item.toolchain ? `<span style="background:var(--canvas-bg); color:var(--text-secondary); border:1px solid var(--border-color); font-size:0.75em; padding:2px 6px; border-radius:4px; margin-left:6px; font-family:monospace;">🔧 ${escapeHtml(item.toolchain)}</span>` : ""}
</div>
<span style="color:var(--success-color); font-weight:bold; background: rgba(46, 204, 113, 0.15); padding: 2px 6px; border-radius: 4px; border: 1px solid var(--success-color); flex-shrink:0;">✓ Resolved</span>
</div>
<div style="margin-top:8px; font-size:0.9em; line-height:1.4;"><strong>Solution:</strong> ${escapeHtml(item.solution || "")}</div>
${item.stack_trace ? `
<details style="margin-top:8px;">
<summary style="font-size:0.8em; color:var(--text-secondary); cursor:pointer; font-weight:600;">Stack Trace</summary>
<pre style="background:#111417; color:#ff7b72; padding:8px; border-radius:4px; border:1px solid var(--border-color); font-size:0.8em; overflow-x:auto; margin-top:4px; white-space:pre-wrap;">${escapeHtml(item.stack_trace)}</pre>
</details>
` : ""}
<div style="margin-top:8px; font-size:0.8em; display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
${item.repo_name ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">📦 ${escapeHtml(item.repo_name)}</span>` : ""}
<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; font-family:monospace; border:1px solid var(--border-color);">Commit: ${item.git_commit ? escapeHtml(item.git_commit.substring(0, 8)) : "None"}</span>
${item.git_branch ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">🌿 ${escapeHtml(item.git_branch)}</span>` : ""}
</div>
`, `,
); );
} }
function loadADRs(): void { function loadADRs(): void {
loadGenericList( loadGenericList(
"/api/adrs", "/api/adrs",
"adrs-container", "adrs-container",
(item) => ` (item) => {
<strong>${item.id} | ${item.title}</strong> const statusLower = (item.status || "").toLowerCase();
<span style="float:right; background:var(--canvas-bg); padding:2px 6px; border-radius:4px; font-size:0.8em; border:1px solid var(--border-color); ${item.status === "superseded" ? "text-decoration: line-through; color:var(--error-color);" : "color:var(--success-color);"}">${item.status}</span> let badgeStyle = "color:var(--accent-color); border-color:rgba(88, 166, 255, 0.4); background:rgba(88, 166, 255, 0.12);";
<div style="font-size:0.9em; margin-bottom:8px; margin-top:8px;"><em>Context:</em> ${item.context || ""}</div> let badgeText = `● ${escapeHtml(item.status || "accepted")}`;
<div style="font-size:0.9em; margin-bottom:8px;"><em>Decision:</em> ${item.decision || ""}</div>
<div style="font-size:0.9em; margin-bottom:8px;"><em>Consequence:</em> ${item.consequence || ""}</div> if (statusLower === "implemented" || statusLower === "resolved") {
${item.supersedes ? `<div style="font-size:0.9em; font-weight:bold; color:var(--warning-color);">Supersedes: ${item.supersedes}</div>` : ""} 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 `
<div style="display:flex; justify-content:space-between; align-items:center;">
<strong>${escapeHtml(item.id)} | ${escapeHtml(item.title)}</strong>
<span style="background:var(--canvas-bg); padding:2px 8px; border-radius:4px; font-size:0.8em; border:1px solid var(--border-color); ${badgeStyle}">${badgeText}</span>
</div>
${(item.git_commit || item.git_branch || item.task_id || item.resolved_at) ? `
<div style="font-size:0.8em; color:var(--text-secondary); margin-top:4px; font-family:monospace; display:flex; gap:12px; flex-wrap:wrap;">
${item.git_branch || item.git_commit ? `<span>📦 ${escapeHtml(item.git_branch || "repo")} @ ${escapeHtml((item.git_commit || "").substring(0, 8))}</span>` : ""}
${item.task_id ? `<span>🎯 Task: ${escapeHtml(item.task_id)}</span>` : ""}
${resolvedDate ? `<span>✓ Resolved: ${escapeHtml(resolvedDate)}</span>` : ""}
</div>
` : ""}
<div style="font-size:0.9em; margin-bottom:8px; margin-top:8px;"><em>Context:</em> ${escapeHtml(item.context || "")}</div>
<div style="font-size:0.9em; margin-bottom:8px;"><em>Decision:</em> ${escapeHtml(item.decision || "")}</div>
<div style="font-size:0.9em; margin-bottom:8px;"><em>Consequence:</em> ${escapeHtml(item.consequence || "")}</div>
${item.supersedes ? `<div style="font-size:0.9em; font-weight:bold; color:var(--warning-color);">Supersedes: ${escapeHtml(item.supersedes)}</div>` : ""}
`;
},
); );
} }
@@ -1129,10 +1302,41 @@ function loadMemos(): void {
loadGenericList( loadGenericList(
"/api/handoff_memos", "/api/handoff_memos",
"memos-container", "memos-container",
(item) => ` (item) => {
<strong>Memo from ${item.author || "System"}</strong> const blockers = Array.isArray(item.blockers) && item.blockers.length > 0 ? item.blockers : [];
<div style="white-space:pre-wrap; font-size:0.9em; line-height:1.4;">${item.content || item.summary || ""}</div> const actionItems = Array.isArray(item.action_items) && item.action_items.length > 0 ? item.action_items : [];
`,
return `
<div style="display:flex; justify-content:space-between; align-items:center; gap:8px;">
<strong style="color:var(--text-primary); font-size:1em;">👤 Memo from ${escapeHtml(item.author || "System")}</strong>
<span style="font-size:0.75em; color:var(--text-secondary); background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">${escapeHtml(item.namespace || "global")}</span>
</div>
${(item.git_branch || item.vcs_revision || item.session_id) ? `
<div style="font-size:0.8em; color:var(--text-secondary); margin-top:3px; font-family:monospace; display:flex; gap:10px; flex-wrap:wrap;">
${item.git_branch ? `<span>🌿 ${escapeHtml(item.git_branch)}</span>` : ""}
${item.vcs_revision ? `<span>Rev: ${escapeHtml(item.vcs_revision.substring(0, 8))}</span>` : ""}
${item.session_id ? `<span>Session: ${escapeHtml(item.session_id)}</span>` : ""}
</div>
` : ""}
<div style="white-space:pre-wrap; font-size:0.9em; line-height:1.4; margin-top:8px;">${escapeHtml(item.content || item.summary || "")}</div>
${blockers.length > 0 ? `
<div style="margin-top:8px; background:rgba(231,76,60,0.1); border:1px solid rgba(231,76,60,0.3); border-radius:4px; padding:6px 10px;">
<div style="color:var(--error-color); font-weight:bold; font-size:0.85em;">🚫 Blockers:</div>
<ul style="margin:4px 0 0 16px; padding:0; font-size:0.85em; color:var(--text-primary);">
${blockers.map((b: string) => `<li>${escapeHtml(b)}</li>`).join("")}
</ul>
</div>
` : ""}
${actionItems.length > 0 ? `
<div style="margin-top:8px; background:rgba(52,152,219,0.1); border:1px solid rgba(52,152,219,0.3); border-radius:4px; padding:6px 10px;">
<div style="color:var(--accent-color); font-weight:bold; font-size:0.85em;">📋 Action Items:</div>
<ul style="margin:4px 0 0 16px; padding:0; font-size:0.85em; color:var(--text-primary);">
${actionItems.map((a: string) => `<li>${escapeHtml(a)}</li>`).join("")}
</ul>
</div>
` : ""}
`;
},
); );
loadGenericList( loadGenericList(
@@ -1153,10 +1357,54 @@ function loadMemos(): void {
loadGenericList( loadGenericList(
"/api/milestones", "/api/milestones",
"milestones-container", "milestones-container",
(item) => ` (item) => {
<strong>${item.title || item.name}</strong> const statusLower = (item.status || "pending").toLowerCase();
<div style="margin-top:5px; font-size:0.85em;">${item.description || ""}</div> 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 `
<div style="display:flex; justify-content:space-between; align-items:center; gap:8px;">
<strong style="font-size:1.05em; color:var(--text-primary);">${escapeHtml(item.title || item.name || "Milestone")}</strong>
<span style="background:${statusBg}; color:${statusColor}; border:1px solid ${statusColor}; font-size:0.75em; font-weight:bold; padding:2px 8px; border-radius:4px;">${escapeHtml(statusLabel)}</span>
</div>
${targetDate || completedDate ? `
<div style="font-size:0.8em; color:var(--text-secondary); margin-top:3px; display:flex; gap:10px;">
${targetDate ? `<span>🎯 Target: ${escapeHtml(targetDate)}</span>` : ""}
${completedDate ? `<span>✓ Finished: ${escapeHtml(completedDate)}</span>` : ""}
</div>
` : ""}
<div style="margin-top:6px; font-size:0.9em; line-height:1.4; color:var(--text-primary);">${escapeHtml(item.description || "")}</div>
${deliverables.length > 0 ? `
<div style="margin-top:8px; font-size:0.85em;">
<strong>Deliverables:</strong>
<ul style="margin:4px 0 0 16px; padding:0;">
${deliverables.map((d: string) => `<li>${escapeHtml(d)}</li>`).join("")}
</ul>
</div>
` : ""}
${taskIds.length > 0 ? `
<div style="margin-top:8px; display:flex; gap:6px; flex-wrap:wrap; align-items:center;">
<span style="font-size:0.8em; color:var(--text-secondary);">Linked Tasks:</span>
${taskIds.map((tid: string) => `<span style="background:var(--canvas-bg); padding:1px 6px; border-radius:3px; font-size:0.75em; font-family:monospace; border:1px solid var(--border-color);">${escapeHtml(tid)}</span>`).join("")}
</div>
` : ""}
`;
},
); );
} }
@@ -1164,14 +1412,30 @@ function loadSnippets(): void {
loadGenericList( loadGenericList(
"/api/snippets", "/api/snippets",
"snippets-container", "snippets-container",
(item) => ` (item) => {
<strong>${item.description || "Snippet"}</strong> const code = item.code || item.content || "";
<div style="margin-bottom:8px; font-size:0.8em;">Language: ${item.language || "txt"} | Tags: ${(item.tags || []).join(", ")}</div> const tags = Array.isArray(item.tags) ? item.tags : [];
<pre style="background:#111417; color:#d1d8e0; padding:10px; border-radius:4px; overflow-x:auto; font-size:0.85em;">${item.content || item.code || ""}</pre> const origin = item.origin_file ? `${item.origin_file}${item.line_range ? `:${item.line_range}` : ""}` : "";
`,
return `
<div style="display:flex; justify-content:space-between; align-items:flex-start; gap:8px;">
<div>
<strong style="font-size:1.05em; color:var(--text-primary);">${escapeHtml(item.name || item.description || "Snippet")}</strong>
${item.language ? `<span style="background:var(--canvas-bg); color:var(--accent-color); border:1px solid var(--border-color); font-size:0.75em; font-weight:bold; padding:2px 6px; border-radius:4px; margin-left:6px; font-family:monospace;">${escapeHtml(item.language)}</span>` : ""}
</div>
<button class="complete-btn" style="position:static;" onclick='navigator.clipboard.writeText(${JSON.stringify(code)}).then(() => { this.innerText="✓ Copied"; setTimeout(() => { this.innerText="📋 Copy"; }, 1500); })' title="Copy to Clipboard">📋 Copy</button>
</div>
${item.description && item.description !== item.name ? `<div style="margin-top:4px; font-size:0.85em; color:var(--text-secondary);">${escapeHtml(item.description)}</div>` : ""}
${origin ? `<div style="margin-top:6px; font-family:monospace; font-size:0.8em; color:var(--accent-color);">📄 ${escapeHtml(origin)}</div>` : ""}
<pre style="background:#111417; color:#d1d8e0; padding:10px; border-radius:4px; overflow-x:auto; font-size:0.85em; margin-top:8px; border:1px solid var(--border-color);">${escapeHtml(code)}</pre>
<div style="margin-top:8px; font-size:0.8em; display:flex; gap:6px; flex-wrap:wrap; align-items:center;">
${item.repo_name ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">📦 ${escapeHtml(item.repo_name)}</span>` : ""}
${tags.map((t: string) => `<span style="background:var(--card-bg); padding:1px 6px; border-radius:3px; font-size:0.75em; border:1px solid var(--border-color); color:var(--text-secondary);">#${escapeHtml(t)}</span>`).join(" ")}
</div>
`;
},
); );
} }
function loadAllExtras(): void { function loadAllExtras(): void {
loadTerminal(); loadTerminal();
loadTechDebt(); loadTechDebt();
+144 -2
View File
@@ -378,7 +378,7 @@ impl McpTool for DecisionsHandler {
fn schema(&self) -> Value { fn schema(&self) -> Value {
crate::mcp::tool_def::<DecisionsTool>( crate::mcp::tool_def::<DecisionsTool>(
"decisions", "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 context = req.context.unwrap_or_default();
let decision = req.decision.unwrap_or_default(); let decision = req.decision.unwrap_or_default();
let consequence = req.consequences.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 idx = state.get_search_index().await;
let mut final_id = String::new(); let mut final_id = String::new();
@@ -404,7 +410,7 @@ impl McpTool for DecisionsHandler {
state.code.adrs.modify(|adrs| { state.code.adrs.modify(|adrs| {
if let Some(superseded_id) = &req.supersedes { if let Some(superseded_id) = &req.supersedes {
for old_adr in adrs.iter_mut() { 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(); old_adr.status = "superseded".to_string();
adrs_to_index.push(old_adr.clone()); adrs_to_index.push(old_adr.clone());
break; break;
@@ -427,6 +433,10 @@ impl McpTool for DecisionsHandler {
alternatives_considered: req.alternatives_considered.unwrap_or_default(), alternatives_considered: req.alternatives_considered.unwrap_or_default(),
affected_components: req.affected_components.unwrap_or_default(), affected_components: req.affected_components.unwrap_or_default(),
author: req.author, 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()); adrs_to_index.push(a.clone());
@@ -444,6 +454,94 @@ impl McpTool for DecisionsHandler {
); );
Ok(format!("Logged decision {}: {}", final_id, title)) 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 => { DecisionAction::Query => {
let limit = req.limit.unwrap_or(20); let limit = req.limit.unwrap_or(20);
let include_body = req.include_body.unwrap_or(true); 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.title, q)
|| crate::handlers::utils::contains_ignore_ascii_case(&a.context, 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.decision, q)
|| crate::handlers::utils::contains_ignore_ascii_case(&a.consequence, q)
} else { } else {
true true
} }
@@ -476,6 +575,10 @@ impl McpTool for DecisionsHandler {
"title": a.title, "title": a.title,
"status": a.status, "status": a.status,
"timestamp": a.timestamp, "timestamp": a.timestamp,
"git_commit": a.git_commit,
"git_branch": a.git_branch,
"resolved_at": a.resolved_at,
"task_id": a.task_id,
}) })
}) })
.collect(); .collect();
@@ -1787,6 +1890,11 @@ mod tests {
.map_err(|e| crate::error::AppError::Internal(e.to_string())) .map_err(|e| crate::error::AppError::Internal(e.to_string()))
.unwrap(); .unwrap();
assert_eq!(res1, "Logged decision ADR-0001: Architecture"); 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 debt_handler = TechDebtHandler;
let args_debt = json!({ let args_debt = json!({
@@ -2000,6 +2108,40 @@ mod tests {
.await .await
.unwrap(); .unwrap();
assert!(q_dec_res.contains("Use Axum")); 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 let del_dec_res = handler_dec
.execute(serde_json::json!({"action": "delete", "id": "ADR-0001"}), state.clone()) .execute(serde_json::json!({"action": "delete", "id": "ADR-0001"}), state.clone())
+9
View File
@@ -367,6 +367,7 @@ pub struct Adr {
pub title: String, pub title: String,
pub context: String, pub context: String,
pub decision: String, pub decision: String,
#[serde(alias = "consequences")]
pub consequence: String, pub consequence: String,
#[serde(default = "default_adr_status")] #[serde(default = "default_adr_status")]
pub status: String, pub status: String,
@@ -383,6 +384,14 @@ pub struct Adr {
pub affected_components: Vec<String>, pub affected_components: Vec<String>,
#[serde(default)] #[serde(default)]
pub author: Option<String>, pub author: Option<String>,
#[serde(default)]
pub git_commit: Option<String>,
#[serde(default)]
pub git_branch: Option<String>,
#[serde(default)]
pub resolved_at: Option<u64>,
#[serde(default)]
pub task_id: Option<String>,
} }
fn default_adr_status() -> String { fn default_adr_status() -> String {
+14 -4
View File
@@ -792,7 +792,7 @@ pub struct SnippetsTool {
pub line_range: Option<String>, pub line_range: Option<String>,
} }
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)] #[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq, Clone)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum DecisionAction { pub enum DecisionAction {
#[serde(alias = "log", alias = "LOG", alias = "Log")] #[serde(alias = "log", alias = "LOG", alias = "Log")]
@@ -801,15 +801,18 @@ pub enum DecisionAction {
Query, Query,
#[serde(alias = "delete", alias = "DELETE", alias = "Delete")] #[serde(alias = "delete", alias = "DELETE", alias = "Delete")]
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: /// Actions:
/// - 'log': Record ADR. Required: title. Optional: status, context, decision, consequences, namespace. /// - 'log': Record ADR. Required: title. Optional: status, context, decision, consequences, namespace.
/// - 'query': Search ADRs. Optional: query, 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. /// - 'delete': Remove ADR. Required: id.
/// Next steps on error: Provide title for 'log' or id for 'delete'. /// Next steps on error: Provide title for 'log' or id for 'update'/'delete'.
#[derive(Debug, Deserialize, Serialize, JsonSchema)] #[derive(Deserialize, Serialize, JsonSchema, Debug, Clone)]
pub struct DecisionsTool { pub struct DecisionsTool {
/// Action to perform: 'log', 'query', or 'delete'. /// Action to perform: 'log', 'query', or 'delete'.
pub action: DecisionAction, pub action: DecisionAction,
@@ -824,6 +827,7 @@ pub struct DecisionsTool {
/// Decision made (for 'log'). /// Decision made (for 'log').
pub decision: Option<String>, pub decision: Option<String>,
/// Consequences & tradeoffs (for 'log'). /// Consequences & tradeoffs (for 'log').
#[serde(alias = "consequence")]
pub consequences: Option<String>, pub consequences: Option<String>,
/// Search query string (for 'query'). /// Search query string (for 'query').
pub query: Option<String>, pub query: Option<String>,
@@ -843,6 +847,12 @@ pub struct DecisionsTool {
pub limit: Option<usize>, pub limit: Option<usize>,
/// Include the full body of the matched decisions. Defaults to true. /// Include the full body of the matched decisions. Defaults to true.
pub include_body: Option<bool>, pub include_body: Option<bool>,
/// Optional Git commit where decision was implemented.
pub git_commit: Option<String>,
/// Optional Git branch where decision was implemented.
pub git_branch: Option<String>,
/// Optional task ID linked to this decision.
pub task_id: Option<String>,
} }
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)] #[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]