Files
stack/packages/webui/src/public/processing.js
T

102 lines
5.5 KiB
JavaScript

/* Durable instance controls, outside all route-specific views. */
(() => {
const byId = id => document.getElementById(id);
const host = byId('processing-controls');
if (!host) return;
const status = byId('processing-state'), halt = byId('processing-halt'), resume = byId('processing-resume');
const unavailable = byId('processing-unavailable'), receipt = byId('processing-receipt'), banner = byId('processing-banner');
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
const out = () => document.body.classList.contains('signed-out');
const signOut = signIn => {
if (!out()) document.dispatchEvent(signIn
? new CustomEvent('mosaic-signed-out', { detail: { signIn: true } })
: new Event('mosaic-signed-out'));
};
let snapshot = null, canWrite = false, busy = false, sequence = 0, timer, drawn;
async function responseBody(response) {
if (response.type === 'opaqueredirect') { signOut(false); return null; }
const body = await response.json();
if (response.status === 401 && body?.error === 'sign-in required') { signOut(true); return null; }
return body;
}
function draw() {
const signature = JSON.stringify([snapshot?.version, snapshot?.generation, snapshot?.halted, snapshot?.stopping, canWrite, busy, out()]);
if (signature === drawn) {
document.dispatchEvent(new CustomEvent('mosaic-processing-state', { detail: { halted: snapshot?.halted ?? null } }));
return;
}
drawn = signature;
const halted = snapshot?.halted === true, valid = snapshot?.version === 1 && UUID.test(snapshot?.generation);
host.dataset.state = halted ? 'halted' : snapshot?.halted === false ? 'running' : 'unknown';
status.textContent = halted ? snapshot.stopping ? 'Processing halted. Stopping workloads.' : 'Processing halted.'
: snapshot?.halted === false ? 'Processing is enabled.' : 'Processing state unavailable.';
const short = byId('processing-short');
if (short) short.textContent = halted ? 'Processing stopped' : snapshot?.halted === false ? 'Processing on' : 'Processing unavailable';
const mobileShort = byId('mobile-processing-link');
if (mobileShort && short) mobileShort.textContent = short.textContent;
document.dispatchEvent(new CustomEvent('mosaic-processing-state', { detail: { halted: snapshot?.halted ?? null } }));
banner.hidden = !halted;
banner.textContent = 'Processing is halted for this instance. Reads and human recovery controls remain available.';
halt.disabled = out() || !canWrite || busy;
resume.hidden = !halted;
resume.disabled = out() || !canWrite || busy || !valid;
unavailable.hidden = canWrite;
unavailable.textContent = out() ? 'Sign in to use processing controls.'
: 'Processing controls are unavailable here. Use mosaic halt or mosaic resume from the instance CLI.';
}
async function refresh() {
if (busy || out()) return;
const current = ++sequence;
try {
const [configResponse, stateResponse] = await Promise.all([
fetch('/api/config', { cache: 'no-store', redirect: 'manual' }),
fetch('/api/processing', { cache: 'no-store', redirect: 'manual' }),
]);
const [config, state] = await Promise.all([responseBody(configResponse), responseBody(stateResponse)]);
if (current !== sequence || out()) return;
canWrite = configResponse.ok && config.send === true;
snapshot = stateResponse.ok && [true, false, null].includes(state.halted) ? state : null;
} catch {
if (current !== sequence || out()) return;
canWrite = false; snapshot = null;
}
draw();
}
async function write(action) {
if (out() || !canWrite || busy) return;
if (action === 'resume' && (!snapshot?.halted || !UUID.test(snapshot.generation) ||
!window.confirm('Resume processing for this instance? New work may launch again.'))) return;
const args = action === 'halt' ? {} : { generation: snapshot.generation };
busy = true; sequence++; receipt.textContent = 'Request in progress.'; draw();
try {
const response = await fetch('/api/processing/' + action, {
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(args), redirect: 'manual', cache: 'no-store',
});
const result = await responseBody(response);
if (out()) return;
if (!response.ok) throw Error();
if (typeof result.halted !== 'boolean' || result.version !== 1 || !UUID.test(result.generation)) throw Error();
snapshot = result;
receipt.textContent = result.quiescent === false ? 'Processing is halted. Some stops are unconfirmed. Read the current status.'
: result.halted ? 'Processing halted.' : 'Processing resumed. Future work may launch.';
} catch {
if (!out()) receipt.textContent = 'Outcome unknown. Read processing status before trying again. Nothing was retried.';
} finally {
busy = false; draw(); await refresh();
}
}
halt.addEventListener('click', () => write('halt'));
resume.addEventListener('click', () => write('resume'));
document.addEventListener('mosaic-signed-out', () => {
clearInterval(timer); sequence++; canWrite = false; snapshot = null; receipt.textContent = ''; draw();
});
window.addEventListener('hashchange', refresh);
window.addEventListener('focus', refresh);
// All deferred modules, including app.js's shared sign-out listener, are ready first.
document.addEventListener('DOMContentLoaded', () => {
if (out()) return;
timer = setInterval(refresh, 5000);
refresh();
}, { once: true });
})();