2376 lines
105 KiB
JavaScript
2376 lines
105 KiB
JavaScript
// server/src/dashboard.ts
|
||
var currentTabId = "activity-tab";
|
||
function switchTab(tabId, btn) {
|
||
currentTabId = tabId;
|
||
document.querySelectorAll(".tab-content").forEach((el) => el.classList.remove("active"));
|
||
document.querySelectorAll(".tab-button").forEach((el) => el.classList.remove("active"));
|
||
const targetTab = document.getElementById(tabId);
|
||
if (targetTab)
|
||
targetTab.classList.add("active");
|
||
const targetBtn = btn || document.querySelector(`.tab-button[onclick*="'${tabId}'"]`);
|
||
if (targetBtn)
|
||
targetBtn.classList.add("active");
|
||
switch (tabId) {
|
||
case "graph-tab":
|
||
if (network) {
|
||
network.redraw();
|
||
} else {
|
||
loadGraph();
|
||
}
|
||
break;
|
||
case "activity-tab":
|
||
loadActivityHistory();
|
||
break;
|
||
case "task-tab":
|
||
loadTasks();
|
||
break;
|
||
case "techdebt-tab":
|
||
loadTechDebt();
|
||
break;
|
||
case "adrs-tab":
|
||
loadADRs();
|
||
break;
|
||
case "memos-tab":
|
||
loadMemos();
|
||
break;
|
||
case "snippets-tab":
|
||
loadSnippets();
|
||
break;
|
||
case "terminal-tab":
|
||
loadTerminal();
|
||
break;
|
||
case "ledger-tab":
|
||
loadLedger();
|
||
break;
|
||
case "env-tab":
|
||
loadEnvironment();
|
||
break;
|
||
}
|
||
}
|
||
function toggleTheme() {
|
||
const currentTheme = document.documentElement.getAttribute("data-theme");
|
||
const newTheme = currentTheme === "dark" ? "light" : "dark";
|
||
document.documentElement.setAttribute("data-theme", newTheme);
|
||
localStorage.setItem("theme", newTheme);
|
||
}
|
||
if (localStorage.getItem("theme") === "dark" || !localStorage.getItem("theme") && window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||
document.documentElement.setAttribute("data-theme", "dark");
|
||
}
|
||
var network = null;
|
||
var nodesData = new vis.DataSet;
|
||
var edgesData = new vis.DataSet;
|
||
var rawEntities = {};
|
||
var rawRelations = [];
|
||
var activeFilters = new Set;
|
||
var allTypes = new Set;
|
||
var typeColors = {
|
||
Concept: { bg: "#9b59b6", border: "#8e44ad" },
|
||
Technology: { bg: "#3498db", border: "#2980b9" },
|
||
Infrastructure: { bg: "#e67e22", border: "#d35400" },
|
||
Database_Table: { bg: "#f1c40f", border: "#f39c12" },
|
||
Service: { bg: "#1abc9c", border: "#f39c12" },
|
||
Tool: { bg: "#e74c3c", border: "#c0392b" },
|
||
Integration: { bg: "#2ecc71", border: "#27ae60" },
|
||
System: { bg: "#34495e", border: "#2c3e50" },
|
||
Rule: { bg: "#fd79a8", border: "#e84393" }
|
||
};
|
||
function getColorForType(type) {
|
||
if (typeColors[type])
|
||
return typeColors[type];
|
||
let hash = 0;
|
||
for (let i = 0;i < type.length; i++)
|
||
hash = type.charCodeAt(i) + ((hash << 5) - hash);
|
||
const hue = Math.abs(hash) % 360;
|
||
return {
|
||
background: `hsl(${hue}, 70%, 60%)`,
|
||
border: `hsl(${hue}, 70%, 40%)`
|
||
};
|
||
}
|
||
function zoomGraph(step) {
|
||
if (!network)
|
||
return;
|
||
const currentScale = network.getScale();
|
||
network.moveTo({ scale: currentScale * (1 + step) });
|
||
}
|
||
function resetGraph() {
|
||
if (!network)
|
||
return;
|
||
network.fit({
|
||
animation: { duration: 500, easingFunction: "easeInOutQuad" }
|
||
});
|
||
}
|
||
function closeInspector() {
|
||
const inspector = document.getElementById("inspector-panel");
|
||
if (inspector)
|
||
inspector.classList.remove("open");
|
||
if (network)
|
||
network.unselectAll();
|
||
}
|
||
function showInspector(nodeId) {
|
||
const entity = rawEntities[nodeId];
|
||
if (!entity)
|
||
return;
|
||
const titleEl = document.getElementById("inspector-title");
|
||
if (titleEl)
|
||
titleEl.innerText = entity.name;
|
||
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 = `
|
||
<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");
|
||
if (nsEl)
|
||
nsEl.innerText = entity.namespace || "global";
|
||
}
|
||
const obsHtml = (entity.observations || []).map((o) => `<li>${escapeHtml(o)}</li>`).join("");
|
||
const obsEl = document.getElementById("inspector-observations");
|
||
if (obsEl)
|
||
obsEl.innerHTML = obsHtml || "<li><em>No observations recorded.</em></li>";
|
||
const rels = rawRelations.filter((r) => r.from === nodeId || r.to === nodeId);
|
||
const relHtml = rels.map((r) => {
|
||
if (r.from === nodeId)
|
||
return `<li>→ <em>${escapeHtml(r.relation_type)}</em> → <strong>${escapeHtml(r.to)}</strong></li>`;
|
||
return `<li>← <em>${escapeHtml(r.relation_type)}</em> ← <strong>${escapeHtml(r.from)}</strong></li>`;
|
||
}).join("");
|
||
const relEl = document.getElementById("inspector-relations");
|
||
if (relEl)
|
||
relEl.innerHTML = relHtml || "<li><em>No direct relations.</em></li>";
|
||
const inspector = document.getElementById("inspector-panel");
|
||
if (inspector)
|
||
inspector.classList.add("open");
|
||
}
|
||
function toggleFilter(type) {
|
||
if (activeFilters.has(type))
|
||
activeFilters.delete(type);
|
||
else
|
||
activeFilters.add(type);
|
||
renderFilters();
|
||
updateGraphData();
|
||
}
|
||
function renderFilters() {
|
||
const container = document.getElementById("graph-filters");
|
||
if (!container)
|
||
return;
|
||
container.innerHTML = Array.from(allTypes).sort().map((type) => {
|
||
const isActive = activeFilters.has(type);
|
||
const color = getColorForType(type).bg || getColorForType(type).background;
|
||
return `<button class="filter-chip ${isActive ? "active" : ""}" onclick="toggleFilter('${type}')">
|
||
<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${color};margin-right:4px;"></span>
|
||
${type}
|
||
</button>`;
|
||
}).join("");
|
||
}
|
||
var lastGraphJson = "";
|
||
function updateGraphData() {
|
||
const newNodes = [];
|
||
const newEdges = [];
|
||
const nodeIds = new Set;
|
||
for (const [name, entity] of Object.entries(rawEntities)) {
|
||
allTypes.add(entity.entity_type);
|
||
if (activeFilters.size > 0 && !activeFilters.has(entity.entity_type))
|
||
continue;
|
||
const color = getColorForType(entity.entity_type);
|
||
newNodes.push({
|
||
id: name,
|
||
label: name,
|
||
title: `${name}
|
||
Type: ${entity.entity_type}`,
|
||
color: { background: color.bg || color.background, border: color.border },
|
||
font: {
|
||
color: document.documentElement.getAttribute("data-theme") === "dark" ? "#eee" : "#333"
|
||
}
|
||
});
|
||
nodeIds.add(name);
|
||
}
|
||
rawRelations.forEach((r, idx) => {
|
||
if (nodeIds.has(r.from) && nodeIds.has(r.to)) {
|
||
newEdges.push({
|
||
id: `${r.from}-${r.to}-${r.relation_type}-${idx}`,
|
||
from: r.from,
|
||
to: r.to,
|
||
label: r.relation_type,
|
||
arrows: "to",
|
||
font: {
|
||
color: document.documentElement.getAttribute("data-theme") === "dark" ? "#aaa" : "#666",
|
||
strokeWidth: 0
|
||
}
|
||
});
|
||
}
|
||
});
|
||
nodesData.clear();
|
||
if (newNodes.length > 0)
|
||
nodesData.add(newNodes);
|
||
edgesData.clear();
|
||
if (newEdges.length > 0)
|
||
edgesData.add(newEdges);
|
||
renderFilters();
|
||
}
|
||
async function loadGraph() {
|
||
try {
|
||
const res = await fetch("/api/graph", { cache: "no-store" });
|
||
const dataText = await res.text();
|
||
if (dataText === lastGraphJson && network) {
|
||
return;
|
||
}
|
||
lastGraphJson = dataText;
|
||
const data = JSON.parse(dataText);
|
||
rawEntities = data.entities || {};
|
||
rawRelations = data.relations || [];
|
||
updateGraphData();
|
||
if (!network) {
|
||
const container = document.getElementById("network-container");
|
||
if (!container)
|
||
return;
|
||
const options = {
|
||
nodes: { shape: "dot", size: 16, font: { size: 12 } },
|
||
edges: {
|
||
color: { inherit: "from", opacity: 0.6 },
|
||
font: { size: 10, align: "middle" },
|
||
smooth: { type: "continuous" }
|
||
},
|
||
physics: {
|
||
enabled: true,
|
||
solver: "barnesHut",
|
||
barnesHut: {
|
||
gravitationalConstant: -3000,
|
||
centralGravity: 0.3,
|
||
springLength: 95,
|
||
springConstant: 0.04,
|
||
damping: 0.09,
|
||
avoidOverlap: 0.1
|
||
},
|
||
stabilization: {
|
||
enabled: true,
|
||
iterations: 150,
|
||
updateInterval: 25,
|
||
fit: true
|
||
}
|
||
},
|
||
interaction: { hover: true, tooltipDelay: 100, zoomView: false }
|
||
};
|
||
network = new vis.Network(container, { nodes: nodesData, edges: edgesData }, options);
|
||
network.once("stabilizationIterationsDone", function() {
|
||
network.setOptions({ physics: { enabled: false } });
|
||
});
|
||
network.on("dragStart", function() {
|
||
network.setOptions({ physics: { enabled: true } });
|
||
});
|
||
network.on("dragEnd", function() {
|
||
network.setOptions({ physics: { enabled: false } });
|
||
});
|
||
container.addEventListener("wheel", function(event) {
|
||
event.preventDefault();
|
||
const direction = event.deltaY > 0 ? -0.15 : 0.15;
|
||
zoomGraph(direction);
|
||
}, { passive: false });
|
||
network.on("selectNode", function(params) {
|
||
if (params.nodes.length > 0)
|
||
showInspector(params.nodes[0]);
|
||
});
|
||
network.on("deselectNode", function(params) {
|
||
if (params.nodes.length === 0)
|
||
closeInspector();
|
||
});
|
||
}
|
||
} catch (err) {
|
||
console.error("Failed to load graph", err);
|
||
const container = document.getElementById("network-container");
|
||
if (container) {
|
||
container.innerHTML = '<div style="color:#e74c3c;padding:20px;font-weight:bold;">Error loading graph: ' + err.message + "</div>";
|
||
}
|
||
}
|
||
}
|
||
async function completeTask(id) {
|
||
try {
|
||
if (activeWS && activeWS.readyState === WebSocket.OPEN) {
|
||
await sendWsRpc("tasks/complete", { id });
|
||
} else {
|
||
await fetch(`/api/tasks/${encodeURIComponent(id)}/complete`, { method: "POST" });
|
||
}
|
||
loadTasks();
|
||
} catch (e) {
|
||
console.error("Failed to complete task via WS, falling back to HTTP", e);
|
||
try {
|
||
await fetch(`/api/tasks/${encodeURIComponent(id)}/complete`, { method: "POST" });
|
||
loadTasks();
|
||
} catch (err) {
|
||
console.error("Failed to complete task via HTTP fallback", err);
|
||
}
|
||
}
|
||
}
|
||
function buildTaskTreeHTML(tasks, parentId, depth = 0) {
|
||
let html = "";
|
||
const children = tasks.filter((t) => {
|
||
const pid = t.parent_id || t.parentId;
|
||
if (!parentId)
|
||
return !pid;
|
||
return pid === parentId;
|
||
});
|
||
if (children.length === 0)
|
||
return html;
|
||
children.forEach((t) => {
|
||
const isCompleted = t.status === "completed" || t.status === "done";
|
||
const isCancelled = t.status === "cancelled" || t.status === "abandoned";
|
||
let cardClass = "task-card";
|
||
if (isCompleted)
|
||
cardClass += " completed";
|
||
if (isCancelled)
|
||
cardClass += " cancelled";
|
||
let isBlocked = false;
|
||
let blockers = [];
|
||
const deps = t.dependencies || [];
|
||
deps.forEach((depId) => {
|
||
const depTask = tasks.find((dt) => dt.id === depId);
|
||
if (depTask && depTask.status !== "completed" && depTask.status !== "done") {
|
||
isBlocked = true;
|
||
blockers.push(depTask.title);
|
||
}
|
||
});
|
||
const allChildren = tasks.filter((ct) => (ct.parentId || ct.parent_id) === t.id);
|
||
const completedChildren = allChildren.filter((ct) => ct.status === "completed" || ct.status === "done");
|
||
let progressHtml = "";
|
||
if (allChildren.length > 0) {
|
||
const pct = Math.round(completedChildren.length / allChildren.length * 100);
|
||
progressHtml = `
|
||
<div style="margin-top:10px; background:#e1e8ed; border-radius:4px; height:8px; overflow:hidden;">
|
||
<div style="background:#3498db; width:${pct}%; height:100%; transition:width 0.3s;"></div>
|
||
</div>
|
||
<div style="font-size:0.8em; color:var(--text-secondary); text-align:right; margin-top:2px;">${pct}% (${completedChildren.length}/${allChildren.length} child tasks)</div>
|
||
`;
|
||
if (completedChildren.length < allChildren.length) {
|
||
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 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) {
|
||
html += `<span style="background:#e74c3c; color:white; font-size:0.7em; padding:2px 6px; border-radius:3px; font-weight:bold;">BLOCKED</span>`;
|
||
}
|
||
if (isCancelled) {
|
||
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>`;
|
||
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>`;
|
||
}
|
||
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 || [];
|
||
if (criteria.length > 0) {
|
||
html += `<ul style="margin:8px 0 0 0; padding-left:20px; font-size: 0.9em; color: var(--text-secondary);">`;
|
||
let unmetCriteria = false;
|
||
criteria.forEach((c) => {
|
||
const isMet = c.is_met;
|
||
if (!isMet)
|
||
unmetCriteria = true;
|
||
const check = isMet ? "☑" : "☐";
|
||
const strike = isMet ? "text-decoration: line-through;" : "";
|
||
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>`;
|
||
if (unmetCriteria && !isCompleted && !isCancelled)
|
||
isBlocked = true;
|
||
}
|
||
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 (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) {
|
||
html += `<div style="margin-top: 15px; border-left: 2px solid var(--border-color); padding-left: 10px;">`;
|
||
html += buildTaskTreeHTML(tasks, t.id, 0);
|
||
html += `</div>`;
|
||
}
|
||
html += `</div>`;
|
||
});
|
||
return html;
|
||
}
|
||
var cachedTasks = [];
|
||
var taskFilterQuery = "";
|
||
var taskStatusFilter = "";
|
||
function handleTaskFilter(val) {
|
||
taskFilterQuery = val.toLowerCase().trim();
|
||
applyTaskFilters();
|
||
}
|
||
function handleTaskStatusFilter(val) {
|
||
taskStatusFilter = val.toLowerCase().trim();
|
||
applyTaskFilters();
|
||
}
|
||
function applyTaskFilters() {
|
||
const taskContainer = document.getElementById("task-tree-container");
|
||
if (!taskContainer)
|
||
return;
|
||
let filtered = cachedTasks;
|
||
if (taskFilterQuery) {
|
||
filtered = filtered.filter((t) => t.title && t.title.toLowerCase().includes(taskFilterQuery) || t.description && t.description.toLowerCase().includes(taskFilterQuery) || t.id && t.id.toLowerCase().includes(taskFilterQuery) || t.assigned_agent && t.assigned_agent.toLowerCase().includes(taskFilterQuery));
|
||
}
|
||
if (taskStatusFilter) {
|
||
if (taskStatusFilter === "blocked") {
|
||
filtered = filtered.filter((t) => {
|
||
const deps = t.dependencies || [];
|
||
return deps.some((dId) => {
|
||
const dt = cachedTasks.find((x) => x.id === dId);
|
||
return dt && dt.status !== "completed" && dt.status !== "done";
|
||
});
|
||
});
|
||
} else {
|
||
filtered = filtered.filter((t) => (t.status || "pending").toLowerCase() === taskStatusFilter);
|
||
}
|
||
}
|
||
const rootHtml = buildTaskTreeHTML(filtered, null, 0);
|
||
if (!rootHtml) {
|
||
if (cachedTasks.length === 0) {
|
||
taskContainer.innerHTML = '<div class="enterprise-empty-state">No active tasks.</div>';
|
||
} else {
|
||
taskContainer.innerHTML = '<div class="enterprise-empty-state">No tasks matching current filter.</div>';
|
||
}
|
||
} else {
|
||
taskContainer.innerHTML = rootHtml;
|
||
}
|
||
}
|
||
async function loadTasks() {
|
||
try {
|
||
const res = await fetch("/api/tasks", { cache: "no-store" });
|
||
cachedTasks = await res.json();
|
||
applyTaskFilters();
|
||
} catch (err) {
|
||
console.error("Failed to load tasks", err);
|
||
}
|
||
}
|
||
function highlightText(text, query) {
|
||
if (!query)
|
||
return text;
|
||
const regex = new RegExp(`(${query})`, "gi");
|
||
return text.replace(regex, '<span class="search-highlight">$1</span>');
|
||
}
|
||
var searchDebounceTimer = null;
|
||
var activeSearchAbortController = null;
|
||
async function handleSearch(e) {
|
||
const input = e.target;
|
||
const q = input.value.trim();
|
||
if (searchDebounceTimer)
|
||
clearTimeout(searchDebounceTimer);
|
||
if (!q) {
|
||
if (activeSearchAbortController)
|
||
activeSearchAbortController.abort();
|
||
const resultsEl = document.getElementById("search-results");
|
||
if (resultsEl)
|
||
resultsEl.innerHTML = "";
|
||
return;
|
||
}
|
||
const delay = e.key === "Enter" ? 0 : 250;
|
||
searchDebounceTimer = setTimeout(async () => {
|
||
if (activeSearchAbortController)
|
||
activeSearchAbortController.abort();
|
||
activeSearchAbortController = new AbortController;
|
||
try {
|
||
const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`, {
|
||
signal: activeSearchAbortController.signal
|
||
});
|
||
const data = await res.json();
|
||
const container = document.getElementById("search-results");
|
||
if (!container)
|
||
return;
|
||
if (!data.results || data.results.length === 0) {
|
||
container.innerHTML = '<div style="padding:15px;color:var(--text-secondary);">No results found.</div>';
|
||
return;
|
||
}
|
||
container.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 style="display:flex; justify-content:space-between; align-items:flex-start;">
|
||
<div>
|
||
<strong>${highlightText(r.title, q)}</strong>
|
||
<span style="font-size: 0.7em; color: var(--text-secondary); background: var(--canvas-bg); padding: 2px 6px; border-radius: 4px; margin-left: 8px; text-transform: uppercase; border: 1px solid var(--border-color);">${r.type_name}</span>
|
||
</div>
|
||
<div style="font-size:0.75em; color:var(--text-secondary);">${r.score.toFixed(2)}</div>
|
||
</div>
|
||
<div style="margin-top:6px; font-size:0.9em; line-height:1.4;">${highlightText(r.content.substring(0, 150), q)}${r.content.length > 150 ? "..." : ""}</div>
|
||
<div style="font-size:0.75em; color:var(--text-secondary); margin-top:8px; font-family:monospace; background: var(--canvas-bg); padding: 2px 4px; border-radius: 3px; display: inline-block;">ID: ${r.id}</div>
|
||
</div>
|
||
`).join("");
|
||
} catch (err) {
|
||
if (err.name !== "AbortError") {
|
||
console.error("Search error:", err);
|
||
}
|
||
}
|
||
}, delay);
|
||
}
|
||
var MAX_ACTIVITY_HISTORY = 100;
|
||
function escapeHtml(str) {
|
||
if (str === null || str === undefined)
|
||
return "";
|
||
return String(str).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
||
}
|
||
function parseActivityPayload(item) {
|
||
let category = "TOOL";
|
||
let summary = "";
|
||
let details = "";
|
||
let timestamp = Date.now();
|
||
if (typeof item === "string") {
|
||
try {
|
||
const parsed = JSON.parse(item);
|
||
return parseActivityPayload(parsed);
|
||
} catch (e) {
|
||
summary = item;
|
||
}
|
||
} else if (typeof item === "object" && item !== null) {
|
||
if (item.method === "notifications/activity" && item.params) {
|
||
return parseActivityPayload(item.params);
|
||
}
|
||
category = (item.category || item.type || "TOOL").toUpperCase();
|
||
let rawSum = item.summary || item.message || item.description || item.data;
|
||
if (rawSum !== undefined && rawSum !== null) {
|
||
if (typeof rawSum === "object") {
|
||
summary = JSON.stringify(rawSum);
|
||
} else {
|
||
summary = String(rawSum);
|
||
}
|
||
}
|
||
if (!summary) {
|
||
if (item.title)
|
||
summary = String(item.title);
|
||
else if (item.name)
|
||
summary = String(item.name);
|
||
else if (item.action)
|
||
summary = String(item.action);
|
||
else if (item.content)
|
||
summary = String(item.content);
|
||
else {
|
||
const {
|
||
timestamp: _t,
|
||
time: _tm,
|
||
updated_at: _u,
|
||
category: _c,
|
||
type: _tp,
|
||
method: _m,
|
||
params: _p,
|
||
jsonrpc: _j,
|
||
...rest
|
||
} = item;
|
||
if (Object.keys(rest).length > 0) {
|
||
summary = JSON.stringify(rest);
|
||
} else {
|
||
summary = JSON.stringify(item);
|
||
}
|
||
}
|
||
}
|
||
let rawDet = item.details;
|
||
if (rawDet !== undefined && rawDet !== null) {
|
||
if (typeof rawDet === "object") {
|
||
details = JSON.stringify(rawDet);
|
||
} else {
|
||
details = String(rawDet);
|
||
}
|
||
}
|
||
timestamp = item.timestamp || item.time || item.updated_at || timestamp;
|
||
if (typeof timestamp === "number" && timestamp < 10000000000) {
|
||
timestamp = timestamp * 1000;
|
||
}
|
||
}
|
||
const catUpper = category.toUpperCase();
|
||
const colorMap = {
|
||
GRAPH: "#2ecc71",
|
||
DECISION: "#f39c12",
|
||
CODE: "#3498db",
|
||
CODE_CHANGE: "#3498db",
|
||
TASK: "#1abc9c",
|
||
TASK_CREATE: "#1abc9c",
|
||
TASK_UPDATE: "#1abc9c",
|
||
CLIPBOARD: "#9b59b6",
|
||
HANDOFF_MEMO: "#e67e22",
|
||
CHECKPOINT: "#e74c3c",
|
||
ERROR_FIX: "#e74c3c",
|
||
TECH_DEBT: "#d35400",
|
||
SUBAGENT: "#8e44ad",
|
||
AGENT_SIGNAL: "#8e44ad",
|
||
SNIPPET: "#16a085",
|
||
TOOL: "#3498db",
|
||
EDITOR: "#9b59b6",
|
||
TERMINAL: "#27ae60",
|
||
SNAPSHOT: "#f1c40f",
|
||
NAMESPACE: "#e67e22",
|
||
SYSTEM: "#95a5a6"
|
||
};
|
||
const badgeColor = colorMap[catUpper] || "#3498db";
|
||
const dateObj = new Date(timestamp);
|
||
const validDate = isNaN(dateObj.getTime()) ? new Date : dateObj;
|
||
const timeStr = validDate.toLocaleString([], {
|
||
month: "2-digit",
|
||
day: "2-digit",
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
second: "2-digit"
|
||
});
|
||
let html = `<span class="time">[${timeStr}]</span> <span style="background:var(--canvas-bg); color:${badgeColor}; padding:2px 6px; border-radius:3px; font-weight:bold; margin-right:6px; border:1px solid ${badgeColor}60; text-transform:uppercase; font-size:0.75em;">${escapeHtml(catUpper)}</span> ${escapeHtml(summary)}`;
|
||
if (details) {
|
||
html += `<div style="font-size:0.85em; color:var(--text-secondary); margin-left:20px; margin-top:2px;">${escapeHtml(details)}</div>`;
|
||
}
|
||
return html;
|
||
}
|
||
async function loadActivityHistory() {
|
||
try {
|
||
const response = await fetch("/api/activity");
|
||
const history = await response.json();
|
||
const feed = document.getElementById("activity-feed");
|
||
if (!feed)
|
||
return;
|
||
feed.innerHTML = "";
|
||
if (!history || history.length === 0) {
|
||
feed.innerHTML = '<div style="padding:40px 20px; color:#a4b0be; text-align:center; font-family:sans-serif;">No recent activity recorded yet.<br/><span style="font-size:0.85em; opacity:0.8;">Live activity will stream here in real-time as MCP tools and system events execute.</span></div>';
|
||
return;
|
||
}
|
||
const getMillis = (item) => {
|
||
if (!item)
|
||
return 0;
|
||
if (typeof item === "string") {
|
||
try {
|
||
item = JSON.parse(item);
|
||
} catch (e) {
|
||
return 0;
|
||
}
|
||
}
|
||
if (item.method === "notifications/activity" && item.params) {
|
||
item = item.params;
|
||
}
|
||
let val = item.timestamp || item.time || item.updated_at || 0;
|
||
if (typeof val === "number" && val < 10000000000) {
|
||
val = val * 1000;
|
||
}
|
||
return val;
|
||
};
|
||
const sorted = [...history].sort((a, b) => getMillis(b) - getMillis(a));
|
||
sorted.forEach((item) => {
|
||
const div = document.createElement("div");
|
||
div.className = "feed-entry";
|
||
div.innerHTML = parseActivityPayload(item);
|
||
feed.appendChild(div);
|
||
});
|
||
if (sorted.length > 0) {
|
||
feed.scrollTop = 0;
|
||
}
|
||
} catch (e) {
|
||
console.error("Failed to load activity history", e);
|
||
}
|
||
}
|
||
var wsConnected = false;
|
||
var activeSSE = null;
|
||
var activeWS = null;
|
||
var wsRpcId = 1;
|
||
var pendingWsRequests = new Map;
|
||
function sendWsRpc(method, params = {}, timeoutMs = 5000) {
|
||
return new Promise((resolve, reject) => {
|
||
if (!activeWS || activeWS.readyState !== WebSocket.OPEN) {
|
||
return reject(new Error("WebSocket not open"));
|
||
}
|
||
const id = ++wsRpcId;
|
||
const timer = setTimeout(() => {
|
||
pendingWsRequests.delete(id);
|
||
reject(new Error(`WebSocket RPC timeout for ${method}`));
|
||
}, timeoutMs);
|
||
pendingWsRequests.set(id, { resolve, reject, timer });
|
||
try {
|
||
activeWS.send(JSON.stringify({
|
||
jsonrpc: "2.0",
|
||
id,
|
||
method,
|
||
params
|
||
}));
|
||
} catch (e) {
|
||
clearTimeout(timer);
|
||
pendingWsRequests.delete(id);
|
||
reject(e);
|
||
}
|
||
});
|
||
}
|
||
var tabRefreshDebounceTimers = {};
|
||
function requestDomainRefresh(domain) {
|
||
const tabMap = {
|
||
graph: "graph-tab",
|
||
task: "task-tab",
|
||
techdebt: "techdebt-tab",
|
||
adrs: "adrs-tab",
|
||
snippets: "snippets-tab",
|
||
terminal: "terminal-tab",
|
||
ledger: "ledger-tab",
|
||
memos: "memos-tab",
|
||
env: "env-tab",
|
||
gates: "env-tab"
|
||
};
|
||
const targetTab = tabMap[domain];
|
||
if (!targetTab || targetTab !== currentTabId) {
|
||
return;
|
||
}
|
||
if (tabRefreshDebounceTimers[domain]) {
|
||
clearTimeout(tabRefreshDebounceTimers[domain]);
|
||
}
|
||
tabRefreshDebounceTimers[domain] = setTimeout(() => {
|
||
delete tabRefreshDebounceTimers[domain];
|
||
switch (domain) {
|
||
case "graph":
|
||
loadGraph();
|
||
break;
|
||
case "task":
|
||
loadTasks();
|
||
break;
|
||
case "techdebt":
|
||
loadTechDebt();
|
||
break;
|
||
case "adrs":
|
||
loadADRs();
|
||
break;
|
||
case "snippets":
|
||
loadSnippets();
|
||
break;
|
||
case "terminal":
|
||
loadTerminal();
|
||
break;
|
||
case "ledger":
|
||
loadLedger();
|
||
break;
|
||
case "memos":
|
||
loadMemos();
|
||
break;
|
||
case "env":
|
||
case "gates":
|
||
loadEnvironment();
|
||
break;
|
||
}
|
||
}, 250);
|
||
}
|
||
function handleIncomingActivity(payload) {
|
||
let category = "";
|
||
let method = "";
|
||
if (typeof payload === "object" && payload !== null) {
|
||
method = payload.method || "";
|
||
if (payload.params) {
|
||
category = (payload.params.category || payload.params.type || "").toUpperCase();
|
||
} else {
|
||
category = (payload.category || payload.type || "").toUpperCase();
|
||
}
|
||
}
|
||
if (method === "notifications/resources/updated" || category === "GRAPH") {
|
||
requestDomainRefresh("graph");
|
||
} else if (category === "DECISION" || category === "ADR") {
|
||
requestDomainRefresh("adrs");
|
||
requestDomainRefresh("graph");
|
||
} else if (method === "notifications/task/completed" || category.startsWith("TASK")) {
|
||
requestDomainRefresh("task");
|
||
} else if (category === "GATE" || category === "QUALITY_GATE" || category === "ENVIRONMENT" || category === "ENV") {
|
||
requestDomainRefresh("gates");
|
||
requestDomainRefresh("env");
|
||
} else if (category === "TECH_DEBT" || category === "ERROR_FIX" || category === "CHECKPOINT") {
|
||
requestDomainRefresh("techdebt");
|
||
} else if (category === "SNIPPET") {
|
||
requestDomainRefresh("snippets");
|
||
} else if (category === "TERMINAL") {
|
||
requestDomainRefresh("terminal");
|
||
} else if (category === "CODE_CHANGE" || category === "LEDGER") {
|
||
requestDomainRefresh("ledger");
|
||
} else if (category === "MEMO" || category === "MILESTONE" || category === "HANDOFF_MEMO") {
|
||
requestDomainRefresh("memos");
|
||
}
|
||
const feed = document.getElementById("activity-feed");
|
||
if (feed) {
|
||
if (feed.querySelector(".feed-entry") === null) {
|
||
feed.innerHTML = "";
|
||
}
|
||
const isScrolledToTop = feed.scrollTop <= 20;
|
||
const div = document.createElement("div");
|
||
div.className = "feed-entry";
|
||
div.innerHTML = parseActivityPayload(payload);
|
||
feed.prepend(div);
|
||
while (feed.children.length > MAX_ACTIVITY_HISTORY && feed.lastChild) {
|
||
feed.removeChild(feed.lastChild);
|
||
}
|
||
if (isScrolledToTop)
|
||
feed.scrollTop = 0;
|
||
}
|
||
}
|
||
function setupWS() {
|
||
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
||
let ws;
|
||
try {
|
||
ws = new WebSocket(`${protocol}//${location.host}/ws?client=ui`);
|
||
} catch (e) {
|
||
console.error("WebSocket creation error", e);
|
||
setupSSE();
|
||
return;
|
||
}
|
||
ws.onopen = function() {
|
||
wsConnected = true;
|
||
activeWS = ws;
|
||
if (activeSSE) {
|
||
activeSSE.close();
|
||
activeSSE = null;
|
||
}
|
||
};
|
||
ws.onmessage = function(event) {
|
||
try {
|
||
const data = JSON.parse(event.data);
|
||
if (data.id !== undefined && pendingWsRequests.has(data.id)) {
|
||
const handler = pendingWsRequests.get(data.id);
|
||
pendingWsRequests.delete(data.id);
|
||
clearTimeout(handler.timer);
|
||
if (data.error) {
|
||
handler.reject(new Error(data.error.message || JSON.stringify(data.error)));
|
||
} else {
|
||
handler.resolve(data.result);
|
||
}
|
||
return;
|
||
}
|
||
if (data.type === "activity" || data.type === "gate" || data.type === "task" || data.type === "tech_debt" || data.type === "nvim_telemetry" || data.method === "notifications/activity" || data.method === "notifications/task/completed" || data.method === "notifications/resources/updated") {
|
||
const payload = data.params || data.data || data;
|
||
handleIncomingActivity(payload);
|
||
}
|
||
} catch (e) {
|
||
console.error("WebSocket message parse error", e);
|
||
}
|
||
};
|
||
ws.onclose = function() {
|
||
wsConnected = false;
|
||
activeWS = null;
|
||
pendingWsRequests.forEach((handler) => {
|
||
clearTimeout(handler.timer);
|
||
handler.reject(new Error("WebSocket disconnected"));
|
||
});
|
||
pendingWsRequests.clear();
|
||
console.log("WebSocket closed, falling back to SSE and retrying WS in 5s...");
|
||
setupSSE();
|
||
setTimeout(setupWS, 5000);
|
||
};
|
||
ws.onerror = function() {
|
||
wsConnected = false;
|
||
activeWS = null;
|
||
pendingWsRequests.forEach((handler) => {
|
||
clearTimeout(handler.timer);
|
||
handler.reject(new Error("WebSocket error"));
|
||
});
|
||
pendingWsRequests.clear();
|
||
setupSSE();
|
||
};
|
||
}
|
||
var listControllers = new Map;
|
||
function applyListFiltering(containerId) {
|
||
const state = listControllers.get(containerId);
|
||
if (!state)
|
||
return;
|
||
let result = state.items.slice();
|
||
const q = (state.searchQuery || "").trim().toLowerCase();
|
||
if (q) {
|
||
result = result.filter((item) => {
|
||
if (state.options.searchFields) {
|
||
const fields = state.options.searchFields(item);
|
||
return fields.some((f) => f && String(f).toLowerCase().includes(q));
|
||
}
|
||
return JSON.stringify(item).toLowerCase().includes(q);
|
||
});
|
||
}
|
||
if (state.options.filters) {
|
||
for (const filterDef of state.options.filters) {
|
||
const activeVal = state.activeFilters[filterDef.id];
|
||
if (activeVal !== undefined && activeVal !== "") {
|
||
result = result.filter((item) => filterDef.predicate(item, activeVal));
|
||
}
|
||
}
|
||
}
|
||
const factor = state.sortOrder === "desc" ? -1 : 1;
|
||
const extractor = state.options.sortExtractor || ((item) => {
|
||
if (state.options.timestampExtractor) {
|
||
const ts = state.options.timestampExtractor(item);
|
||
if (ts != null)
|
||
return ts;
|
||
}
|
||
return item.timestamp || item.created_at || item.id || 0;
|
||
});
|
||
result.sort((a, b) => {
|
||
const valA = extractor(a);
|
||
const valB = extractor(b);
|
||
if (typeof valA === "number" && typeof valB === "number") {
|
||
return (valA - valB) * factor;
|
||
}
|
||
return String(valA).localeCompare(String(valB)) * factor;
|
||
});
|
||
state.filteredItems = result;
|
||
}
|
||
function renderListCardsAndPagination(containerId) {
|
||
const state = listControllers.get(containerId);
|
||
if (!state)
|
||
return;
|
||
const totalItems = state.filteredItems.length;
|
||
const pageSize = state.pageSize;
|
||
const totalPages = pageSize <= 0 ? 1 : Math.max(1, Math.ceil(totalItems / pageSize));
|
||
if (state.currentPage > totalPages) {
|
||
state.currentPage = totalPages;
|
||
}
|
||
if (state.currentPage < 1) {
|
||
state.currentPage = 1;
|
||
}
|
||
const startIndex = totalItems === 0 ? 0 : (state.currentPage - 1) * (pageSize > 0 ? pageSize : totalItems) + 1;
|
||
const endIndex = pageSize <= 0 ? totalItems : Math.min(state.currentPage * pageSize, totalItems);
|
||
const metaEl = document.getElementById(`${containerId}-meta`);
|
||
if (metaEl) {
|
||
const itemNoun = state.options.itemNoun || "items";
|
||
let summaryText = "";
|
||
if (totalItems === 0) {
|
||
summaryText = `0 ${itemNoun}`;
|
||
} else {
|
||
summaryText = `Showing <strong>${startIndex}–${endIndex}</strong> of <strong>${totalItems}</strong> ${itemNoun}`;
|
||
}
|
||
if (totalItems !== state.items.length) {
|
||
summaryText += ` <span style="color:var(--text-secondary);">(filtered from ${state.items.length} total)</span>`;
|
||
}
|
||
metaEl.innerHTML = `<div>${summaryText}</div><div>Page <strong>${state.currentPage}</strong> of <strong>${totalPages}</strong></div>`;
|
||
}
|
||
const cardsEl = document.getElementById(`${containerId}-cards`);
|
||
if (cardsEl) {
|
||
if (totalItems === 0) {
|
||
const itemNoun = state.options.itemNoun || "items";
|
||
if (state.items.length === 0) {
|
||
cardsEl.innerHTML = `<div class="enterprise-empty-state">No ${itemNoun} recorded.</div>`;
|
||
} else {
|
||
cardsEl.innerHTML = `
|
||
<div class="enterprise-empty-state">
|
||
<div>\uD83D\uDD0D No matching ${itemNoun} found for current filter.</div>
|
||
<button class="enterprise-btn" style="margin-top:12px;" onclick="onListResetFilters('${containerId}')">Reset Filters</button>
|
||
</div>`;
|
||
}
|
||
} else {
|
||
const pageSlice = pageSize <= 0 ? state.filteredItems : state.filteredItems.slice((state.currentPage - 1) * pageSize, state.currentPage * pageSize);
|
||
cardsEl.innerHTML = pageSlice.map((item) => {
|
||
const ts = state.options.timestampExtractor ? state.options.timestampExtractor(item) : item.timestamp || item.created_at || null;
|
||
const dateStr = ts ? new Date(ts * 1000).toLocaleString() : "";
|
||
const borderCol = state.options.cardBorderColor || "#9b59b6";
|
||
return `
|
||
<div class="task-card" style="border-left-color: ${borderCol};">
|
||
${dateStr ? `<div style="font-size:0.75em; color:var(--text-secondary); float:right;">${dateStr}</div>` : ""}
|
||
${state.formatter(item)}
|
||
</div>`;
|
||
}).join("");
|
||
}
|
||
}
|
||
const paginationEl = document.getElementById(`${containerId}-pagination`);
|
||
if (paginationEl) {
|
||
if (totalPages <= 1) {
|
||
paginationEl.style.display = "none";
|
||
paginationEl.innerHTML = "";
|
||
} else {
|
||
paginationEl.style.display = "flex";
|
||
let pagHtml = "";
|
||
pagHtml += `<button onclick="onListPageChange('${containerId}', 1)" ${state.currentPage === 1 ? "disabled" : ""} title="First Page">⏮</button>`;
|
||
pagHtml += `<button onclick="onListPageChange('${containerId}', ${state.currentPage - 1})" ${state.currentPage === 1 ? "disabled" : ""} title="Previous Page">◀</button>`;
|
||
const cur = state.currentPage;
|
||
const startP = Math.max(1, cur - 2);
|
||
const endP = Math.min(totalPages, cur + 2);
|
||
if (startP > 1) {
|
||
pagHtml += `<button onclick="onListPageChange('${containerId}', 1)">1</button>`;
|
||
if (startP > 2)
|
||
pagHtml += `<span style="padding:0 4px; color:var(--text-secondary);">…</span>`;
|
||
}
|
||
for (let p = startP;p <= endP; p++) {
|
||
pagHtml += `<button class="${p === cur ? "active" : ""}" onclick="onListPageChange('${containerId}', ${p})">${p}</button>`;
|
||
}
|
||
if (endP < totalPages) {
|
||
if (endP < totalPages - 1)
|
||
pagHtml += `<span style="padding:0 4px; color:var(--text-secondary);">…</span>`;
|
||
pagHtml += `<button onclick="onListPageChange('${containerId}', ${totalPages})">${totalPages}</button>`;
|
||
}
|
||
pagHtml += `<button onclick="onListPageChange('${containerId}', ${state.currentPage + 1})" ${state.currentPage === totalPages ? "disabled" : ""} title="Next Page">▶</button>`;
|
||
pagHtml += `<button onclick="onListPageChange('${containerId}', ${totalPages})" ${state.currentPage === totalPages ? "disabled" : ""} title="Last Page">⏭</button>`;
|
||
paginationEl.innerHTML = pagHtml;
|
||
}
|
||
}
|
||
const clearBtn = document.getElementById(`${containerId}-clear`);
|
||
if (clearBtn) {
|
||
clearBtn.style.display = state.searchQuery ? "block" : "none";
|
||
}
|
||
const sortBtn = document.getElementById(`${containerId}-sort-btn`);
|
||
if (sortBtn) {
|
||
sortBtn.innerHTML = state.sortOrder === "desc" ? "⬇️ Newest First" : "⬆️ Oldest First";
|
||
}
|
||
}
|
||
function renderListStructure(containerId) {
|
||
const state = listControllers.get(containerId);
|
||
if (!state)
|
||
return;
|
||
const container = document.getElementById(containerId);
|
||
if (!container)
|
||
return;
|
||
const itemNoun = state.options.itemNoun || "items";
|
||
let filtersHtml = "";
|
||
if (state.options.filters && state.options.filters.length > 0) {
|
||
for (const f of state.options.filters) {
|
||
const activeVal = state.activeFilters[f.id] || "";
|
||
filtersHtml += `
|
||
<select class="enterprise-select" id="${containerId}-filter-${f.id}" onchange="onListFilterChange('${containerId}', '${f.id}', this.value)" title="Filter by ${f.label}">
|
||
${f.options.map((opt) => `<option value="${escapeHtml(opt.value)}" ${opt.value === activeVal ? "selected" : ""}>${escapeHtml(opt.label)}</option>`).join("")}
|
||
</select>`;
|
||
}
|
||
}
|
||
const curPageSize = state.pageSize;
|
||
const pageSizeOptions = [
|
||
{ label: "10 / page", value: "10" },
|
||
{ label: "25 / page", value: "25" },
|
||
{ label: "50 / page", value: "50" },
|
||
{ label: "100 / page", value: "100" },
|
||
{ label: "All items", value: "-1" }
|
||
];
|
||
const pageSizeHtml = `
|
||
<select class="enterprise-select" id="${containerId}-pagesize" onchange="onListPageSizeChange('${containerId}', this.value)" title="Items per page">
|
||
${pageSizeOptions.map((opt) => `<option value="${opt.value}" ${parseInt(opt.value, 10) === curPageSize ? "selected" : ""}>${opt.label}</option>`).join("")}
|
||
</select>`;
|
||
container.innerHTML = `
|
||
<div class="enterprise-toolbar" id="${containerId}-toolbar">
|
||
<div class="enterprise-search-wrap">
|
||
<span class="enterprise-search-icon">\uD83D\uDD0D</span>
|
||
<input
|
||
type="text"
|
||
class="enterprise-search-input"
|
||
id="${containerId}-search"
|
||
placeholder="Filter ${itemNoun} (/)..."
|
||
value="${escapeHtml(state.searchQuery)}"
|
||
oninput="onListSearchInput('${containerId}', this.value)"
|
||
/>
|
||
<button
|
||
class="enterprise-clear-btn"
|
||
id="${containerId}-clear"
|
||
onclick="onListClearSearch('${containerId}')"
|
||
title="Clear search"
|
||
style="display:${state.searchQuery ? "block" : "none"};"
|
||
>✕</button>
|
||
</div>
|
||
<div class="enterprise-filters-group">
|
||
${filtersHtml}
|
||
<button
|
||
class="enterprise-btn"
|
||
id="${containerId}-sort-btn"
|
||
onclick="onListToggleSort('${containerId}')"
|
||
title="Toggle sort direction"
|
||
>
|
||
${state.sortOrder === "desc" ? "⬇️ Newest First" : "⬆️ Oldest First"}
|
||
</button>
|
||
${pageSizeHtml}
|
||
<button
|
||
class="enterprise-btn"
|
||
onclick="onListResetFilters('${containerId}')"
|
||
title="Reset all filters and sorting"
|
||
>
|
||
\uD83D\uDD04 Reset
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="enterprise-meta-bar" id="${containerId}-meta"></div>
|
||
<div class="enterprise-cards-list" id="${containerId}-cards"></div>
|
||
<div class="enterprise-pagination" id="${containerId}-pagination"></div>
|
||
`;
|
||
renderListCardsAndPagination(containerId);
|
||
}
|
||
function onListSearchInput(containerId, value) {
|
||
const state = listControllers.get(containerId);
|
||
if (!state)
|
||
return;
|
||
if (state.debounceTimer)
|
||
clearTimeout(state.debounceTimer);
|
||
state.searchQuery = value;
|
||
state.debounceTimer = setTimeout(() => {
|
||
state.currentPage = 1;
|
||
applyListFiltering(containerId);
|
||
renderListCardsAndPagination(containerId);
|
||
}, 150);
|
||
}
|
||
function onListClearSearch(containerId) {
|
||
const state = listControllers.get(containerId);
|
||
if (!state)
|
||
return;
|
||
state.searchQuery = "";
|
||
const inputEl = document.getElementById(`${containerId}-search`);
|
||
if (inputEl) {
|
||
inputEl.value = "";
|
||
inputEl.focus();
|
||
}
|
||
state.currentPage = 1;
|
||
applyListFiltering(containerId);
|
||
renderListCardsAndPagination(containerId);
|
||
}
|
||
function onListFilterChange(containerId, filterId, value) {
|
||
const state = listControllers.get(containerId);
|
||
if (!state)
|
||
return;
|
||
state.activeFilters[filterId] = value;
|
||
state.currentPage = 1;
|
||
applyListFiltering(containerId);
|
||
renderListCardsAndPagination(containerId);
|
||
}
|
||
function onListToggleSort(containerId) {
|
||
const state = listControllers.get(containerId);
|
||
if (!state)
|
||
return;
|
||
state.sortOrder = state.sortOrder === "desc" ? "asc" : "desc";
|
||
applyListFiltering(containerId);
|
||
renderListCardsAndPagination(containerId);
|
||
}
|
||
function onListPageSizeChange(containerId, value) {
|
||
const state = listControllers.get(containerId);
|
||
if (!state)
|
||
return;
|
||
state.pageSize = parseInt(value, 10);
|
||
state.currentPage = 1;
|
||
renderListCardsAndPagination(containerId);
|
||
}
|
||
function onListPageChange(containerId, page) {
|
||
const state = listControllers.get(containerId);
|
||
if (!state)
|
||
return;
|
||
state.currentPage = page;
|
||
renderListCardsAndPagination(containerId);
|
||
const container = document.getElementById(containerId);
|
||
if (container) {
|
||
container.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
||
}
|
||
}
|
||
function onListResetFilters(containerId) {
|
||
const state = listControllers.get(containerId);
|
||
if (!state)
|
||
return;
|
||
state.searchQuery = "";
|
||
state.activeFilters = {};
|
||
state.sortOrder = state.options.defaultSort || "desc";
|
||
state.pageSize = state.options.pageSize || 25;
|
||
state.currentPage = 1;
|
||
applyListFiltering(containerId);
|
||
renderListStructure(containerId);
|
||
}
|
||
async function loadGenericList(endpoint, containerId, formatter, options) {
|
||
const container = document.getElementById(containerId);
|
||
if (!container)
|
||
return;
|
||
const resolvedOptions = options || {};
|
||
const existingState = listControllers.get(containerId);
|
||
const state = existingState || {
|
||
endpoint,
|
||
containerId,
|
||
formatter,
|
||
options: resolvedOptions,
|
||
items: [],
|
||
filteredItems: [],
|
||
searchQuery: "",
|
||
activeFilters: {},
|
||
sortOrder: resolvedOptions.defaultSort || "desc",
|
||
currentPage: 1,
|
||
pageSize: resolvedOptions.pageSize || 25,
|
||
debounceTimer: null
|
||
};
|
||
state.options = resolvedOptions;
|
||
state.formatter = formatter;
|
||
listControllers.set(containerId, state);
|
||
try {
|
||
const res = await fetch(endpoint, { cache: "no-store" });
|
||
if (!res.ok) {
|
||
container.innerHTML = `<div style="color:var(--error-color); padding:20px; text-align:center;">⚠️ Failed to load ${escapeHtml(endpoint)} (${res.status} ${escapeHtml(res.statusText)})</div>`;
|
||
return;
|
||
}
|
||
const data = await res.json();
|
||
state.items = Array.isArray(data) ? data : [];
|
||
applyListFiltering(containerId);
|
||
const toolbarExists = document.getElementById(`${containerId}-toolbar`);
|
||
if (!toolbarExists) {
|
||
renderListStructure(containerId);
|
||
} else {
|
||
renderListCardsAndPagination(containerId);
|
||
}
|
||
} catch (err) {
|
||
console.error(`Failed to load ${endpoint}`, err);
|
||
container.innerHTML = `<div style="color:var(--error-color); padding:20px; text-align:center;">⚠️ Failed to load ${escapeHtml(endpoint)} (${escapeHtml(String(err))})</div>`;
|
||
}
|
||
}
|
||
function loadLedger() {
|
||
loadGenericList("/api/ledger?limit=500", "ledger-container", (item) => {
|
||
const vcs = (item.vcs_type || (item.revision && item.revision.startsWith("r") ? "svn" : "git")).toUpperCase();
|
||
const vcsColor = vcs === "SVN" ? "#e67e22" : "#3498db";
|
||
const rev = item.revision || item.git_commit;
|
||
const revLabel = vcs === "SVN" ? "Rev" : "Commit";
|
||
const branch = item.branch || item.git_branch || "None";
|
||
const repo = item.repo_name || item.namespace || "global";
|
||
const changeKind = (item.change_kind || "modified").toLowerCase();
|
||
let kindColor = "var(--accent-color)";
|
||
let kindLabel = "Modified";
|
||
if (changeKind === "added") {
|
||
kindColor = "var(--success-color)";
|
||
kindLabel = "+ Added";
|
||
} else if (changeKind === "deleted") {
|
||
kindColor = "var(--error-color)";
|
||
kindLabel = "- Deleted";
|
||
} else if (changeKind === "renamed") {
|
||
kindColor = "#e67e22";
|
||
kindLabel = "→ Renamed";
|
||
}
|
||
const symbols = Array.isArray(item.symbols) && item.symbols.length > 0 ? item.symbols.map((s) => `<span style="background:var(--card-bg); padding:1px 6px; border-radius:3px; font-size:0.75em; border:1px solid var(--border-color); font-family:monospace;">${escapeHtml(s)}</span>`).join(" ") : "";
|
||
const authorBadge = item.author ? `<span style="background:var(--canvas-bg); padding:2px 8px; border-radius:4px; border:1px solid var(--border-color); font-size:0.8em; color:var(--text-secondary);">\uD83D\uDC64 ${escapeHtml(item.author)}</span>` : "";
|
||
const lineBadge = item.line_range ? `<span style="background:var(--canvas-bg); padding:2px 8px; border-radius:4px; border:1px solid var(--border-color); font-size:0.8em; font-family:monospace;">${escapeHtml(item.line_range)}</span>` : "";
|
||
return `
|
||
<div style="display:flex; justify-content:space-between; align-items:flex-start; gap:10px;">
|
||
<span style="font-weight:bold; font-size:1.05em; color:var(--text-primary); font-family:monospace;">${escapeHtml(item.file_path || "Unknown")}</span>
|
||
<div style="display:flex; gap:6px; align-items:center;">
|
||
<span style="background:${vcsColor}; color:#fff; font-size:0.75em; font-weight:bold; padding:2px 6px; border-radius:4px; text-transform:uppercase;">${vcs}</span>
|
||
<span style="background:var(--canvas-bg); color:${kindColor}; border:1px solid ${kindColor}; font-size:0.75em; font-weight:bold; padding:2px 6px; border-radius:4px;">${kindLabel}</span>
|
||
</div>
|
||
</div>
|
||
<div style="margin-top:6px; font-size:0.95em; color:var(--text-primary);">${escapeHtml(item.description || "")}</div>
|
||
${symbols ? `<div style="margin-top:8px; display:flex; gap:5px; flex-wrap:wrap; align-items:center;"><span style="font-size:0.8em; color:var(--text-secondary);">Symbols:</span> ${symbols}</div>` : ""}
|
||
<div style="margin-top:10px; font-size:0.85em; display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
|
||
<span style="background:var(--canvas-bg); padding:2px 8px; border-radius:4px; font-weight:bold; border:1px solid var(--border-color); color:var(--text-primary);">\uD83D\uDCE6 ${escapeHtml(repo)}</span>
|
||
<span style="background:var(--canvas-bg); padding:2px 8px; border-radius:4px; font-family:monospace; border:1px solid var(--border-color);">${revLabel}: ${rev ? escapeHtml(rev.substring(0, 10)) : "None"}</span>
|
||
<span style="background:var(--canvas-bg); padding:2px 8px; border-radius:4px; border:1px solid var(--border-color);">\uD83C\uDF3F ${escapeHtml(branch)}</span>
|
||
${lineBadge}
|
||
${authorBadge}
|
||
</div>
|
||
`;
|
||
}, {
|
||
itemNoun: "Ledger Entries",
|
||
cardBorderColor: "#3498db",
|
||
defaultSort: "desc",
|
||
timestampExtractor: (item) => item.timestamp,
|
||
searchFields: (item) => [
|
||
item.file_path,
|
||
item.description,
|
||
item.author,
|
||
item.repo_name,
|
||
item.branch,
|
||
item.git_commit,
|
||
item.revision,
|
||
Array.isArray(item.symbols) ? item.symbols.join(" ") : ""
|
||
],
|
||
filters: [
|
||
{
|
||
id: "vcs",
|
||
label: "VCS",
|
||
options: [
|
||
{ label: "All VCS", value: "" },
|
||
{ label: "Git", value: "git" },
|
||
{ label: "SVN", value: "svn" }
|
||
],
|
||
predicate: (item, val) => {
|
||
const vcs = (item.vcs_type || (item.revision && item.revision.startsWith("r") ? "svn" : "git")).toLowerCase();
|
||
return vcs === val.toLowerCase();
|
||
}
|
||
},
|
||
{
|
||
id: "kind",
|
||
label: "Change Kind",
|
||
options: [
|
||
{ label: "All Types", value: "" },
|
||
{ label: "+ Added", value: "added" },
|
||
{ label: "Modified", value: "modified" },
|
||
{ label: "- Deleted", value: "deleted" },
|
||
{ label: "→ Renamed", value: "renamed" }
|
||
],
|
||
predicate: (item, val) => (item.change_kind || "modified").toLowerCase() === val.toLowerCase()
|
||
}
|
||
]
|
||
});
|
||
}
|
||
function loadTerminal() {
|
||
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 `
|
||
<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 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>
|
||
` : ""}
|
||
`;
|
||
}, {
|
||
itemNoun: "Commands",
|
||
cardBorderColor: "#1abc9c",
|
||
defaultSort: "desc",
|
||
timestampExtractor: (item) => item.timestamp,
|
||
searchFields: (item) => [
|
||
item.command,
|
||
item.cwd,
|
||
item.repo_name,
|
||
item.invoker,
|
||
item.shell,
|
||
item.stdout_summary,
|
||
item.error_output
|
||
],
|
||
filters: [
|
||
{
|
||
id: "status",
|
||
label: "Exit Code",
|
||
options: [
|
||
{ label: "All Results", value: "" },
|
||
{ label: "Success (0)", value: "0" },
|
||
{ label: "Failed (!= 0)", value: "err" }
|
||
],
|
||
predicate: (item, val) => {
|
||
if (val === "0")
|
||
return item.exit_code === 0;
|
||
if (val === "err")
|
||
return item.exit_code !== 0;
|
||
return true;
|
||
}
|
||
}
|
||
]
|
||
});
|
||
}
|
||
function loadTechDebt() {
|
||
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 ? '<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 style="display:flex; align-items:center; gap:8px;">
|
||
<div>${statusBadge}</div>
|
||
${!item.is_resolved ? `<button class="complete-btn" style="position:static;" onclick="resolveTechDebt('${escapeHtml(item.id)}')" title="Resolve Tech Debt">✓</button>` : ""}
|
||
</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>
|
||
`;
|
||
}, {
|
||
itemNoun: "Tech Debts",
|
||
cardBorderColor: "#e67e22",
|
||
defaultSort: "desc",
|
||
timestampExtractor: (item) => item.timestamp,
|
||
searchFields: (item) => [
|
||
item.id,
|
||
item.description,
|
||
item.ideal_solution,
|
||
item.workaround,
|
||
item.file_path,
|
||
item.repo_name,
|
||
item.severity
|
||
],
|
||
filters: [
|
||
{
|
||
id: "status",
|
||
label: "Status",
|
||
options: [
|
||
{ label: "All Statuses", value: "" },
|
||
{ label: "Open", value: "open" },
|
||
{ label: "Resolved", value: "resolved" }
|
||
],
|
||
predicate: (item, val) => {
|
||
if (val === "resolved")
|
||
return !!item.is_resolved;
|
||
if (val === "open")
|
||
return !item.is_resolved;
|
||
return true;
|
||
}
|
||
},
|
||
{
|
||
id: "severity",
|
||
label: "Severity",
|
||
options: [
|
||
{ label: "All Severities", value: "" },
|
||
{ label: "Critical", value: "critical" },
|
||
{ label: "High", value: "high" },
|
||
{ label: "Medium", value: "medium" },
|
||
{ label: "Low", value: "low" }
|
||
],
|
||
predicate: (item, val) => String(item.severity || "").toLowerCase() === val.toLowerCase()
|
||
}
|
||
]
|
||
});
|
||
loadGenericList("/api/error_fixes", "errorfixes-container", (item) => `
|
||
<div style="display:flex; justify-content:space-between; align-items:flex-start; gap:8px;">
|
||
<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>
|
||
`, {
|
||
itemNoun: "Error Fixes",
|
||
cardBorderColor: "#2ecc71",
|
||
defaultSort: "desc",
|
||
timestampExtractor: (item) => item.timestamp,
|
||
searchFields: (item) => [
|
||
item.signature,
|
||
item.solution,
|
||
item.toolchain,
|
||
item.error_category,
|
||
item.repo_name,
|
||
item.stack_trace
|
||
],
|
||
filters: [
|
||
{
|
||
id: "category",
|
||
label: "Category",
|
||
options: [
|
||
{ label: "All Categories", value: "" },
|
||
{ label: "Compilation", value: "compilation" },
|
||
{ label: "Runtime", value: "runtime" },
|
||
{ label: "Test", value: "test" },
|
||
{ label: "Configuration", value: "configuration" }
|
||
],
|
||
predicate: (item, val) => String(item.error_category || "").toLowerCase() === val.toLowerCase()
|
||
}
|
||
]
|
||
});
|
||
}
|
||
function loadADRs() {
|
||
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 `
|
||
<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>` : ""}
|
||
`;
|
||
}, {
|
||
itemNoun: "ADRs",
|
||
cardBorderColor: "#3498db",
|
||
defaultSort: "desc",
|
||
sortExtractor: (item) => {
|
||
const match = String(item.id || "").match(/\d+/);
|
||
if (match)
|
||
return parseInt(match[0], 10);
|
||
return item.timestamp || 0;
|
||
},
|
||
searchFields: (item) => [
|
||
item.id,
|
||
item.title,
|
||
item.context,
|
||
item.decision,
|
||
item.consequence,
|
||
item.status,
|
||
item.git_branch,
|
||
item.task_id
|
||
],
|
||
filters: [
|
||
{
|
||
id: "status",
|
||
label: "Status",
|
||
options: [
|
||
{ label: "All Statuses", value: "" },
|
||
{ label: "Accepted", value: "accepted" },
|
||
{ label: "Implemented", value: "implemented" },
|
||
{ label: "Proposed", value: "proposed" },
|
||
{ label: "Draft", value: "draft" },
|
||
{ label: "Superseded", value: "superseded" }
|
||
],
|
||
predicate: (item, val) => String(item.status || "").toLowerCase() === val.toLowerCase()
|
||
}
|
||
]
|
||
});
|
||
}
|
||
function loadMemos() {
|
||
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 `
|
||
<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>
|
||
` : ""}
|
||
`;
|
||
}, {
|
||
itemNoun: "Memos",
|
||
cardBorderColor: "#9b59b6",
|
||
defaultSort: "desc",
|
||
timestampExtractor: (item) => item.timestamp,
|
||
searchFields: (item) => [
|
||
item.author,
|
||
item.content,
|
||
item.summary,
|
||
item.namespace,
|
||
item.git_branch
|
||
]
|
||
});
|
||
loadGenericList("/api/session_summaries", "summaries-container", (item) => `
|
||
<div style="display:flex; justify-content:space-between; align-items:center;">
|
||
<strong>Session ${item.session_id ? escapeHtml(item.session_id) : "Summary"}</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_commit || item.git_branch ? `<div style="font-size:0.8em; color:var(--text-secondary); margin-top:3px; font-family:monospace;">${escapeHtml(item.git_branch || "")} @ ${escapeHtml((item.git_commit || "").substring(0, 8))}</div>` : ""}
|
||
<div style="white-space:pre-wrap; font-size:0.9em; line-height:1.4; margin-top:8px;">${escapeHtml(item.summary || "")}</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>` : ""}
|
||
`, {
|
||
itemNoun: "Summaries",
|
||
cardBorderColor: "#34495e",
|
||
defaultSort: "desc",
|
||
timestampExtractor: (item) => item.timestamp,
|
||
searchFields: (item) => [
|
||
item.session_id,
|
||
item.summary,
|
||
item.namespace,
|
||
item.git_branch
|
||
]
|
||
});
|
||
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 `
|
||
<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>
|
||
` : ""}
|
||
`;
|
||
}, {
|
||
itemNoun: "Milestones",
|
||
cardBorderColor: "#f39c12",
|
||
defaultSort: "desc",
|
||
timestampExtractor: (item) => item.timestamp || item.target_date,
|
||
searchFields: (item) => [
|
||
item.title,
|
||
item.name,
|
||
item.description,
|
||
item.status
|
||
],
|
||
filters: [
|
||
{
|
||
id: "status",
|
||
label: "Status",
|
||
options: [
|
||
{ label: "All Statuses", value: "" },
|
||
{ label: "Pending", value: "pending" },
|
||
{ label: "In Progress", value: "in_progress" },
|
||
{ label: "Completed", value: "completed" }
|
||
],
|
||
predicate: (item, val) => String(item.status || "pending").toLowerCase() === val.toLowerCase()
|
||
}
|
||
]
|
||
});
|
||
}
|
||
function loadSnippets() {
|
||
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 `
|
||
<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>
|
||
`;
|
||
}, {
|
||
itemNoun: "Snippets",
|
||
cardBorderColor: "#27ae60",
|
||
defaultSort: "desc",
|
||
timestampExtractor: (item) => item.timestamp,
|
||
searchFields: (item) => [
|
||
item.name,
|
||
item.description,
|
||
item.code,
|
||
item.language,
|
||
item.origin_file,
|
||
Array.isArray(item.tags) ? item.tags.join(" ") : ""
|
||
],
|
||
filters: [
|
||
{
|
||
id: "language",
|
||
label: "Language",
|
||
options: [
|
||
{ label: "All Languages", value: "" },
|
||
{ label: "Rust", value: "rust" },
|
||
{ label: "TypeScript / JS", value: "typescript" },
|
||
{ label: "Python", value: "python" },
|
||
{ label: "SQL", value: "sql" },
|
||
{ label: "Shell / Bash", value: "bash" },
|
||
{ label: "Markdown / Text", value: "markdown" }
|
||
],
|
||
predicate: (item, val) => {
|
||
const lang = String(item.language || "").toLowerCase();
|
||
if (val === "typescript")
|
||
return lang === "typescript" || lang === "javascript" || lang === "ts" || lang === "js";
|
||
if (val === "bash")
|
||
return lang === "bash" || lang === "sh" || lang === "pwsh" || lang === "powershell";
|
||
return lang === val.toLowerCase();
|
||
}
|
||
}
|
||
]
|
||
});
|
||
}
|
||
function loadEnvironment() {
|
||
loadGenericList("/api/environments", "environments-container", (item) => {
|
||
const isVpn = item.requires_vpn;
|
||
const envType = (item.env_type || "dev").toLowerCase();
|
||
let typeColor = "#3498db";
|
||
if (envType === "prod")
|
||
typeColor = "#e74c3c";
|
||
else if (envType === "staging")
|
||
typeColor = "#f39c12";
|
||
else if (envType === "qa")
|
||
typeColor = "#9b59b6";
|
||
return `
|
||
<div style="display:flex; justify-content:space-between; align-items:center; gap:8px;">
|
||
<div>
|
||
<strong style="font-size:1.05em; color:var(--text-primary);">${escapeHtml(item.name || "Environment")}</strong>
|
||
<span style="background:${typeColor}; color:#fff; font-size:0.75em; font-weight:bold; padding:2px 6px; border-radius:4px; margin-left:6px; text-transform:uppercase;">${escapeHtml(envType)}</span>
|
||
${isVpn ? '<span style="background:#e67e22; color:#fff; font-size:0.75em; font-weight:bold; padding:2px 6px; border-radius:4px; margin-left:6px;">\uD83D\uDD12 VPN Required</span>' : ""}
|
||
</div>
|
||
<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 || "default")}</span>
|
||
</div>
|
||
${item.url ? `<div style="margin-top:6px; font-family:monospace; font-size:0.85em;"><a href="${escapeHtml(item.url)}" target="_blank" rel="noopener noreferrer" style="color:var(--accent-color); text-decoration:none;">\uD83C\uDF10 ${escapeHtml(item.url)}</a></div>` : ""}
|
||
${item.description ? `<div style="margin-top:4px; font-size:0.9em; color:var(--text-primary);">${escapeHtml(item.description)}</div>` : ""}
|
||
<div style="margin-top:8px; font-size:0.8em; display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
|
||
${item.ssh_host ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color); font-family:monospace;">SSH: ${escapeHtml(item.ssh_host)}</span>` : ""}
|
||
${item.healthcheck_endpoint ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color); font-family:monospace;">Health: ${escapeHtml(item.healthcheck_endpoint)}</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>` : ""}
|
||
</div>
|
||
`;
|
||
}, {
|
||
itemNoun: "Environments",
|
||
cardBorderColor: "#3498db",
|
||
defaultSort: "desc",
|
||
timestampExtractor: (item) => item.updated_at,
|
||
searchFields: (item) => [item.name, item.url, item.description, item.env_type, item.ssh_host, item.namespace]
|
||
});
|
||
loadGenericList("/api/env_fingerprints", "fingerprints-container", (item) => {
|
||
const toolVersions = item.tool_versions || {};
|
||
const tools = Object.entries(toolVersions).map(([k, v]) => `<span style="background:var(--canvas-bg); padding:2px 8px; border-radius:4px; font-size:0.8em; font-family:monospace; border:1px solid var(--border-color);"><strong>${escapeHtml(k)}:</strong> ${escapeHtml(String(v))}</span>`).join(" ");
|
||
return `
|
||
<div style="display:flex; justify-content:space-between; align-items:center; gap:8px;">
|
||
<div>
|
||
<strong style="font-size:1.05em; color:var(--text-primary);">\uD83D\uDCBB ${escapeHtml(item.os || "Unknown OS")} (${escapeHtml(item.shell || "shell")})</strong>
|
||
</div>
|
||
<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 || "default")}</span>
|
||
</div>
|
||
${tools ? `<div style="margin-top:10px; display:flex; gap:6px; flex-wrap:wrap; align-items:center;">${tools}</div>` : '<div style="margin-top:6px; font-size:0.85em; color:var(--text-secondary);">No tool versions registered</div>'}
|
||
<div style="margin-top:8px; font-size:0.8em; color:var(--text-secondary);">
|
||
${item.repo_name ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color); margin-right:8px;">\uD83D\uDCE6 ${escapeHtml(item.repo_name)}</span>` : ""}
|
||
</div>
|
||
`;
|
||
}, {
|
||
itemNoun: "Fingerprints",
|
||
cardBorderColor: "#1abc9c",
|
||
defaultSort: "desc",
|
||
timestampExtractor: (item) => item.updated_at,
|
||
searchFields: (item) => [item.namespace, item.os, item.shell, item.repo_name, JSON.stringify(item.tool_versions || {})]
|
||
});
|
||
loadGenericList("/api/env_requirements", "requirements-container", (item) => {
|
||
const isSecret = item.is_secret;
|
||
return `
|
||
<div style="display:flex; justify-content:space-between; align-items:center; gap:8px;">
|
||
<div>
|
||
<span style="font-family:monospace; font-size:1.05em; font-weight:bold; color:var(--text-primary);">${escapeHtml(item.key)}</span>
|
||
${isSecret ? '<span style="background:#e74c3c; color:#fff; font-size:0.75em; font-weight:bold; padding:2px 6px; border-radius:4px; margin-left:6px;">\uD83D\uDD12 Secret</span>' : '<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;">Public</span>'}
|
||
</div>
|
||
<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 || "default")}</span>
|
||
</div>
|
||
${item.description ? `<div style="margin-top:6px; font-size:0.9em; color:var(--text-primary);">${escapeHtml(item.description)}</div>` : ""}
|
||
<div style="margin-top:8px; font-size:0.8em; display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
|
||
${item.default_value ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color); font-family:monospace;">Default: ${escapeHtml(item.default_value)}</span>` : ""}
|
||
${item.validation_regex ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color); font-family:monospace;">Regex: ${escapeHtml(item.validation_regex)}</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>` : ""}
|
||
</div>
|
||
`;
|
||
}, {
|
||
itemNoun: "Requirements",
|
||
cardBorderColor: "#e67e22",
|
||
defaultSort: "desc",
|
||
searchFields: (item) => [item.key, item.description, item.default_value, item.namespace, item.repo_name]
|
||
});
|
||
loadGenericList("/api/gates", "gates-container", (item) => {
|
||
const status = (item.status || "pending").toLowerCase();
|
||
let statusColor = "var(--accent-color)";
|
||
if (status === "authorized")
|
||
statusColor = "var(--success-color)";
|
||
else if (status === "blocked")
|
||
statusColor = "var(--error-color)";
|
||
return `
|
||
<div style="display:flex; justify-content:space-between; align-items:center; gap:8px;">
|
||
<div>
|
||
<strong style="font-size:1.05em; color:var(--text-primary);">${escapeHtml(item.action)} → ${escapeHtml(item.target)}</strong>
|
||
<span style="background:${statusColor}; color:#fff; font-size:0.75em; font-weight:bold; padding:2px 6px; border-radius:4px; margin-left:6px; text-transform:uppercase;">${escapeHtml(status)}</span>
|
||
</div>
|
||
<div style="display:flex; align-items:center; gap:6px;">
|
||
<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>
|
||
${status !== "authorized" ? `<button class="tab-button" style="padding:2px 8px; font-size:0.75em; background:rgba(46,204,113,0.15); color:var(--success-color); border:1px solid var(--success-color); cursor:pointer;" onclick="authorizeGate('${escapeHtml(item.id)}')" title="Authorize Gate">✓ Authorize</button>` : ""}
|
||
${status !== "blocked" ? `<button class="tab-button" style="padding:2px 8px; font-size:0.75em; background:rgba(231,76,60,0.15); color:var(--error-color); border:1px solid var(--error-color); cursor:pointer;" onclick="blockGate('${escapeHtml(item.id)}')" title="Block Gate">\uD83D\uDEAB Block</button>` : ""}
|
||
<button class="tab-button" style="padding:2px 6px; font-size:0.75em; background:var(--canvas-bg); color:var(--text-secondary); border:1px solid var(--border-color); cursor:pointer;" onclick="deleteGate('${escapeHtml(item.id)}')" title="Delete Gate">✕</button>
|
||
</div>
|
||
</div>
|
||
${item.reason ? `<div style="margin-top:6px; font-size:0.9em; color:var(--text-primary);">${escapeHtml(item.reason)}</div>` : ""}
|
||
<div style="margin-top:8px; font-size:0.8em; display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
|
||
${item.gate_type ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">Type: ${escapeHtml(item.gate_type)}</span>` : ""}
|
||
${item.enforcer ? `<span style="background:var(--canvas-bg); padding:2px 6px; border-radius:4px; border:1px solid var(--border-color);">Enforcer: ${escapeHtml(item.enforcer)}</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>` : ""}
|
||
</div>
|
||
`;
|
||
}, {
|
||
itemNoun: "Gates",
|
||
cardBorderColor: "#e74c3c",
|
||
defaultSort: "desc",
|
||
timestampExtractor: (item) => item.timestamp,
|
||
searchFields: (item) => [item.action, item.target, item.status, item.reason, item.enforcer]
|
||
});
|
||
}
|
||
function loadAllExtras() {
|
||
loadTerminal();
|
||
loadTechDebt();
|
||
loadADRs();
|
||
loadMemos();
|
||
loadSnippets();
|
||
loadLedger();
|
||
}
|
||
async function testClipboard() {
|
||
const modal = document.getElementById("clipboard-modal");
|
||
const resultDiv = document.getElementById("clipboard-result");
|
||
if (modal)
|
||
modal.style.display = "flex";
|
||
if (resultDiv)
|
||
resultDiv.innerHTML = '<p id="clipboard-loading" style="color:var(--text-secondary);">Analyzing your clipboard...</p>';
|
||
try {
|
||
const res = await fetch("/api/clipboard/capture", { method: "POST" });
|
||
const data = await res.json();
|
||
if (resultDiv) {
|
||
if (data.success) {
|
||
resultDiv.innerHTML = `<img src="${data.image}" style="max-width:100%; max-height:60vh; border-radius:4px; border:1px solid var(--border-color); box-shadow: 0 5px 15px rgba(0,0,0,0.3);" />`;
|
||
} else {
|
||
resultDiv.innerHTML = `<p style="color:var(--error-color); font-weight:bold;">Failed to read clipboard: ${data.error}</p>`;
|
||
}
|
||
}
|
||
} catch (err) {
|
||
if (resultDiv) {
|
||
resultDiv.innerHTML = `<p style="color:var(--error-color); font-weight:bold;">Error calling endpoint: ${err.message}</p>`;
|
||
}
|
||
}
|
||
}
|
||
async function loadVersion() {
|
||
try {
|
||
const res = await fetch("/api/version");
|
||
const data = await res.json();
|
||
const verEl = document.getElementById("app-version");
|
||
if (verEl) {
|
||
verEl.innerHTML = `<span style="background:var(--card-bg); border:1px solid var(--border-color); color:var(--text-secondary); padding:3px 10px; border-radius:12px; font-size:0.85em; font-weight:600; font-family:monospace;">v${data.version}</span>`;
|
||
}
|
||
} catch (err) {
|
||
console.error("Failed to load version", err);
|
||
}
|
||
}
|
||
function setupSSE() {
|
||
if (wsConnected || activeSSE)
|
||
return;
|
||
try {
|
||
activeSSE = new EventSource("/api/activity/stream");
|
||
activeSSE.onmessage = function(event) {
|
||
if (event.data && !wsConnected) {
|
||
try {
|
||
const parsed = JSON.parse(event.data);
|
||
handleIncomingActivity(parsed);
|
||
} catch (e) {
|
||
handleIncomingActivity(event.data);
|
||
}
|
||
}
|
||
};
|
||
activeSSE.onerror = function() {
|
||
if (activeSSE) {
|
||
activeSSE.close();
|
||
activeSSE = null;
|
||
}
|
||
};
|
||
} catch (e) {
|
||
console.error("SSE initialization error", e);
|
||
}
|
||
}
|
||
async function resolveTechDebt(id) {
|
||
try {
|
||
if (activeWS && activeWS.readyState === WebSocket.OPEN) {
|
||
await sendWsRpc("tech_debts/resolve", { id });
|
||
} else {
|
||
const res = await fetch(`/api/tech_debts/${encodeURIComponent(id)}/resolve`, { method: "POST" });
|
||
if (!res.ok)
|
||
console.error("Failed to resolve tech debt", await res.text());
|
||
}
|
||
loadTechDebt();
|
||
} catch (err) {
|
||
console.error("Error resolving tech debt via WS, trying HTTP fallback", err);
|
||
try {
|
||
const res = await fetch(`/api/tech_debts/${encodeURIComponent(id)}/resolve`, { method: "POST" });
|
||
if (!res.ok)
|
||
console.error("Failed to resolve tech debt", await res.text());
|
||
loadTechDebt();
|
||
} catch (fallbackErr) {
|
||
console.error("Error resolving tech debt", fallbackErr);
|
||
}
|
||
}
|
||
}
|
||
async function authorizeGate(id) {
|
||
try {
|
||
if (activeWS && activeWS.readyState === WebSocket.OPEN) {
|
||
await sendWsRpc("gates/authorize", { id });
|
||
} else {
|
||
const res = await fetch(`/api/gates/${encodeURIComponent(id)}/authorize`, { method: "POST" });
|
||
if (!res.ok)
|
||
console.error("Failed to authorize gate", await res.text());
|
||
}
|
||
loadEnvironment();
|
||
} catch (err) {
|
||
console.error("Error authorizing gate via WS, trying HTTP fallback", err);
|
||
try {
|
||
const res = await fetch(`/api/gates/${encodeURIComponent(id)}/authorize`, { method: "POST" });
|
||
if (!res.ok)
|
||
console.error("Failed to authorize gate", await res.text());
|
||
loadEnvironment();
|
||
} catch (fallbackErr) {
|
||
console.error("Error authorizing gate", fallbackErr);
|
||
}
|
||
}
|
||
}
|
||
async function blockGate(id) {
|
||
try {
|
||
if (activeWS && activeWS.readyState === WebSocket.OPEN) {
|
||
await sendWsRpc("gates/block", { id });
|
||
} else {
|
||
const res = await fetch(`/api/gates/${encodeURIComponent(id)}/block`, { method: "POST" });
|
||
if (!res.ok)
|
||
console.error("Failed to block gate", await res.text());
|
||
}
|
||
loadEnvironment();
|
||
} catch (err) {
|
||
console.error("Error blocking gate via WS, trying HTTP fallback", err);
|
||
try {
|
||
const res = await fetch(`/api/gates/${encodeURIComponent(id)}/block`, { method: "POST" });
|
||
if (!res.ok)
|
||
console.error("Failed to block gate", await res.text());
|
||
loadEnvironment();
|
||
} catch (fallbackErr) {
|
||
console.error("Error blocking gate", fallbackErr);
|
||
}
|
||
}
|
||
}
|
||
async function deleteGate(id) {
|
||
try {
|
||
if (activeWS && activeWS.readyState === WebSocket.OPEN) {
|
||
await sendWsRpc("gates/delete", { id });
|
||
} else {
|
||
const res = await fetch(`/api/gates/${encodeURIComponent(id)}`, { method: "DELETE" });
|
||
if (!res.ok)
|
||
console.error("Failed to delete gate", await res.text());
|
||
}
|
||
loadEnvironment();
|
||
} catch (err) {
|
||
console.error("Error deleting gate via WS, trying HTTP fallback", err);
|
||
try {
|
||
const res = await fetch(`/api/gates/${encodeURIComponent(id)}`, { method: "DELETE" });
|
||
if (!res.ok)
|
||
console.error("Failed to delete gate", await res.text());
|
||
loadEnvironment();
|
||
} catch (fallbackErr) {
|
||
console.error("Error deleting gate", fallbackErr);
|
||
}
|
||
}
|
||
}
|
||
var paletteItems = [];
|
||
var filteredPaletteItems = [];
|
||
var paletteSelectedIndex = 0;
|
||
var paletteDataLoaded = false;
|
||
async function preloadPaletteData() {
|
||
try {
|
||
const [tasksRes, debtsRes, adrsRes, envsRes, gatesRes] = await Promise.all([
|
||
fetch("/api/tasks", { cache: "no-store" }).then((r) => r.json()).catch(() => []),
|
||
fetch("/api/tech_debts", { cache: "no-store" }).then((r) => r.json()).catch(() => []),
|
||
fetch("/api/adrs", { cache: "no-store" }).then((r) => r.json()).catch(() => []),
|
||
fetch("/api/environments", { cache: "no-store" }).then((r) => r.json()).catch(() => []),
|
||
fetch("/api/gates", { cache: "no-store" }).then((r) => r.json()).catch(() => [])
|
||
]);
|
||
const items = [
|
||
{ id: "nav-activity", category: "nav", icon: "⚡", title: "Live Activity Feed", sub: "Inspect real-time agent signals & SSE", badge: "VIEW", tabId: "activity-tab" },
|
||
{ id: "nav-ledger", category: "nav", icon: "\uD83D\uDCD6", title: "Code Ledger", sub: "View logged code changes & commits", badge: "VIEW", tabId: "ledger-tab" },
|
||
{ id: "nav-graph", category: "nav", icon: "\uD83D\uDD78️", title: "Knowledge Graph Explorer", sub: "Explore nodes, relationships & dependencies", badge: "VIEW", tabId: "graph-tab" },
|
||
{ id: "nav-search", category: "nav", icon: "\uD83D\uDD0D", title: "Omni-Search", sub: "Full-text cross-entity search", badge: "VIEW", tabId: "search-tab" },
|
||
{ id: "nav-tasks", category: "nav", icon: "\uD83D\uDCCB", title: "Task Board", sub: "HTN / DAG task hierarchies", badge: "VIEW", tabId: "task-tab" },
|
||
{ id: "nav-techdebt", category: "nav", icon: "⚡", title: "Tech Debt & Error Fixes", sub: "Known tech debt and error fixes", badge: "VIEW", tabId: "techdebt-tab" },
|
||
{ id: "nav-adrs", category: "nav", icon: "\uD83D\uDCDC", title: "Architectural Decision Records", sub: "ADRs and design choices", badge: "VIEW", tabId: "adrs-tab" },
|
||
{ id: "nav-memos", category: "nav", icon: "\uD83D\uDCDD", title: "Handoff Memos", sub: "Agent handoffs & session summaries", badge: "VIEW", tabId: "memos-tab" },
|
||
{ id: "nav-snippets", category: "nav", icon: "\uD83D\uDCCE", title: "Code Snippets", sub: "Indexed snippets & patterns", badge: "VIEW", tabId: "snippets-tab" },
|
||
{ id: "nav-terminal", category: "nav", icon: "\uD83D\uDCBB", title: "Terminal History", sub: "Command execution log & exit codes", badge: "VIEW", tabId: "terminal-tab" },
|
||
{ id: "nav-env", category: "nav", icon: "\uD83C\uDF10", title: "Environment & Quality Gates", sub: "Deploy targets, fingerprints, requirements & gates", badge: "VIEW", tabId: "env-tab" }
|
||
];
|
||
if (Array.isArray(tasksRes)) {
|
||
tasksRes.forEach((t) => {
|
||
items.push({
|
||
id: t.id,
|
||
category: "task",
|
||
icon: t.status === "completed" ? "✓" : "\uD83D\uDCCB",
|
||
title: t.title || t.id,
|
||
sub: t.description || `Task (${t.status || "pending"})`,
|
||
badge: t.status || "TASK",
|
||
tabId: "task-tab"
|
||
});
|
||
});
|
||
}
|
||
if (Array.isArray(debtsRes)) {
|
||
debtsRes.forEach((d) => {
|
||
items.push({
|
||
id: d.id,
|
||
category: "techdebt",
|
||
icon: "⚡",
|
||
title: `${d.id}: ${d.description || ""}`.trim(),
|
||
sub: d.file_path ? `${d.file_path} - Severity: ${d.severity || "medium"}` : `Severity: ${d.severity || "medium"}`,
|
||
badge: d.is_resolved ? "RESOLVED" : d.severity || "DEBT",
|
||
tabId: "techdebt-tab"
|
||
});
|
||
});
|
||
}
|
||
if (Array.isArray(adrsRes)) {
|
||
adrsRes.forEach((a) => {
|
||
items.push({
|
||
id: a.id,
|
||
category: "adr",
|
||
icon: "\uD83D\uDCDC",
|
||
title: `${a.id}: ${a.title || ""}`.trim(),
|
||
sub: a.decision || `Status: ${a.status || "accepted"}`,
|
||
badge: a.status || "ADR",
|
||
tabId: "adrs-tab"
|
||
});
|
||
});
|
||
}
|
||
if (Array.isArray(envsRes)) {
|
||
envsRes.forEach((e) => {
|
||
items.push({
|
||
id: e.name || e.url,
|
||
category: "env",
|
||
icon: "\uD83C\uDF10",
|
||
title: e.name || "Target Environment",
|
||
sub: `${e.url || ""} ${e.description ? "• " + e.description : ""}`.trim(),
|
||
badge: e.env_type || "ENV",
|
||
tabId: "env-tab"
|
||
});
|
||
});
|
||
}
|
||
if (Array.isArray(gatesRes)) {
|
||
gatesRes.forEach((g) => {
|
||
items.push({
|
||
id: g.id,
|
||
category: "gate",
|
||
icon: g.status === "authorized" ? "\uD83D\uDFE2" : g.status === "blocked" ? "\uD83D\uDD34" : "\uD83D\uDFE1",
|
||
title: `${g.action} → ${g.target}`,
|
||
sub: g.reason || `Enforcer: ${g.enforcer || "gate"}`,
|
||
badge: g.status || "GATE",
|
||
tabId: "env-tab"
|
||
});
|
||
});
|
||
}
|
||
paletteItems = items;
|
||
paletteDataLoaded = true;
|
||
} catch (err) {
|
||
console.error("Failed to preload palette data", err);
|
||
}
|
||
}
|
||
function openCommandPalette() {
|
||
const backdrop = document.getElementById("cmd-palette-backdrop");
|
||
const input = document.getElementById("palette-input");
|
||
if (!backdrop || !input)
|
||
return;
|
||
backdrop.style.display = "flex";
|
||
input.value = "";
|
||
paletteSelectedIndex = 0;
|
||
preloadPaletteData().then(() => {
|
||
onPaletteSearch("");
|
||
input.focus();
|
||
});
|
||
}
|
||
function closeCommandPalette() {
|
||
const backdrop = document.getElementById("cmd-palette-backdrop");
|
||
if (backdrop)
|
||
backdrop.style.display = "none";
|
||
}
|
||
function onPaletteBackdropClick(e) {
|
||
if (e.target === document.getElementById("cmd-palette-backdrop")) {
|
||
closeCommandPalette();
|
||
}
|
||
}
|
||
function onPaletteSearch(query) {
|
||
const q = query.toLowerCase().trim();
|
||
if (!q) {
|
||
filteredPaletteItems = paletteItems.slice(0, 30);
|
||
} else {
|
||
filteredPaletteItems = paletteItems.filter((item) => item.title.toLowerCase().includes(q) || item.sub.toLowerCase().includes(q) || item.badge.toLowerCase().includes(q) || item.category.toLowerCase().includes(q)).slice(0, 40);
|
||
}
|
||
paletteSelectedIndex = 0;
|
||
renderPaletteResults();
|
||
}
|
||
function renderPaletteResults() {
|
||
const container = document.getElementById("palette-results");
|
||
const countEl = document.getElementById("palette-count");
|
||
if (!container)
|
||
return;
|
||
if (countEl)
|
||
countEl.innerText = `${filteredPaletteItems.length} results`;
|
||
if (filteredPaletteItems.length === 0) {
|
||
container.innerHTML = '<li class="enterprise-empty-state" style="margin: 10px 18px;">No matching items found.</li>';
|
||
return;
|
||
}
|
||
container.innerHTML = filteredPaletteItems.map((item, idx) => {
|
||
const isSel = idx === paletteSelectedIndex;
|
||
return `
|
||
<li class="palette-item ${isSel ? "selected" : ""}" data-index="${idx}" onclick="selectPaletteItem(${idx})">
|
||
<div class="palette-item-left">
|
||
<span class="palette-item-icon">${escapeHtml(item.icon)}</span>
|
||
<div class="palette-item-text">
|
||
<span class="palette-item-title">${escapeHtml(item.title)}</span>
|
||
<span class="palette-item-sub">${escapeHtml(item.sub)}</span>
|
||
</div>
|
||
</div>
|
||
<span class="palette-item-badge">${escapeHtml(item.badge)}</span>
|
||
</li>
|
||
`;
|
||
}).join("");
|
||
const selectedEl = container.querySelector(`.palette-item[data-index="${paletteSelectedIndex}"]`);
|
||
if (selectedEl) {
|
||
selectedEl.scrollIntoView({ block: "nearest" });
|
||
}
|
||
}
|
||
function selectPaletteItem(idx) {
|
||
const item = filteredPaletteItems[idx];
|
||
if (!item)
|
||
return;
|
||
closeCommandPalette();
|
||
const tabBtn = document.querySelector(`button[onclick*="${item.tabId}"]`);
|
||
if (tabBtn) {
|
||
switchTab(item.tabId, tabBtn);
|
||
}
|
||
}
|
||
function onPaletteKeyDown(e) {
|
||
if (e.key === "ArrowDown") {
|
||
e.preventDefault();
|
||
if (filteredPaletteItems.length > 0) {
|
||
paletteSelectedIndex = (paletteSelectedIndex + 1) % filteredPaletteItems.length;
|
||
renderPaletteResults();
|
||
}
|
||
} else if (e.key === "ArrowUp") {
|
||
e.preventDefault();
|
||
if (filteredPaletteItems.length > 0) {
|
||
paletteSelectedIndex = (paletteSelectedIndex - 1 + filteredPaletteItems.length) % filteredPaletteItems.length;
|
||
renderPaletteResults();
|
||
}
|
||
} else if (e.key === "Enter") {
|
||
e.preventDefault();
|
||
selectPaletteItem(paletteSelectedIndex);
|
||
} else if (e.key === "Escape") {
|
||
e.preventDefault();
|
||
closeCommandPalette();
|
||
}
|
||
}
|
||
document.addEventListener("keydown", function(e) {
|
||
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") {
|
||
e.preventDefault();
|
||
const backdrop = document.getElementById("cmd-palette-backdrop");
|
||
if (backdrop && backdrop.style.display === "flex") {
|
||
closeCommandPalette();
|
||
} else {
|
||
openCommandPalette();
|
||
}
|
||
return;
|
||
}
|
||
if (e.key === "Escape") {
|
||
const backdrop = document.getElementById("cmd-palette-backdrop");
|
||
if (backdrop && backdrop.style.display === "flex") {
|
||
closeCommandPalette();
|
||
return;
|
||
}
|
||
}
|
||
if (e.key === "/" && document.activeElement?.tagName !== "INPUT" && document.activeElement?.tagName !== "TEXTAREA") {
|
||
const backdrop = document.getElementById("cmd-palette-backdrop");
|
||
if (backdrop && backdrop.style.display === "flex")
|
||
return;
|
||
const activeSearch = document.querySelector(".tab-content.active .enterprise-search-input, .tab-content.active #task-filter-input");
|
||
if (activeSearch) {
|
||
e.preventDefault();
|
||
activeSearch.focus();
|
||
activeSearch.select();
|
||
}
|
||
}
|
||
});
|
||
function refreshActiveTab() {
|
||
switch (currentTabId) {
|
||
case "graph-tab":
|
||
loadGraph();
|
||
break;
|
||
case "task-tab":
|
||
loadTasks();
|
||
break;
|
||
case "techdebt-tab":
|
||
loadTechDebt();
|
||
break;
|
||
case "adrs-tab":
|
||
loadADRs();
|
||
break;
|
||
case "memos-tab":
|
||
loadMemos();
|
||
break;
|
||
case "snippets-tab":
|
||
loadSnippets();
|
||
break;
|
||
case "terminal-tab":
|
||
loadTerminal();
|
||
break;
|
||
case "ledger-tab":
|
||
loadLedger();
|
||
break;
|
||
case "env-tab":
|
||
loadEnvironment();
|
||
break;
|
||
case "activity-tab":
|
||
loadActivityHistory();
|
||
break;
|
||
}
|
||
}
|
||
Object.assign(window, {
|
||
switchTab,
|
||
toggleTheme,
|
||
getColorForType,
|
||
zoomGraph,
|
||
resetGraph,
|
||
closeInspector,
|
||
showInspector,
|
||
toggleFilter,
|
||
renderFilters,
|
||
updateGraphData,
|
||
loadGraph,
|
||
completeTask,
|
||
buildTaskTreeHTML,
|
||
loadTasks,
|
||
handleTaskFilter,
|
||
handleTaskStatusFilter,
|
||
highlightText,
|
||
handleSearch,
|
||
escapeHtml,
|
||
parseActivityPayload,
|
||
loadActivityHistory,
|
||
requestDomainRefresh,
|
||
handleIncomingActivity,
|
||
setupWS,
|
||
sendWsRpc,
|
||
loadGenericList,
|
||
onListSearchInput,
|
||
onListClearSearch,
|
||
onListFilterChange,
|
||
onListToggleSort,
|
||
onListPageSizeChange,
|
||
onListPageChange,
|
||
onListResetFilters,
|
||
loadTerminal,
|
||
loadLedger,
|
||
loadTechDebt,
|
||
loadADRs,
|
||
loadMemos,
|
||
loadSnippets,
|
||
loadAllExtras,
|
||
loadEnvironment,
|
||
resolveTechDebt,
|
||
authorizeGate,
|
||
blockGate,
|
||
deleteGate,
|
||
openCommandPalette,
|
||
closeCommandPalette,
|
||
onPaletteBackdropClick,
|
||
onPaletteSearch,
|
||
onPaletteKeyDown,
|
||
selectPaletteItem,
|
||
testClipboard,
|
||
loadVersion,
|
||
setupSSE,
|
||
refreshActiveTab
|
||
});
|
||
loadVersion();
|
||
loadActivityHistory();
|
||
setupWS();
|
||
var observer = new MutationObserver(() => updateGraphData());
|
||
observer.observe(document.documentElement, {
|
||
attributes: true,
|
||
attributeFilter: ["data-theme"]
|
||
});
|