79 lines
3.2 KiB
JavaScript
79 lines
3.2 KiB
JavaScript
|
|
(() => {
|
||
|
|
const CONTROL_ID = 'myelin-fleet-control';
|
||
|
|
|
||
|
|
async function syncControl() {
|
||
|
|
const nodeView = document.querySelector('.node-view[data-node]');
|
||
|
|
if (!nodeView) return;
|
||
|
|
const rawNodeId = nodeView.getAttribute('data-node') || '';
|
||
|
|
if (!/^\d+$/.test(rawNodeId)) return;
|
||
|
|
const logicalNodeId = Number(rawNodeId);
|
||
|
|
|
||
|
|
let model;
|
||
|
|
try {
|
||
|
|
const response = await fetch('/api/control/status', { cache: 'no-store' });
|
||
|
|
if (!response.ok) return;
|
||
|
|
const payload = await response.json();
|
||
|
|
model = payload.Status;
|
||
|
|
} catch (_) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const node = model?.nodes?.find(candidate => candidate.logical_node_id === logicalNodeId);
|
||
|
|
if (!node) return;
|
||
|
|
|
||
|
|
const heading = nodeView.querySelector('section.panel > h2');
|
||
|
|
if (!heading) return;
|
||
|
|
let control = document.getElementById(CONTROL_ID);
|
||
|
|
if (!control) {
|
||
|
|
control = document.createElement('div');
|
||
|
|
control.id = CONTROL_ID;
|
||
|
|
control.style.cssText = 'display:flex;align-items:center;gap:10px;margin:0 0 12px';
|
||
|
|
const button = document.createElement('button');
|
||
|
|
button.type = 'button';
|
||
|
|
button.dataset.action = 'kill';
|
||
|
|
button.style.cssText = 'border:1px solid #ef4444;background:transparent;color:#ef4444;border-radius:3px;padding:6px 10px;font:600 12px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;cursor:pointer';
|
||
|
|
const message = document.createElement('span');
|
||
|
|
message.dataset.message = '';
|
||
|
|
message.style.cssText = 'font:12px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:var(--muted)';
|
||
|
|
control.append(button, message);
|
||
|
|
heading.after(control);
|
||
|
|
}
|
||
|
|
|
||
|
|
const button = control.querySelector('[data-action="kill"]');
|
||
|
|
const message = control.querySelector('[data-message]');
|
||
|
|
const terminal = node.phase === 'stopped' || node.phase === 'orphan';
|
||
|
|
const pending = node.phase === 'kill_requested' || node.phase === 'stopping';
|
||
|
|
button.hidden = terminal;
|
||
|
|
button.disabled = pending;
|
||
|
|
button.textContent = pending ? 'Kill requested' : 'Kill';
|
||
|
|
button.style.opacity = pending ? '.55' : '1';
|
||
|
|
message.textContent = terminal ? `managed node ${logicalNodeId}: ${node.phase}` : '';
|
||
|
|
|
||
|
|
button.onclick = async () => {
|
||
|
|
if (!window.confirm(`Kill managed node ${logicalNodeId}? Vast.ai contracts are destroyed and billing stops.`)) return;
|
||
|
|
button.disabled = true;
|
||
|
|
message.textContent = 'submitting…';
|
||
|
|
const commandId = `fleet-kill-${globalThis.crypto?.randomUUID?.() || Date.now()}`;
|
||
|
|
try {
|
||
|
|
const response = await fetch(`/api/control/nodes/${logicalNodeId}/kill`, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'content-type': 'application/json' },
|
||
|
|
body: JSON.stringify({ command_id: commandId }),
|
||
|
|
});
|
||
|
|
if (!response.ok) {
|
||
|
|
const body = await response.json().catch(() => ({}));
|
||
|
|
throw new Error(body.error || `HTTP ${response.status}`);
|
||
|
|
}
|
||
|
|
message.textContent = 'Kill accepted';
|
||
|
|
} catch (error) {
|
||
|
|
button.disabled = false;
|
||
|
|
message.textContent = error.message;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
const page = document.getElementById('page');
|
||
|
|
if (page) new MutationObserver(syncControl).observe(page, { childList: true, subtree: true });
|
||
|
|
syncControl();
|
||
|
|
setInterval(syncControl, 1000);
|
||
|
|
})();
|