Files
ttulttul-ComfyUI-Modal/web/vast_manager.js
T

155 lines
5.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
const VAST_STATUS_ROUTE = "/remote/vast/status";
const VAST_VERIFY_ROUTE = "/remote/vast/verify";
const VAST_REAP_ROUTE = "/remote/vast/reap";
const VAST_DESTROY_ROUTE = "/remote/vast/destroy";
/** Return JSON or raise the server's credential-safe diagnostic. */
async function requestJson(route, options = {}) {
const response = await api.fetchApi(route, options);
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(payload.error || `${response.status} ${response.statusText}`);
}
return payload;
}
/** Format one epoch deadline for local inspection. */
function deadlineLabel(epoch) {
const value = Number(epoch);
return Number.isFinite(value) ? new Date(value * 1000).toLocaleString() : "unknown";
}
/** Build one managed lease row with an exact destructive action. */
function leaseRow(lease, refresh, setStatus) {
const row = document.createElement("tr");
const values = [
lease.instance_id,
lease.profile_name,
`${lease.gpu_count}× ${lease.gpu_name}`,
`$${Number(lease.hourly_cost_usd || 0).toFixed(4)}/h`,
lease.actual_status,
lease.active_invocations,
deadlineLabel(lease.idle_deadline_epoch),
];
for (const value of values) {
const cell = document.createElement("td");
cell.textContent = String(value);
row.appendChild(cell);
}
const action = document.createElement("td");
const destroy = document.createElement("button");
destroy.type = "button";
destroy.textContent = "Destroy";
destroy.disabled = Number(lease.active_invocations || 0) > 0;
destroy.addEventListener("click", async () => {
if (typeof window.confirm === "function" && !window.confirm(
`Permanently destroy managed Vast.ai instance ${lease.instance_id}?`,
)) return;
try {
setStatus(`Destroying instance ${lease.instance_id}…`);
await requestJson(VAST_DESTROY_ROUTE, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ instance_id: lease.instance_id }),
});
await refresh();
} catch (error) {
setStatus(`Destroy failed: ${String(error?.message ?? error)}`);
}
});
action.appendChild(destroy);
row.appendChild(action);
return row;
}
/** Open credential verification and owned-lease lifecycle controls. */
export async function openVastManager() {
const overlay = document.createElement("div");
overlay.className = "comfy-remote-overlay";
const dialog = document.createElement("div");
dialog.className = "comfy-remote-dialog";
const title = document.createElement("h2");
title.textContent = "Vast.ai managed leases";
const intro = document.createElement("p");
intro.className = "intro";
intro.textContent = "Capacity, price ceilings, and idle retention live in disconnected Vast.ai Lease Configuration workflow nodes. Set VAST_API_KEY and COMFY_MODAL_VAST_IMAGE in the ComfyUI process environment; secrets are never saved in workflows.";
const table = document.createElement("table");
table.style.width = "100%";
const header = document.createElement("thead");
header.innerHTML = "<tr><th>ID</th><th>Profile</th><th>GPU</th><th>Price</th><th>Status</th><th>Active</th><th>Idle deadline</th><th></th></tr>";
const body = document.createElement("tbody");
table.append(header, body);
const status = document.createElement("div");
status.className = "status";
const setStatus = (message) => { status.textContent = message; };
const refresh = async () => {
const payload = await requestJson(VAST_STATUS_ROUTE);
body.replaceChildren();
for (const lease of payload.leases ?? []) {
body.appendChild(leaseRow(lease, refresh, setStatus));
}
setStatus(`API key: ${payload.configured ? "configured" : "missing"}; worker image: ${payload.image_configured ? "configured" : "missing"}; managed leases: ${(payload.leases ?? []).length}.`);
};
const actions = document.createElement("div");
actions.className = "footer-group";
const button = (label, callback) => {
const element = document.createElement("button");
element.type = "button";
element.textContent = label;
element.addEventListener("click", callback);
actions.appendChild(element);
};
button("Verify API key", async () => {
try {
const result = await requestJson(VAST_VERIFY_ROUTE, { method: "POST" });
setStatus(`Vast.ai credential verified for account ${result.account?.id ?? "unknown"}.`);
} catch (error) {
setStatus(`Verification failed: ${String(error?.message ?? error)}`);
}
});
button("Refresh", () => void refresh());
button("Destroy expired now", async () => {
try {
const result = await requestJson(VAST_REAP_ROUTE, { method: "POST" });
setStatus(`Destroyed ${(result.destroyed_instance_ids ?? []).length} expired lease(s).`);
await refresh();
} catch (error) {
setStatus(`Cleanup failed: ${String(error?.message ?? error)}`);
}
});
button("Close", () => overlay.remove());
dialog.append(title, intro, table, status, actions);
overlay.appendChild(dialog);
overlay.addEventListener("click", (event) => {
if (event.target === overlay) overlay.remove();
});
document.body.appendChild(overlay);
try {
await refresh();
} catch (error) {
setStatus(`Status failed: ${String(error?.message ?? error)}`);
}
}
app.registerExtension({
name: "Comfy.RemoteExecution.Vast",
async setup() {
app.ui.settings.addSetting({
id: "Comfy.RemoteExecution.ManageVast",
name: "Remote Execution: Vast.ai leases",
type: () => {
const button = document.createElement("button");
button.type = "button";
button.textContent = "Manage Vast.ai leases";
button.addEventListener("click", () => void openVastManager());
return button;
},
defaultValue: null,
tooltip: "Verify Vast.ai setup and inspect or destroy node-pack-managed leases.",
});
},
});