feat: event-driven parity, Windows shell detection, dirty build timestamps, and gatekeeper integration
This commit is contained in:
1 parent
64857f9d5e
commit
f4dcf3acbf
33 files changed
+3501
-143
No files matched your search
+507
-13
@@ -42,6 +42,9 @@ function switchTab(tabId, btn) {
|
||||
case "ledger-tab":
|
||||
loadLedger();
|
||||
break;
|
||||
case "env-tab":
|
||||
loadEnvironment();
|
||||
break;
|
||||
}
|
||||
}
|
||||
function toggleTheme() {
|
||||
@@ -291,10 +294,20 @@ async function loadGraph() {
|
||||
}
|
||||
async function completeTask(id) {
|
||||
try {
|
||||
await fetch(`/api/tasks/${id}/complete`, { method: "POST" });
|
||||
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", 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) {
|
||||
@@ -683,6 +696,34 @@ async function loadActivityHistory() {
|
||||
}
|
||||
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 = {
|
||||
@@ -693,7 +734,9 @@ function requestDomainRefresh(domain) {
|
||||
snippets: "snippets-tab",
|
||||
terminal: "terminal-tab",
|
||||
ledger: "ledger-tab",
|
||||
memos: "memos-tab"
|
||||
memos: "memos-tab",
|
||||
env: "env-tab",
|
||||
gates: "env-tab"
|
||||
};
|
||||
const targetTab = tabMap[domain];
|
||||
if (!targetTab || targetTab !== currentTabId) {
|
||||
@@ -729,6 +772,10 @@ function requestDomainRefresh(domain) {
|
||||
case "memos":
|
||||
loadMemos();
|
||||
break;
|
||||
case "env":
|
||||
case "gates":
|
||||
loadEnvironment();
|
||||
break;
|
||||
}
|
||||
}, 250);
|
||||
}
|
||||
@@ -750,6 +797,9 @@ function handleIncomingActivity(payload) {
|
||||
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") {
|
||||
@@ -790,6 +840,7 @@ function setupWS() {
|
||||
}
|
||||
ws.onopen = function() {
|
||||
wsConnected = true;
|
||||
activeWS = ws;
|
||||
if (activeSSE) {
|
||||
activeSSE.close();
|
||||
activeSSE = null;
|
||||
@@ -798,7 +849,18 @@ function setupWS() {
|
||||
ws.onmessage = function(event) {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.type === "activity" || data.method === "notifications/activity" || data.method === "notifications/task/completed" || data.method === "notifications/resources/updated") {
|
||||
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);
|
||||
}
|
||||
@@ -808,12 +870,24 @@ function setupWS() {
|
||||
};
|
||||
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();
|
||||
};
|
||||
}
|
||||
@@ -1328,7 +1402,10 @@ function loadTechDebt() {
|
||||
<span style="background:${sevBg}; color:${sevColor}; border:1px solid ${sevBorder}; font-size:0.75em; font-weight:bold; padding:2px 6px; border-radius:4px; text-transform:uppercase;">${escapeHtml(item.severity || "medium")}</span>
|
||||
${item.effort_estimate ? `<span style="background:var(--canvas-bg); font-size:0.75em; padding:2px 6px; border-radius:4px; border:1px solid var(--border-color); color:var(--text-secondary);">⏳ ${escapeHtml(item.effort_estimate)}</span>` : ""}
|
||||
</div>
|
||||
<div>${statusBadge}</div>
|
||||
<div 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>
|
||||
@@ -1704,6 +1781,121 @@ function loadSnippets() {
|
||||
]
|
||||
});
|
||||
}
|
||||
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();
|
||||
@@ -1772,21 +1964,308 @@ function setupSSE() {
|
||||
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 searchTabBtn = document.querySelector('button[onclick*="search-tab"]');
|
||||
if (searchTabBtn) {
|
||||
switchTab("search-tab", searchTabBtn);
|
||||
}
|
||||
const searchInput = document.getElementById("search-box");
|
||||
if (searchInput) {
|
||||
searchInput.focus();
|
||||
searchInput.select();
|
||||
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();
|
||||
@@ -1821,6 +2300,9 @@ function refreshActiveTab() {
|
||||
case "ledger-tab":
|
||||
loadLedger();
|
||||
break;
|
||||
case "env-tab":
|
||||
loadEnvironment();
|
||||
break;
|
||||
case "activity-tab":
|
||||
loadActivityHistory();
|
||||
break;
|
||||
@@ -1851,6 +2333,7 @@ Object.assign(window, {
|
||||
requestDomainRefresh,
|
||||
handleIncomingActivity,
|
||||
setupWS,
|
||||
sendWsRpc,
|
||||
loadGenericList,
|
||||
onListSearchInput,
|
||||
onListClearSearch,
|
||||
@@ -1866,6 +2349,17 @@ Object.assign(window, {
|
||||
loadMemos,
|
||||
loadSnippets,
|
||||
loadAllExtras,
|
||||
loadEnvironment,
|
||||
resolveTechDebt,
|
||||
authorizeGate,
|
||||
blockGate,
|
||||
deleteGate,
|
||||
openCommandPalette,
|
||||
closeCommandPalette,
|
||||
onPaletteBackdropClick,
|
||||
onPaletteSearch,
|
||||
onPaletteKeyDown,
|
||||
selectPaletteItem,
|
||||
testClipboard,
|
||||
loadVersion,
|
||||
setupSSE,
|
||||
|
||||
Reference in new issue
Block a user