Files
distribution/.agents/observability/app.js

184 lines
6.1 KiB
JavaScript

const refreshButton = document.getElementById("refresh-button");
const refreshStatus = document.getElementById("refresh-status");
const tailSelect = document.getElementById("tail-select");
const fields = {
activeRun: document.getElementById("metric-active-run"),
phase: document.getElementById("metric-phase"),
branch: document.getElementById("metric-branch"),
blockers: document.getElementById("metric-blockers"),
runCount: document.getElementById("run-count"),
runsList: document.getElementById("runs-list"),
generatedAt: document.getElementById("generated-at"),
nextAction: document.getElementById("next-action"),
workspace: document.getElementById("workspace"),
blockersList: document.getElementById("blockers"),
progressPath: document.getElementById("progress-path"),
progressTail: document.getElementById("progress-tail"),
};
let refreshTimer = null;
refreshButton.addEventListener("click", () => loadStatus());
tailSelect.addEventListener("change", () => loadStatus());
loadStatus();
refreshTimer = setInterval(loadStatus, 5000);
window.addEventListener("beforeunload", () => clearInterval(refreshTimer));
async function loadStatus() {
setRefreshState("Loading", "");
try {
const response = await fetch(`/api/status?tail=${encodeURIComponent(tailSelect.value)}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
render(data);
setRefreshState("Live", "status-ok");
} catch (error) {
setRefreshState("Error", "status-dead");
fields.progressTail.textContent = `Failed to load status: ${error.message}`;
}
}
function render(data) {
const summary = data.summary || {};
const checkpoint = data.checkpoint || {};
const blockers = Array.isArray(checkpoint.blockers) ? checkpoint.blockers : [];
fields.activeRun.textContent = summary.active_run_id || "none";
fields.phase.textContent = summary.current_phase || "unknown";
fields.branch.textContent = summary.active_branch || "none";
fields.blockers.textContent = String(blockers.length);
fields.runCount.textContent = `${data.runs.length} total`;
fields.generatedAt.textContent = formatTime(data.generated_at);
fields.nextAction.textContent = summary.next_action || "No next action recorded.";
fields.workspace.textContent = data.workspace || "-";
renderBlockers(blockers);
renderRuns(data.runs, summary.active_run_id);
renderProgress(data.active_progress);
}
function renderBlockers(blockers) {
fields.blockersList.innerHTML = "";
if (blockers.length === 0) {
fields.blockersList.innerHTML = '<p class="muted">No blockers recorded.</p>';
return;
}
for (const blocker of blockers) {
const item = document.createElement("div");
item.className = "blocker";
item.textContent = blocker;
fields.blockersList.appendChild(item);
}
}
function renderRuns(runs, activeRunId) {
fields.runsList.innerHTML = "";
if (runs.length === 0) {
fields.runsList.innerHTML =
'<div class="empty">No runs yet. Create .agents/runs/&lt;run-id&gt;/ with state.json and agents.json.</div>';
return;
}
for (const run of runs) {
fields.runsList.appendChild(renderRun(run, run.id === activeRunId));
}
}
function renderRun(run, active) {
const state = run.state || {};
const card = document.createElement("article");
card.className = "run-card";
const title = document.createElement("div");
title.className = "run-title";
title.innerHTML = `
<strong>${escapeHtml(run.id)}</strong>
<span class="chip ${active ? "status-ok" : ""}">${active ? "active" : "recorded"}</span>
`;
card.appendChild(title);
const meta = document.createElement("div");
meta.className = "run-meta";
meta.innerHTML = `
<span>Phase: ${escapeHtml(state.current_phase || state.phase || "unknown")}</span>
<span>Branch: ${escapeHtml(state.current_branch || state.branch || "none")}</span>
<span>Expected: ${escapeHtml(state.expected_output_artifact || state.expected_output || "none")}</span>
<span>Retries: ${escapeHtml(String(state.retry_count ?? 0))}</span>
`;
card.appendChild(meta);
const agents = document.createElement("div");
agents.className = "agent-list";
if (!run.agents || run.agents.length === 0) {
agents.innerHTML = '<small class="muted">No agents recorded.</small>';
} else {
for (const agent of run.agents) {
agents.appendChild(renderAgent(agent));
}
}
card.appendChild(agents);
return card;
}
function renderAgent(agent) {
const row = document.createElement("div");
row.className = "agent-row";
const statusClass = agent.lease_expired
? "status-dead"
: agent.status === "completed"
? "status-ok"
: "status-warn";
const id = agent.thread_id || agent.process_id || agent.tool_call_id || "none";
row.innerHTML = `
<div>
<strong>${escapeHtml(agent.role || "unknown-role")}</strong>
<small>heartbeat ${escapeHtml(agent.heartbeat_at || "none")} / id ${escapeHtml(String(id))}</small>
${agent.last_error ? `<small>${escapeHtml(agent.last_error)}</small>` : ""}
</div>
<span class="chip ${statusClass}">${escapeHtml(agent.lease_expired ? "expired" : agent.status || "unknown")}</span>
`;
return row;
}
function renderProgress(progress) {
if (!progress || !progress.path) {
fields.progressPath.textContent = "no progress file";
fields.progressTail.textContent = "No progress file found for the active run.";
return;
}
fields.progressPath.textContent = progress.path;
fields.progressTail.textContent = progress.lines.length > 0
? progress.lines.join("\n")
: "Progress file is empty.";
}
function setRefreshState(label, extraClass) {
refreshStatus.className = `status-pill ${extraClass}`;
refreshStatus.textContent = label;
}
function formatTime(value) {
if (!value) return "-";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return date.toLocaleTimeString();
}
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}