fix: simplify serverless cards and keep status dots circular

This commit is contained in:
Robert Wojciechowski
2026-10-11 06:01:46 +00:00
parent a281d03c6d
commit 6eb94a52fe
6 changed files with 113 additions and 64 deletions
+3 -3
View File
@@ -131,7 +131,7 @@ export const PULSE_ANIMATION_CSS = `
`;
export const UI_STYLES = {
statusDot: "display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 10px;",
statusDot: "display: inline-block; flex: 0 0 10px; width: 10px; height: 10px; border-radius: 50%; margin-right: 10px;",
controlsDiv: "padding: 0 12px 12px 12px; display: flex; gap: 6px;",
formGroup: "display: flex; flex-direction: column; gap: 5px;",
formLabel: "font-size: 12px; color: var(--dist-label-text, #ccc); font-weight: 500;",
@@ -147,12 +147,12 @@ export const UI_STYLES = {
// Column styles
columnBase: "display: flex; align-items: center; justify-content: center;",
checkboxColumn: "flex: 0 0 44px; display: flex; align-items: center; justify-content: center; cursor: default;",
contentColumn: "flex: 1; display: flex; flex-direction: column; transition: background-color 0.2s ease;",
contentColumn: "flex: 1; min-width: 0; display: flex; flex-direction: column; transition: background-color 0.2s ease;",
iconColumn: "width: 44px; flex-shrink: 0; font-size: 20px; color: var(--dist-placeholder-add-color, #666);",
// Row and content styles
infoRow: "display: flex; align-items: center; padding: 12px; cursor: pointer; min-height: 64px;",
workerContent: "display: flex; align-items: center; gap: 10px; flex: 1;",
workerContent: "display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0;",
// Form and controls styles
buttonGroup: "display: flex; gap: 4px; margin-top: 10px;",
+11
View File
@@ -235,6 +235,17 @@
border-radius: 4px;
}
.dist-worker-info {
min-width: 0;
overflow-wrap: anywhere;
}
.dist-serverless-status {
display: block;
margin-top: 4px;
line-height: 1.4;
}
.dist-worker-info__title {
color: var(--dist-card-title) !important;
font-size: 1.03em;
+51 -6
View File
@@ -5,7 +5,7 @@ vi.mock("../../scripts/api.js", () => ({ api: { clientId: "test-client" } }), {
import { createApiClient } from "../apiClient.js";
import { createWorkerSettingsForm } from "../ui/settingsForm.js";
import { DistributedUI } from "../ui.js";
import { checkWorkerStatus, getWorkerUrl, launchWorker, stopWorker, viewWorkerLog } from "../workerLifecycle.js";
import { checkWorkerStatus, formatServerlessStatus, getWorkerUrl, launchWorker, stopWorker, viewWorkerLog } from "../workerLifecycle.js";
import { handleClearMemory, handleInterruptWorkers } from "../workerUtils.js";
import { isRemoteWorker, saveWorkerSettings, testServerlessWorker } from "../workerSettings.js";
import { performPreflightCheck } from "../executionUtils.js";
@@ -16,7 +16,9 @@ function createDocumentMock() {
const element = {
tagName,
children: [],
dataset: {},
style: {},
setAttribute(name, value) { this[name] = value; },
classList: {
values: new Set(),
add(...names) { names.forEach((name) => this.values.add(name)); },
@@ -259,6 +261,50 @@ describe("Runpod serverless worker panel", () => {
expect(worker).not.toHaveProperty("api_key");
});
it.each([
[undefined, "Checking status…"],
[{ ok: true, queueRemaining: 0, jobs: [] }, "Ready"],
[{ ok: true, queueRemaining: 1, jobs: [{ status: "IN_QUEUE" }] }, "Queued"],
[{ ok: true, jobs: [{ status: "IN_PROGRESS" }] }, "Running"],
[{ ok: true, jobs: [{ status: "RUNNING" }, { status: "IN_QUEUE" }] }, "Running"],
[{ ok: true, jobs: [{ status: "RUNNING" }, { status: "IN_PROGRESS" }] }, "Running · 2 jobs"],
[{ ok: true, jobs: [{ status: "DOWNLOADING" }] }, "Downloading result"],
[{ ok: true, jobs: [{ status: "CANCEL_REQUESTED" }] }, "Cancelling"],
[{ ok: true, queueRemaining: 1 }, "Active · 1 job"],
[{ ok: true, queueRemaining: 3 }, "Active · 3 jobs"],
[{ ok: true, jobs: [{ status: "COMPLETED" }] }, "Ready"],
[{ ok: true, jobs: [{ status: "FAILED" }] }, "Last job failed"],
[{ ok: false, message: "Check the saved endpoint ID and API key" }, "Unavailable"],
])("formats provider state as one plain-language summary: %j", (status, expected) => {
expect(formatServerlessStatus(status)).toBe(expected);
});
it("keeps the serverless card compact while retaining its expanded settings", () => {
const ui = new DistributedUI();
const worker = {
id: "compact", name: "Used Car Inventory", type: "serverless", enabled: true,
endpoint_id: "ep_private_id", jobs_per_run: 1, concurrency: 1,
};
const extension = {
config: { workers: [worker] },
state: {
isWorkerExpanded: () => false,
getWorker: () => ({ serverlessStatus: { ok: true, queueRemaining: 1, jobs: [{ status: "IN_QUEUE" }] } }),
},
};
const card = ui.renderEntityCard("worker", worker, extension);
const collect = (element) => [element, ...(element.children || []).flatMap(collect)];
const info = collect(card).find((element) => element.classList.values.has("dist-worker-info"));
expect(info.innerHTML).toContain("Runpod Serverless");
expect(info.innerHTML).not.toMatch(/ep_private_id|jobs\/run|slots/);
expect(elements.get("serverless-status-compact").textContent).toBe("Queued");
expect(elements.get("endpoint-id-compact").value).toBe("ep_private_id");
expect(Number(elements.get("jobs-per-run-compact").value)).toBe(1);
expect(Number(elements.get("concurrency-compact").value)).toBe(1);
expect(elements.has("controls-compact")).toBe(false);
expect(elements.get("status-compact").style.cssText).toContain("flex: 0 0 10px");
});
it("uses provider status instead of a direct ComfyUI URL probe", async () => {
const statusElement = { textContent: "" };
elements.set("serverless-status-w4", statusElement);
@@ -288,8 +334,8 @@ describe("Runpod serverless worker panel", () => {
expect(extension.state.setWorkerStatus).toHaveBeenCalledWith("w4", {
online: true, processing: true, queueCount: 0,
});
expect(statusElement.textContent).toContain("Ready to scale");
expect(statusElement.textContent).toContain("RUNNING");
expect(statusElement.textContent).toBe("Running");
expect(statusElement.title).toBe("Ready to scale");
});
it("considers a healthy zero-worker Runpod pool ready and never builds a worker URL", async () => {
@@ -312,8 +358,7 @@ describe("Runpod serverless worker panel", () => {
const ui = new DistributedUI();
const worker = { id: "w-controls", name: "Runpod", type: "serverless", enabled: true };
const controls = ui.createControlsSection({ dynamic: true }, worker, {}, true);
const collect = (element) => [element, ...(element.children || []).flatMap(collect)];
const elementsInControls = collect(controls);
const extension = {
config: { workers: [worker] },
api: {
@@ -321,7 +366,7 @@ describe("Runpod serverless worker panel", () => {
},
};
expect(elementsInControls.filter((element) => element.tagName === "button")).toHaveLength(0);
expect(controls).toBeNull();
await launchWorker(extension, worker.id);
await stopWorker(extension, worker.id);
await viewWorkerLog(extension, worker.id, true);
+14 -21
View File
@@ -69,10 +69,7 @@ const cardConfigs = {
const isServerless = data.type === 'serverless';
if (isServerless) {
const endpoint = data.endpoint_id || "Endpoint not configured";
const jobsPerRun = data.jobs_per_run ?? 1;
const concurrency = data.concurrency ?? 1;
return `<span class="dist-worker-info__title">${escapeWorkerInfo(data.name)}</span><br><span class="dist-worker-info__meta">Runpod Serverless • ${escapeWorkerInfo(endpoint)} • ${escapeWorkerInfo(jobsPerRun)} jobs/run • ${escapeWorkerInfo(concurrency)} slots</span>`;
return `<span class="dist-worker-info__title">${escapeWorkerInfo(data.name)}</span><br><span class="dist-worker-info__meta">Runpod Serverless</span>`;
}
if (isCloud) {
@@ -576,6 +573,8 @@ export class DistributedUI {
createControlsSection(config, data, extension, isRemote) {
if (!config) return null;
// Serverless has no direct-worker actions; its provider is already in the header.
if (config.dynamic && data?.type === "serverless") return null;
const controlsDiv = document.createElement("div");
controlsDiv.id = `controls-${data?.id || 'master'}`;
@@ -607,24 +606,18 @@ export class DistributedUI {
controlsWrapper.appendChild(badge);
} else if (config.dynamic && data) {
if (isRemote) {
if (data.type === "serverless") {
const workerTypeBadge = this.createInfoBox("Runpod Serverless worker");
workerTypeBadge.title = "Provider-managed workers scale on demand";
controlsWrapper.appendChild(workerTypeBadge);
} else {
const isCloud = data.type === 'cloud';
const workerTypeText = isCloud ? "Cloud worker" : "Remote worker";
const workerTypeBadge = this.createInfoBox(workerTypeText);
workerTypeBadge.title = "Worker is externally hosted";
controlsWrapper.appendChild(workerTypeBadge);
const isCloud = data.type === 'cloud';
const workerTypeText = isCloud ? "Cloud worker" : "Remote worker";
const workerTypeBadge = this.createInfoBox(workerTypeText);
workerTypeBadge.title = "Worker is externally hosted";
controlsWrapper.appendChild(workerTypeBadge);
const logBtn = this.createButton('View Log', () => viewWorkerLog(extension, data.id, true));
logBtn.id = `log-${data.id}`;
logBtn.style.cssText = BUTTON_STYLES.base + BUTTON_STYLES.workerControl;
logBtn.classList.add("btn--log");
logBtn.title = "View remote worker log";
controlsWrapper.appendChild(logBtn);
}
const logBtn = this.createButton('View Log', () => viewWorkerLog(extension, data.id, true));
logBtn.id = `log-${data.id}`;
logBtn.style.cssText = BUTTON_STYLES.base + BUTTON_STYLES.workerControl;
logBtn.classList.add("btn--log");
logBtn.title = "View remote worker log";
controlsWrapper.appendChild(logBtn);
} else {
const controls = this.createWorkerControls(data.id, {
launch: () => launchWorker(extension, data.id),
+4 -17
View File
@@ -1,20 +1,6 @@
import { updateWorkerControls, toggleWorkerExpanded } from "../workerLifecycle.js";
import { updateWorkerControls, toggleWorkerExpanded, formatServerlessStatus } from "../workerLifecycle.js";
import { isRemoteWorker } from "../workerSettings.js";
function formatServerlessStatus(status) {
if (!status) return "Runpod status pending";
const message = status.message || (status.ok ? "Ready to scale" : "Runpod status unavailable");
const queue = Number(status.queueRemaining);
const parts = [message];
if (Number.isFinite(queue)) parts.push(`${Math.max(0, queue)} queued`);
const jobs = (Array.isArray(status.jobs) ? status.jobs : [])
.slice(0, 2)
.map((job) => String(job?.status || "").trim())
.filter(Boolean);
if (jobs.length) parts.push(`Jobs: ${jobs.join(", ")}`);
return parts.join(" • ");
}
export function renderEntityCard(ui, cardConfigs, entityType, data, extension) {
const config = cardConfigs[entityType] || {};
const isPlaceholder = entityType === 'blueprint' || entityType === 'add';
@@ -92,9 +78,10 @@ export function renderEntityCard(ui, cardConfigs, entityType, data, extension) {
const statusMessage = document.createElement("small");
statusMessage.id = `serverless-status-${data.id}`;
statusMessage.className = "dist-worker-info__meta dist-serverless-status";
statusMessage.style.display = "block";
statusMessage.setAttribute("aria-live", "polite");
statusMessage.textContent = formatServerlessStatus(extension.state.getWorker(data.id)?.serverlessStatus);
const status = extension.state.getWorker(data.id)?.serverlessStatus;
statusMessage.textContent = data.enabled ? formatServerlessStatus(status) : "Disabled";
statusMessage.title = status?.message || "";
infoSpan.appendChild(statusMessage);
}
+30 -17
View File
@@ -41,26 +41,39 @@ function setButtonVisibility(button, visible) {
button.style.display = visible ? "" : "none";
}
export function formatServerlessStatus(status) {
if (!status) return "Checking status…";
if (!status.ok) return "Unavailable";
const jobs = (Array.isArray(status.jobs) ? status.jobs : [])
.map((job) => String(job?.status || "").trim().toUpperCase());
// Prefer current work over old ledger entries, and avoid raw provider enums.
const phases = [
["Downloading result", ["DOWNLOADING"]],
["Cancelling", ["CANCEL_REQUESTED"]],
["Running", ["RUNNING", "PROCESSING", "IN_PROGRESS"]],
["Starting", ["STARTING"]],
["Submitting", ["SUBMITTING"]],
["Queued", ["WAITING", "IN_QUEUE", "QUEUED", "PENDING"]],
];
for (const [label, states] of phases) {
const count = jobs.filter((state) => states.includes(state)).length;
if (count) return count > 1 ? `${label} · ${count} jobs` : label;
}
// Health counts queued AND running jobs, not just the waiting queue.
const remaining = Number(status.queueRemaining);
if (Number.isFinite(remaining) && remaining > 0) {
return `Active · ${remaining} ${remaining === 1 ? "job" : "jobs"}`;
}
if (jobs[0] === "FAILED") return "Last job failed";
if (jobs[0] === "TIMED_OUT") return "Last job timed out";
return "Ready";
}
function updateServerlessStatusMessage(workerId, status) {
const messageElement = document.getElementById(`serverless-status-${workerId}`);
if (!messageElement) return;
const message = typeof status?.message === "string" && status.message
? status.message
: status?.ok ? "Ready to scale" : "Runpod status unavailable";
const queueRemaining = Number(status?.queueRemaining);
const parts = [message];
if (Number.isFinite(queueRemaining)) {
parts.push(`${Math.max(0, queueRemaining)} queued`);
}
const jobStatuses = (Array.isArray(status?.jobs) ? status.jobs : [])
.slice(0, 2)
.map((job) => String(job?.status || "").trim())
.filter(Boolean);
if (jobStatuses.length) {
parts.push(`Jobs: ${jobStatuses.join(", ")}`);
}
messageElement.textContent = parts.join(" • ");
messageElement.textContent = formatServerlessStatus(status);
messageElement.title = status?.message || "";
}
export async function checkAllWorkerStatuses(extension) {