|
|
|
|
@@ -2,6 +2,7 @@ const state = {
|
|
|
|
|
config: null,
|
|
|
|
|
manifests: [],
|
|
|
|
|
sessions: [],
|
|
|
|
|
sessionMetadata: [],
|
|
|
|
|
runs: [],
|
|
|
|
|
selectedSessionId: "",
|
|
|
|
|
selectedManifestPath: "",
|
|
|
|
|
@@ -29,6 +30,11 @@ const dom = {
|
|
|
|
|
runValidationNodes: document.querySelector("#run-validation-nodes"),
|
|
|
|
|
killRun: document.querySelector("#kill-run"),
|
|
|
|
|
runStatus: document.querySelector("#run-status"),
|
|
|
|
|
sessionForm: document.querySelector("#session-form"),
|
|
|
|
|
sessionProjectPath: document.querySelector("#session-project-path"),
|
|
|
|
|
sessionCreate: document.querySelector("#session-create"),
|
|
|
|
|
sessionClose: document.querySelector("#session-close"),
|
|
|
|
|
sessionCloseMerge: document.querySelector("#session-close-merge"),
|
|
|
|
|
nodeInspector: document.querySelector("#node-inspector"),
|
|
|
|
|
eventsLimit: document.querySelector("#events-limit"),
|
|
|
|
|
eventsRefresh: document.querySelector("#events-refresh"),
|
|
|
|
|
@@ -78,6 +84,7 @@ const dom = {
|
|
|
|
|
cfgTopologyDepth: document.querySelector("#cfg-topology-depth"),
|
|
|
|
|
cfgTopologyRetries: document.querySelector("#cfg-topology-retries"),
|
|
|
|
|
cfgRelationshipChildren: document.querySelector("#cfg-relationship-children"),
|
|
|
|
|
cfgMergeConflictAttempts: document.querySelector("#cfg-merge-conflict-attempts"),
|
|
|
|
|
cfgPortBase: document.querySelector("#cfg-port-base"),
|
|
|
|
|
cfgPortBlockSize: document.querySelector("#cfg-port-block-size"),
|
|
|
|
|
cfgPortBlockCount: document.querySelector("#cfg-port-block-count"),
|
|
|
|
|
@@ -137,6 +144,8 @@ const LABEL_HELP_BY_CONTROL = Object.freeze({
|
|
|
|
|
"run-flags": "Optional JSON object passed in as initial run flags.",
|
|
|
|
|
"run-runtime-context": "Optional JSON object of template values injected into persona prompts (for example repo or ticket).",
|
|
|
|
|
"run-validation-nodes": "Optional comma-separated node IDs to simulate validation outcomes for.",
|
|
|
|
|
"session-project-path": "Absolute project path used when creating an explicit managed session.",
|
|
|
|
|
"session-close-merge": "When enabled, close will merge the session base branch back into the project branch.",
|
|
|
|
|
"events-limit": "Set how many recent runtime events are loaded per refresh.",
|
|
|
|
|
"cfg-webhook-url": "Webhook endpoint that receives runtime event notifications.",
|
|
|
|
|
"cfg-webhook-severity": "Minimum severity level that triggers webhook notifications.",
|
|
|
|
|
@@ -152,6 +161,7 @@ const LABEL_HELP_BY_CONTROL = Object.freeze({
|
|
|
|
|
"cfg-topology-depth": "Maximum orchestration graph depth permitted by topology rules.",
|
|
|
|
|
"cfg-topology-retries": "Maximum retry expansions allowed by topology orchestration.",
|
|
|
|
|
"cfg-relationship-children": "Maximum children each persona relationship can spawn.",
|
|
|
|
|
"cfg-merge-conflict-attempts": "Maximum merge-conflict resolution attempts before emitting unresolved conflict events.",
|
|
|
|
|
"cfg-port-base": "Starting port number for provisioning port allocations.",
|
|
|
|
|
"cfg-port-block-size": "Number of ports reserved per allocated block.",
|
|
|
|
|
"cfg-port-block-count": "Number of port blocks available for allocation.",
|
|
|
|
|
@@ -1036,6 +1046,7 @@ async function loadConfig() {
|
|
|
|
|
dom.cfgTopologyDepth.value = String(limits.topologyMaxDepth);
|
|
|
|
|
dom.cfgTopologyRetries.value = String(limits.topologyMaxRetries);
|
|
|
|
|
dom.cfgRelationshipChildren.value = String(limits.relationshipMaxChildren);
|
|
|
|
|
dom.cfgMergeConflictAttempts.value = String(limits.mergeConflictMaxAttempts);
|
|
|
|
|
dom.cfgPortBase.value = String(limits.portBase);
|
|
|
|
|
dom.cfgPortBlockSize.value = String(limits.portBlockSize);
|
|
|
|
|
dom.cfgPortBlockCount.value = String(limits.portBlockCount);
|
|
|
|
|
@@ -1067,11 +1078,28 @@ function statusChipClass(status) {
|
|
|
|
|
return `status-chip status-${status || "unknown"}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getSessionLifecycleStatus(sessionId) {
|
|
|
|
|
const metadata = state.sessionMetadata.find((entry) => entry?.sessionId === sessionId);
|
|
|
|
|
if (!metadata) {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const status = metadata.sessionStatus;
|
|
|
|
|
if (status === "active" || status === "suspended" || status === "closed" || status === "closed_with_conflicts") {
|
|
|
|
|
return status;
|
|
|
|
|
}
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderRunsAndSessionsTable() {
|
|
|
|
|
const rows = [];
|
|
|
|
|
|
|
|
|
|
for (const session of state.sessions) {
|
|
|
|
|
const sessionStatus = session.status || "unknown";
|
|
|
|
|
const lifecycleStatus = getSessionLifecycleStatus(session.sessionId);
|
|
|
|
|
const sessionStatus =
|
|
|
|
|
lifecycleStatus === "closed" || lifecycleStatus === "closed_with_conflicts"
|
|
|
|
|
? lifecycleStatus
|
|
|
|
|
: session.status || lifecycleStatus || "unknown";
|
|
|
|
|
rows.push(`
|
|
|
|
|
<tr data-session-id="${escapeHtml(session.sessionId)}">
|
|
|
|
|
<td>${escapeHtml(session.sessionId)}</td>
|
|
|
|
|
@@ -1099,6 +1127,7 @@ function renderRunsAndSessionsTable() {
|
|
|
|
|
async function loadSessions() {
|
|
|
|
|
const payload = await apiRequest("/api/sessions");
|
|
|
|
|
state.sessions = payload.sessions || [];
|
|
|
|
|
state.sessionMetadata = payload.sessionMetadata || [];
|
|
|
|
|
state.runs = payload.runs || [];
|
|
|
|
|
|
|
|
|
|
if (!state.selectedSessionId && state.sessions.length > 0) {
|
|
|
|
|
@@ -1511,6 +1540,17 @@ async function startRun(event) {
|
|
|
|
|
simulateValidationNodeIds: fromCsv(dom.runValidationNodes.value),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const selectedSessionMetadata = state.sessionMetadata.find(
|
|
|
|
|
(entry) => entry?.sessionId === state.selectedSessionId,
|
|
|
|
|
);
|
|
|
|
|
if (
|
|
|
|
|
selectedSessionMetadata &&
|
|
|
|
|
(selectedSessionMetadata.sessionStatus === "active" ||
|
|
|
|
|
selectedSessionMetadata.sessionStatus === "suspended")
|
|
|
|
|
) {
|
|
|
|
|
payload.sessionId = selectedSessionMetadata.sessionId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (manifestSelection === RUN_MANIFEST_EDITOR_VALUE) {
|
|
|
|
|
const manifestFromEditor = parseJsonSafe(dom.manifestEditor.value, null);
|
|
|
|
|
if (!manifestFromEditor) {
|
|
|
|
|
@@ -1566,6 +1606,64 @@ async function cancelActiveRun() {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function createSessionFromUi() {
|
|
|
|
|
const projectPath = dom.sessionProjectPath.value.trim();
|
|
|
|
|
if (!projectPath) {
|
|
|
|
|
showRunStatus("Project path is required to create a session.", true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const payload = await apiRequest("/api/sessions", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
projectPath,
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const created = payload.session;
|
|
|
|
|
if (created?.sessionId) {
|
|
|
|
|
state.selectedSessionId = created.sessionId;
|
|
|
|
|
showRunStatus(`Session ${created.sessionId} created.`);
|
|
|
|
|
} else {
|
|
|
|
|
showRunStatus("Session created.");
|
|
|
|
|
}
|
|
|
|
|
await loadSessions();
|
|
|
|
|
if (state.selectedSessionId) {
|
|
|
|
|
dom.sessionSelect.value = state.selectedSessionId;
|
|
|
|
|
await refreshGraph();
|
|
|
|
|
await refreshEvents();
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
showRunStatus(error instanceof Error ? error.message : String(error), true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function closeSelectedSessionFromUi() {
|
|
|
|
|
const sessionId = state.selectedSessionId || dom.sessionSelect.value;
|
|
|
|
|
if (!sessionId) {
|
|
|
|
|
showRunStatus("Select a session before closing.", true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const payload = await apiRequest(`/api/sessions/${encodeURIComponent(sessionId)}/close`, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
mergeToProject: dom.sessionCloseMerge.checked,
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const nextStatus = payload?.session?.sessionStatus || "closed";
|
|
|
|
|
showRunStatus(`Session ${sessionId} closed with status ${nextStatus}.`);
|
|
|
|
|
await loadSessions();
|
|
|
|
|
await refreshGraph();
|
|
|
|
|
await refreshEvents();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
showRunStatus(error instanceof Error ? error.message : String(error), true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function saveNotifications(event) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
const payload = {
|
|
|
|
|
@@ -1611,6 +1709,7 @@ async function saveLimits(event) {
|
|
|
|
|
topologyMaxDepth: Number(dom.cfgTopologyDepth.value),
|
|
|
|
|
topologyMaxRetries: Number(dom.cfgTopologyRetries.value),
|
|
|
|
|
relationshipMaxChildren: Number(dom.cfgRelationshipChildren.value),
|
|
|
|
|
mergeConflictMaxAttempts: Number(dom.cfgMergeConflictAttempts.value),
|
|
|
|
|
portBase: Number(dom.cfgPortBase.value),
|
|
|
|
|
portBlockSize: Number(dom.cfgPortBlockSize.value),
|
|
|
|
|
portBlockCount: Number(dom.cfgPortBlockCount.value),
|
|
|
|
|
@@ -1737,6 +1836,12 @@ function bindUiEvents() {
|
|
|
|
|
dom.killRun.addEventListener("click", () => {
|
|
|
|
|
void cancelActiveRun();
|
|
|
|
|
});
|
|
|
|
|
dom.sessionCreate.addEventListener("click", () => {
|
|
|
|
|
void createSessionFromUi();
|
|
|
|
|
});
|
|
|
|
|
dom.sessionClose.addEventListener("click", () => {
|
|
|
|
|
void closeSelectedSessionFromUi();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
dom.notificationsForm.addEventListener("submit", (event) => {
|
|
|
|
|
void saveNotifications(event);
|
|
|
|
|
|