/* 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 }); })();