Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 173 additions & 4 deletions dashboard/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,72 @@ var appState = {
events: [], // recent events (newest first), capped at 50
eventCount: 0,
selectedAgent: null, // composite key of currently selected agent
runs: {}, // correlation_id -> run state { stage, status, steps, workflow_name }
};

// ── Run tracking ──
// Maps a step's agent to a pipeline stage so we can position the dot
function runStageFromEvent(evt) {
if (!evt || !evt.data) return null;
var agent = evt.agent || (evt.data && evt.data.agent);
if (agent) return stageFor(agent);
return null;
}

// Process a workflow event and update run tracking state
function processWorkflowEvent(evt) {
var id = evt.correlation_id;
if (!id) return;

if (evt.event === "workflow.started") {
appState.runs[id] = {
id: id,
status: "running",
stage: null,
stepIndex: 0,
totalSteps: (evt.data && evt.data.total_steps) || 0,
workflowName: (evt.data && evt.data.workflow_name) || "",
startedAt: evt.timestamp
};
return;
Comment thread
nutt-adam marked this conversation as resolved.
}

var run = appState.runs[id];
if (!run) return;

if (evt.event === "workflow.step.started") {
run.stage = runStageFromEvent(evt);
run.stepIndex = (evt.data && evt.data.step_index) || run.stepIndex;
run.totalSteps = (evt.data && evt.data.total_steps) || run.totalSteps;
run.status = "running";
} else if (evt.event === "workflow.step.completed") {
// step done — stage will update when next step starts
} else if (evt.event === "workflow.step.failed") {
run.stage = runStageFromEvent(evt);
run.status = "failed";
} else if (evt.event === "workflow.completed") {
run.status = "completed";
// Auto-remove completed runs after 8 seconds so the dot exits
setTimeout(function() {
delete appState.runs[id];
scheduleRender();
}, 8000);
Comment thread
nutt-adam marked this conversation as resolved.
} else if (evt.event === "workflow.failed") {
run.status = "failed";
}
}

// Get active (non-terminal) runs at a given stage
function runsAtStage(stage) {
var result = [];
var ids = Object.keys(appState.runs);
for (var i = 0; i < ids.length; i++) {
var run = appState.runs[ids[i]];
if (run.stage === stage && run.status !== "completed" && run.status !== "failed") result.push(run);
}
return result;
}

// Build a composite key for workspace-scoped agent storage
function agentKey(workspace, agent) {
return (workspace || "_") + ":" + agent;
Expand Down Expand Up @@ -127,12 +191,17 @@ function renderPipeline() {
// Flow connector between stages
if (i > 0) {
var conn = el("div", "flow-connector");
var prevAgents = stageAgents[STAGE_ORDER[i - 1]] || [];
var prevStage = STAGE_ORDER[i - 1];
var prevAgents = stageAgents[prevStage] || [];
var prevActive = false;
for (var p = 0; p < prevAgents.length; p++) {
if (stateClass(prevAgents[p]) === "working") { prevActive = true; break; }
}
if (prevActive) conn.classList.add("active");
// Add flowing animation if a run is transitioning through this connector
if (runsAtStage(prevStage).length > 0 || runsAtStage(stage).length > 0) {
conn.classList.add("flowing");
}
$pipeline.appendChild(conn);
}

Expand All @@ -159,6 +228,20 @@ function renderPipeline() {
})(key));
}

// Render work-item dots for active runs at this stage
var stageRuns = runsAtStage(stage);
if (stageRuns.length > 0) {
var dotsRow = el("div", "run-dots");
for (var r = 0; r < stageRuns.length; r++) {
var dot = el("span", "run-dot");
if (stageRuns[r].status === "failed") dot.classList.add("run-dot-failed");
else dot.classList.add("run-dot-active");
dot.title = stageRuns[r].workflowName + " (step " + stageRuns[r].stepIndex + "/" + stageRuns[r].totalSteps + ")";
dotsRow.appendChild(dot);
}
card.appendChild(dotsRow);
}

$pipeline.appendChild(card);
}

Expand All @@ -174,6 +257,18 @@ function renderPipeline() {
$hudBlock.textContent = blocked + " blocked";
$hudBlock.className = "hud-item" + (blocked > 0 ? " alert" : "");

// Run count in HUD
var runIds = Object.keys(appState.runs);
var runningCount = 0;
for (i = 0; i < runIds.length; i++) {
if (appState.runs[runIds[i]].status === "running") runningCount++;
}
var $hudRuns = document.getElementById("hud-runs");
if ($hudRuns) {
$hudRuns.textContent = runningCount > 0 ? runningCount + " run" + (runningCount > 1 ? "s" : "") : "";
$hudRuns.className = "hud-item" + (runningCount > 0 ? " active-run" : "");
}

// Bottleneck indicator
var bottleneck = findBottleneck();
if (bottleneck) {
Expand Down Expand Up @@ -338,17 +433,23 @@ function connectSSE() {
appState.agents[key] = merged;
}

// Track workflow runs
if (data.event && data.event.indexOf("workflow.") === 0) {
processWorkflowEvent(data);
}

scheduleRender();
} catch (_) { /* ignore parse errors */ }
};

