Baseline: distribution workspace before observability redesign
This commit is contained in:
183
.agents/observability/app.js
Normal file
183
.agents/observability/app.js
Normal file
@@ -0,0 +1,183 @@
|
||||
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/<run-id>/ 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("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
Reference in New Issue
Block a user