64 lines
4.8 KiB
JavaScript
64 lines
4.8 KiB
JavaScript
'use strict';
|
|
|
|
(function exposeEtahenLinkUx(root, factory) {
|
|
const api = factory();
|
|
if (typeof module === 'object' && module.exports) module.exports = api;
|
|
if (root) root.ChimeraLinkUx = api;
|
|
})(typeof window === 'object' ? window : null, () => {
|
|
const STATES = new Set(['online', 'offline', 'unknown']);
|
|
const JOB_STATES = new Set(['none', 'staged', 'approval_required', 'recovery_required']);
|
|
const safeState = (value, allowed, fallback) => allowed.has(String(value || '')) ? String(value) : fallback;
|
|
const text = (value, fallback = '') => String(value || fallback).slice(0, 160);
|
|
|
|
function createModel(packet = {}) {
|
|
const runtime = packet.runtime || {};
|
|
const last = packet.lastInventory || null;
|
|
const etaHenState = safeState(runtime.state, STATES, 'unknown');
|
|
const agentState = safeState(packet.agent?.state, STATES, etaHenState === 'online' && last ? 'online' : 'unknown');
|
|
const chimeraState = last ? 'online' : 'unknown';
|
|
const jobState = safeState(packet.job?.state, JOB_STATES, 'none');
|
|
const stagedCount = Math.max(0, Number(packet.job?.stagedCount) || 0);
|
|
return Object.freeze({
|
|
etaHen: Object.freeze({ state: etaHenState, version: text(runtime.identity?.version, '—') }),
|
|
agent: Object.freeze({ state: agentState, paired: packet.agent?.paired === true }),
|
|
chimera: Object.freeze({ state: chimeraState, lastScanAt: last?.completedAt || null }),
|
|
transfer: Object.freeze({ state: jobState, stagedCount, installedCount: 0 }),
|
|
recoveryRequired: jobState === 'recovery_required',
|
|
approvalRequired: jobState === 'approval_required',
|
|
});
|
|
}
|
|
|
|
function exactConfirmation({ action, target, runtimeEpoch, localIntent, recoveryAvailable }) {
|
|
if (!/^[A-Z][A-Z0-9_]{2,31}$/.test(String(action || ''))) throw new Error('Invalid action.');
|
|
if (!/^[A-Za-z0-9_-]{8,96}$/.test(String(target || '')) || !/^[A-Za-z0-9_-]{8,96}$/.test(String(runtimeEpoch || ''))) throw new Error('Invalid confirmation identity.');
|
|
if (localIntent !== true) throw new Error('Local console intent is required.');
|
|
if (recoveryAvailable !== true) throw new Error('Verified recovery is required.');
|
|
return `CONFIRM ${action} ${target} ${runtimeEpoch}`;
|
|
}
|
|
|
|
function mount({ root, loadStatus, translate = (_key, fallback) => fallback } = {}) {
|
|
if (!root || typeof loadStatus !== 'function') return null;
|
|
const t = translate;
|
|
root.innerHTML = `<section class="card etahen-link-card" aria-labelledby="etahen-link-title">
|
|
<div class="card-header"><div><span class="etahen-link-kicker">${t('etahenLink.kicker', 'Console companion')}</span><h3 id="etahen-link-title">${t('etahenLink.title', 'Chimera Link for etaHEN')}</h3><p class="hint">${t('etahenLink.description', 'Separate health for etaHEN, the paired agent and Chimera evidence.')}</p></div><button id="etahen-link-refresh" class="btn btn-sm" type="button">${t('common.refresh', 'Refresh')}</button></div>
|
|
<div class="etahen-link-health" role="list" aria-label="${t('etahenLink.healthLabel', 'Chimera Link health')}"></div>
|
|
<div class="etahen-link-alert" role="status" aria-live="polite"></div>
|
|
</section>`;
|
|
const health = root.querySelector('.etahen-link-health'); const alert = root.querySelector('.etahen-link-alert'); const button = root.querySelector('#etahen-link-refresh');
|
|
const label = state => t(`etahenLink.state.${state}`, state === 'online' ? 'Online' : state === 'offline' ? 'Offline' : 'Unknown');
|
|
const render = packet => {
|
|
const model = createModel(packet);
|
|
health.innerHTML = [['etaHEN', model.etaHen], [t('etahenLink.agent', 'Link agent'), model.agent], ['Chimera', model.chimera]].map(([name, item]) => `<div class="etahen-link-health-item state-${item.state}" role="listitem"><span>${name}</span><strong>${label(item.state)}</strong></div>`).join('');
|
|
alert.className = `etahen-link-alert ${model.recoveryRequired ? 'is-critical' : ''}`;
|
|
alert.textContent = model.recoveryRequired ? t('etahenLink.recoveryRequired', 'Recovery required before any new action.') : model.approvalRequired ? t('etahenLink.approvalRequired', 'Approval must be confirmed locally on the console.') : model.transfer.stagedCount ? t('etahenLink.stagedOnly', 'Artifact staged — not installed.') : t('etahenLink.noPendingWork', 'No pending Link work.');
|
|
return model;
|
|
};
|
|
const refresh = async () => { button.disabled = true; try { return render(await loadStatus()); } catch (_) { return render({ runtime: { state: 'unknown' } }); } finally { button.disabled = false; } };
|
|
button.addEventListener('click', refresh); root.addEventListener('keydown', event => { if (event.key === 'Enter' && event.target === root) refresh(); });
|
|
root.tabIndex = 0; refresh();
|
|
return Object.freeze({ refresh, render });
|
|
}
|
|
|
|
return Object.freeze({ createModel, exactConfirmation, mount });
|
|
});
|