// Event types actually emitted by the server (state/mod.rs transition_events)
// Event types actually emitted by the server
var eventTypes = [
"agent.started", "agent.stopped",
"agent.working", "agent.idle",
"agent.auth_failed", "agent.auth_recovered",
"agent.rate_limited", "agent.provider_down", "agent.provider_recovered"
// Phase 1b will add: workflow.step, workflow.stage_transition, run.completed
"agent.rate_limited", "agent.provider_down", "agent.provider_recovered",
"workflow.started", "workflow.completed", "workflow.failed",
"workflow.step.started", "workflow.step.completed", "workflow.step.failed"
];
for (var i = 0; i < eventTypes.length; i++) {
es.addEventListener(eventTypes[i], handler);
Expand All @@ -358,6 +459,74 @@ function connectSSE() {
es.onmessage = handler;
}

// ── Dispatch panel ──
var $dispatchToggle = document.getElementById("dispatch-toggle");
var $dispatchForm = document.getElementById("dispatch-form");
var $dispatchWf = document.getElementById("dispatch-workflow");
var $dispatchIssue = document.getElementById("dispatch-issue");
var $dispatchGo = document.getElementById("dispatch-go");
var $dispatchStatus = document.getElementById("dispatch-status");

if ($dispatchToggle) {
$dispatchToggle.addEventListener("click", function() {
$dispatchForm.classList.toggle("open");
if ($dispatchForm.classList.contains("open") && $dispatchWf.options.length <= 1) {
loadWorkflows();
}
});
}

function loadWorkflows() {
fetch("/v1/workflows").then(function(res) { return res.json(); }).then(function(json) {
var wfs = (json.data && json.data.workflows) || json.data || [];
$dispatchWf.innerHTML = "";
if (wfs.length === 0) {
$dispatchWf.appendChild(new Option("no workflows", ""));
return;
}
for (var i = 0; i < wfs.length; i++) {
var name = typeof wfs[i] === "string" ? wfs[i] : (wfs[i].name || "");
if (name) $dispatchWf.appendChild(new Option(name, name));
}
}).catch(function() {
$dispatchWf.innerHTML = "";
$dispatchWf.appendChild(new Option("error loading", ""));
});
}

if ($dispatchGo) {
$dispatchGo.addEventListener("click", function() {
var wf = $dispatchWf.value;
if (!wf) return;
$dispatchGo.disabled = true;
$dispatchStatus.textContent = "dispatching…";
$dispatchStatus.className = "dispatch-status";

var body = { workflow: wf };
var issue = ($dispatchIssue.value || "").trim();
if (issue) body.issue = issue;

fetch("/v1/actions/run", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body)
}).then(function(res) { return res.json(); }).then(function(json) {
$dispatchGo.disabled = false;
if (json.status === "ok") {
$dispatchStatus.textContent = "dispatched";
$dispatchStatus.className = "dispatch-status ok";
} else {
$dispatchStatus.textContent = (json.error && json.error.message) || "failed";
$dispatchStatus.className = "dispatch-status err";
}
}).catch(function(e) {
$dispatchGo.disabled = false;
$dispatchStatus.textContent = "error: " + e.message;
$dispatchStatus.className = "dispatch-status err";
});
});
}

// ── Boot ──
fetchHealth().then(function() {
connectSSE();
Expand Down
11 changes: 11 additions & 0 deletions dashboard/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<div class="bar-right">
<span class="hud-item" id="hud-active" title="working agents">0 active</span>
<span class="hud-item" id="hud-blocked" title="blocked or auth-failed agents">0 blocked</span>
<span class="hud-item" id="hud-runs" title="active workflow runs"></span>
<span class="hud-item" id="hud-bottleneck" title="longest-working agent"></span>
<span class="conn-dot" id="conn-status" title="SSE connection"></span>
</div>
Expand All @@ -35,6 +36,16 @@
<ul class="detail-events" id="detail-events"></ul>
</div>

<div id="dispatch-panel">
<button class="dispatch-toggle" id="dispatch-toggle" title="dispatch a workflow run">+ run</button>
<div class="dispatch-form" id="dispatch-form">
<select id="dispatch-workflow"><option value="">loading…</option></select>
<input id="dispatch-issue" type="text" placeholder="issue # (optional)" />
<button id="dispatch-go">dispatch</button>
<span id="dispatch-status" class="dispatch-status"></span>
Comment thread
nutt-adam marked this conversation as resolved.
</div>
</div>

<footer id="timeline">
<h3>recent events</h3>
<ul id="event-list"></ul>
Expand Down
Loading
Loading