// 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 = `
Type: ${escapeHtml(entity.entity_type)}
Namespace: ${escapeHtml(entity.namespace || "global")}
${entity.repo_name ? `
Repository: ${escapeHtml(entity.repo_name)}
` : ""} ${entity.file_path ? `
File: ${escapeHtml(entity.file_path)}
` : ""} ${entity.git_branch ? `
Branch: ${escapeHtml(entity.git_branch)}
` : ""} ${createdStr ? `
Created: ${escapeHtml(createdStr)}
` : ""} ${updatedStr ? `
Updated: ${escapeHtml(updatedStr)}
` : ""} `; } else { const typeEl = document.getElementById("inspector-type"); if (typeEl) typeEl.innerText = entity.entity_type; const nsEl = document.getElementById("inspector-namespace"); if (nsEl) nsEl.innerText = entity.namespace || "global"; } const obsHtml = (entity.observations || []).map((o) => `
  • ${escapeHtml(o)}
  • `).join(""); const obsEl = document.getElementById("inspector-observations"); if (obsEl) obsEl.innerHTML = obsHtml || "
  • No observations recorded.
  • "; const rels = rawRelations.filter((r) => r.from === nodeId || r.to === nodeId); const relHtml = rels.map((r) => { if (r.from === nodeId) return `
  • → ${escapeHtml(r.relation_type)} → ${escapeHtml(r.to)}
  • `; return `
  • ← ${escapeHtml(r.relation_type)} ← ${escapeHtml(r.from)}
  • `; }).join(""); const relEl = document.getElementById("inspector-relations"); if (relEl) relEl.innerHTML = relHtml || "
  • No direct relations.
  • "; 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 ``; }).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 = '
    Error loading graph: ' + err.message + "
    "; } } } 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 = `
    ${pct}% (${completedChildren.length}/${allChildren.length} child tasks)
    `; if (completedChildren.length < allChildren.length) { isBlocked = true; } } const prio = (t.priority || "").toLowerCase(); let prioBadge = ""; if (prio === "critical" || prio === "urgent") { prioBadge = `CRITICAL`; } else if (prio === "high") { prioBadge = `HIGH`; } else if (prio === "medium" || prio === "med") { prioBadge = `MEDIUM`; } else if (prio === "low") { prioBadge = `LOW`; } html += `
    `; html += `
    `; html += `
    `; if (prioBadge) html += prioBadge; if (t.assigned_agent) { html += `\uD83E\uDD16 ${escapeHtml(t.assigned_agent)}`; } if (isBlocked && !isCompleted && !isCancelled) { html += `BLOCKED`; } if (isCancelled) { html += `CANCELLED`; } html += `
    `; if (!isCompleted && !isCancelled && !isBlocked) { html += ``; } html += `
    `; if (isBlocked && blockers.length > 0 && !isCompleted && !isCancelled) { html += `
    Waiting on: ${escapeHtml(blockers.join(", "))}
    `; } html += `${escapeHtml(t.title)}`; if (t.description) { html += `
    ${escapeHtml(t.description)}
    `; } if (t.verification_command) { html += `
    \uD83E\uDDEA Test: ${escapeHtml(t.verification_command)}
    `; } const criteria = t.acceptance_criteria || []; if (criteria.length > 0) { html += ``; if (unmetCriteria && !isCompleted && !isCancelled) isBlocked = true; } if (t.git_branch || t.repo_name) { html += `
    `; if (t.repo_name) html += `\uD83D\uDCE6 ${escapeHtml(t.repo_name)}`; if (t.git_branch) html += `\uD83C\uDF3F ${escapeHtml(t.git_branch)}`; html += `
    `; } html += progressHtml; if (allChildren.length > 0) { html += `
    `; html += buildTaskTreeHTML(tasks, t.id, 0); html += `
    `; } html += `
    `; }); 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 = '
    No active tasks.
    '; } else { taskContainer.innerHTML = '
    No tasks matching current filter.
    '; } } 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, '$1'); } 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 = '
    No results found.
    '; return; } container.innerHTML = data.results.map((r) => `
    ${highlightText(r.title, q)} ${r.type_name}
    ${r.score.toFixed(2)}
    ${highlightText(r.content.substring(0, 150), q)}${r.content.length > 150 ? "..." : ""}
    ID: ${r.id}
    `).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, "'"); } 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 = `[${timeStr}] ${escapeHtml(catUpper)} ${escapeHtml(summary)}`; if (details) { html += `
    ${escapeHtml(details)}
    `; } 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 = '
    No recent activity recorded yet.
    Live activity will stream here in real-time as MCP tools and system events execute.
    '; 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 ${startIndex}–${endIndex} of ${totalItems} ${itemNoun}`; } if (totalItems !== state.items.length) { summaryText += ` (filtered from ${state.items.length} total)`; } metaEl.innerHTML = `
    ${summaryText}
    Page ${state.currentPage} of ${totalPages}
    `; } const cardsEl = document.getElementById(`${containerId}-cards`); if (cardsEl) { if (totalItems === 0) { const itemNoun = state.options.itemNoun || "items"; if (state.items.length === 0) { cardsEl.innerHTML = `
    No ${itemNoun} recorded.
    `; } else { cardsEl.innerHTML = `
    \uD83D\uDD0D No matching ${itemNoun} found for current filter.
    `; } } 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 `
    ${dateStr ? `
    ${dateStr}
    ` : ""} ${state.formatter(item)}
    `; }).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 += ``; pagHtml += ``; const cur = state.currentPage; const startP = Math.max(1, cur - 2); const endP = Math.min(totalPages, cur + 2); if (startP > 1) { pagHtml += ``; if (startP > 2) pagHtml += `…`; } for (let p = startP;p <= endP; p++) { pagHtml += ``; } if (endP < totalPages) { if (endP < totalPages - 1) pagHtml += `…`; pagHtml += ``; } pagHtml += ``; pagHtml += ``; 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 += ` `; } } 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 = ` `; container.innerHTML = `
    \uD83D\uDD0D
    ${filtersHtml} ${pageSizeHtml}
    `; 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 = `
    ⚠️ Failed to load ${escapeHtml(endpoint)} (${res.status} ${escapeHtml(res.statusText)})
    `; 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 = `
    ⚠️ Failed to load ${escapeHtml(endpoint)} (${escapeHtml(String(err))})
    `; } } 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) => `${escapeHtml(s)}`).join(" ") : ""; const authorBadge = item.author ? `\uD83D\uDC64 ${escapeHtml(item.author)}` : ""; const lineBadge = item.line_range ? `${escapeHtml(item.line_range)}` : ""; return `
    ${escapeHtml(item.file_path || "Unknown")}
    ${vcs} ${kindLabel}
    ${escapeHtml(item.description || "")}
    ${symbols ? `
    Symbols: ${symbols}
    ` : ""}
    \uD83D\uDCE6 ${escapeHtml(repo)} ${revLabel}: ${rev ? escapeHtml(rev.substring(0, 10)) : "None"} \uD83C\uDF3F ${escapeHtml(branch)} ${lineBadge} ${authorBadge}
    `; }, { 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 `
    $ ${escapeHtml(item.command)}
    ${durationStr ? `⏱️ ${escapeHtml(durationStr)}` : ""} Exit ${item.exit_code}
    ${item.shell ? `\uD83D\uDC1A ${escapeHtml(item.shell)}` : ""} OS: ${escapeHtml(item.os || "Unknown")} \uD83D\uDCC2 ${escapeHtml(item.cwd || "Unknown")} ${item.repo_name ? `\uD83D\uDCE6 ${escapeHtml(item.repo_name)}` : ""} ${item.invoker ? `\uD83D\uDC64 ${escapeHtml(item.invoker)}` : ""}
    ${item.status_reason ? `
    Reason: ${escapeHtml(item.status_reason)}
    ` : ""} ${item.stdout_summary ? `
    Output: ${escapeHtml(item.stdout_summary)}
    ` : ""} ${item.error_output ? `
    Error Output
    ${escapeHtml(item.error_output)}
    ` : ""} `; }, { 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 ? '✓ Resolved' : '● Open'; const fileLoc = item.file_path ? `${item.file_path}${item.line_range ? `:${item.line_range}` : ""}` : ""; return `
    ${escapeHtml(item.id)} ${escapeHtml(item.severity || "medium")} ${item.effort_estimate ? `⏳ ${escapeHtml(item.effort_estimate)}` : ""}
    ${statusBadge}
    ${!item.is_resolved ? `` : ""}
    ${fileLoc ? `
    \uD83D\uDCC4 ${escapeHtml(fileLoc)}
    ` : ""}
    Description: ${escapeHtml(item.description || "")}
    ${item.workaround ? `
    Workaround: ${escapeHtml(item.workaround)}
    ` : ""}
    Ideal Solution: ${escapeHtml(item.ideal_solution || "")}
    ${item.repo_name ? `\uD83D\uDCE6 ${escapeHtml(item.repo_name)}` : ""} Commit: ${item.git_commit ? escapeHtml(item.git_commit.substring(0, 8)) : "None"} \uD83C\uDF3F ${escapeHtml(item.git_branch || "None")} \uD83C\uDFF7️ ${escapeHtml(item.namespace || "global")}
    `; }, { 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) => `
    ${escapeHtml(item.signature || "Error")} ${item.error_category ? `${escapeHtml(item.error_category)}` : ""} ${item.toolchain ? `\uD83D\uDD27 ${escapeHtml(item.toolchain)}` : ""}
    ✓ Resolved
    Solution: ${escapeHtml(item.solution || "")}
    ${item.stack_trace ? `
    Stack Trace
    ${escapeHtml(item.stack_trace)}
    ` : ""}
    ${item.repo_name ? `\uD83D\uDCE6 ${escapeHtml(item.repo_name)}` : ""} Commit: ${item.git_commit ? escapeHtml(item.git_commit.substring(0, 8)) : "None"} ${item.git_branch ? `\uD83C\uDF3F ${escapeHtml(item.git_branch)}` : ""}
    `, { 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 `
    ${escapeHtml(item.id)} | ${escapeHtml(item.title)} ${badgeText}
    ${item.git_commit || item.git_branch || item.task_id || item.resolved_at ? `
    ${item.git_branch || item.git_commit ? `\uD83D\uDCE6 ${escapeHtml(item.git_branch || "repo")} @ ${escapeHtml((item.git_commit || "").substring(0, 8))}` : ""} ${item.task_id ? `\uD83C\uDFAF Task: ${escapeHtml(item.task_id)}` : ""} ${resolvedDate ? `✓ Resolved: ${escapeHtml(resolvedDate)}` : ""}
    ` : ""}
    Context: ${escapeHtml(item.context || "")}
    Decision: ${escapeHtml(item.decision || "")}
    Consequence: ${escapeHtml(item.consequence || "")}
    ${item.supersedes ? `
    Supersedes: ${escapeHtml(item.supersedes)}
    ` : ""} `; }, { 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 `
    \uD83D\uDC64 Memo from ${escapeHtml(item.author || "System")} ${escapeHtml(item.namespace || "global")}
    ${item.git_branch || item.vcs_revision || item.session_id ? `
    ${item.git_branch ? `\uD83C\uDF3F ${escapeHtml(item.git_branch)}` : ""} ${item.vcs_revision ? `Rev: ${escapeHtml(item.vcs_revision.substring(0, 8))}` : ""} ${item.session_id ? `Session: ${escapeHtml(item.session_id)}` : ""}
    ` : ""}
    ${escapeHtml(item.content || item.summary || "")}
    ${blockers.length > 0 ? `
    \uD83D\uDEAB Blockers:
    ` : ""} ${actionItems.length > 0 ? `
    \uD83D\uDCCB Action Items:
    ` : ""} `; }, { 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) => `
    Session ${item.session_id ? escapeHtml(item.session_id) : "Summary"} ${escapeHtml(item.namespace || "global")}
    ${item.git_commit || item.git_branch ? `
    ${escapeHtml(item.git_branch || "")} @ ${escapeHtml((item.git_commit || "").substring(0, 8))}
    ` : ""}
    ${escapeHtml(item.summary || "")}
    ${item.tasks_completed && item.tasks_completed.length > 0 ? `
    Tasks Completed:
    ` : ""} ${item.next_steps && item.next_steps.length > 0 ? `
    Next Steps:
    ` : ""} `, { 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 `
    ${escapeHtml(item.title || item.name || "Milestone")} ${escapeHtml(statusLabel)}
    ${targetDate || completedDate ? `
    ${targetDate ? `\uD83C\uDFAF Target: ${escapeHtml(targetDate)}` : ""} ${completedDate ? `✓ Finished: ${escapeHtml(completedDate)}` : ""}
    ` : ""}
    ${escapeHtml(item.description || "")}
    ${deliverables.length > 0 ? `
    Deliverables:
    ` : ""} ${taskIds.length > 0 ? `
    Linked Tasks: ${taskIds.map((tid) => `${escapeHtml(tid)}`).join("")}
    ` : ""} `; }, { 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 `
    ${escapeHtml(item.name || item.description || "Snippet")} ${item.language ? `${escapeHtml(item.language)}` : ""}
    ${item.description && item.description !== item.name ? `
    ${escapeHtml(item.description)}
    ` : ""} ${origin ? `
    \uD83D\uDCC4 ${escapeHtml(origin)}
    ` : ""}
    ${escapeHtml(code)}
    ${item.repo_name ? `\uD83D\uDCE6 ${escapeHtml(item.repo_name)}` : ""} ${tags.map((t) => `#${escapeHtml(t)}`).join(" ")}
    `; }, { 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 `
    ${escapeHtml(item.name || "Environment")} ${escapeHtml(envType)} ${isVpn ? '\uD83D\uDD12 VPN Required' : ""}
    ${escapeHtml(item.namespace || "default")}
    ${item.url ? `
    \uD83C\uDF10 ${escapeHtml(item.url)}
    ` : ""} ${item.description ? `
    ${escapeHtml(item.description)}
    ` : ""}
    ${item.ssh_host ? `SSH: ${escapeHtml(item.ssh_host)}` : ""} ${item.healthcheck_endpoint ? `Health: ${escapeHtml(item.healthcheck_endpoint)}` : ""} ${item.repo_name ? `\uD83D\uDCE6 ${escapeHtml(item.repo_name)}` : ""}
    `; }, { 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]) => `${escapeHtml(k)}: ${escapeHtml(String(v))}`).join(" "); return `
    \uD83D\uDCBB ${escapeHtml(item.os || "Unknown OS")} (${escapeHtml(item.shell || "shell")})
    ${escapeHtml(item.namespace || "default")}
    ${tools ? `
    ${tools}
    ` : '
    No tool versions registered
    '}
    ${item.repo_name ? `\uD83D\uDCE6 ${escapeHtml(item.repo_name)}` : ""}
    `; }, { 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 `
    ${escapeHtml(item.key)} ${isSecret ? '\uD83D\uDD12 Secret' : 'Public'}
    ${escapeHtml(item.namespace || "default")}
    ${item.description ? `
    ${escapeHtml(item.description)}
    ` : ""}
    ${item.default_value ? `Default: ${escapeHtml(item.default_value)}` : ""} ${item.validation_regex ? `Regex: ${escapeHtml(item.validation_regex)}` : ""} ${item.repo_name ? `\uD83D\uDCE6 ${escapeHtml(item.repo_name)}` : ""}
    `; }, { 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 `
    ${escapeHtml(item.action)} → ${escapeHtml(item.target)} ${escapeHtml(status)}
    ${escapeHtml(item.namespace || "global")} ${status !== "authorized" ? `` : ""} ${status !== "blocked" ? `` : ""}
    ${item.reason ? `
    ${escapeHtml(item.reason)}
    ` : ""}
    ${item.gate_type ? `Type: ${escapeHtml(item.gate_type)}` : ""} ${item.enforcer ? `Enforcer: ${escapeHtml(item.enforcer)}` : ""} ${item.repo_name ? `\uD83D\uDCE6 ${escapeHtml(item.repo_name)}` : ""}
    `; }, { 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 = '

    Analyzing your clipboard...

    '; try { const res = await fetch("/api/clipboard/capture", { method: "POST" }); const data = await res.json(); if (resultDiv) { if (data.success) { resultDiv.innerHTML = ``; } else { resultDiv.innerHTML = `

    Failed to read clipboard: ${data.error}

    `; } } } catch (err) { if (resultDiv) { resultDiv.innerHTML = `

    Error calling endpoint: ${err.message}

    `; } } } 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 = `v${data.version}`; } } 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 = '
  • No matching items found.
  • '; return; } container.innerHTML = filteredPaletteItems.map((item, idx) => { const isSel = idx === paletteSelectedIndex; return `
  • ${escapeHtml(item.icon)}
    ${escapeHtml(item.title)} ${escapeHtml(item.sub)}
    ${escapeHtml(item.badge)}
  • `; }).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"] });