220 lines
7.6 KiB
JavaScript
220 lines
7.6 KiB
JavaScript
(function () {
|
|
const params = new URLSearchParams(window.location.search);
|
|
const accessToken = params.get('access_token') || '';
|
|
const apiBase =
|
|
document.querySelector('meta[name="omnl-api-base"]')?.getAttribute('content')?.trim() || '/api/v1';
|
|
|
|
const els = {
|
|
status: document.getElementById('load-status'),
|
|
grid: document.getElementById('posture-grid'),
|
|
actions: document.getElementById('pending-actions'),
|
|
evidence: document.getElementById('evidence-kv'),
|
|
safe: document.getElementById('safe-kv'),
|
|
external: document.getElementById('external-kv'),
|
|
triple: document.getElementById('triple-summary'),
|
|
signoffs: document.getElementById('signoffs-summary'),
|
|
raw: document.getElementById('raw-json'),
|
|
refreshed: document.getElementById('refreshed-at'),
|
|
};
|
|
|
|
function apiHeaders() {
|
|
const h = { Accept: 'application/json' };
|
|
if (accessToken) h.Authorization = 'Bearer ' + accessToken;
|
|
return h;
|
|
}
|
|
|
|
function apiUrl(path) {
|
|
const base = apiBase.replace(/\/$/, '');
|
|
const rel = path.startsWith('/') ? path : '/' + path;
|
|
const u = new URL(base + rel, window.location.origin);
|
|
if (accessToken) u.searchParams.set('access_token', accessToken);
|
|
return u.toString();
|
|
}
|
|
|
|
function badge(ok, okLabel, badLabel) {
|
|
if (ok === true) return '<span class="badge ok">' + okLabel + '</span>';
|
|
if (ok === false) return '<span class="badge bad">' + badLabel + '</span>';
|
|
return '<span class="badge neutral">Unknown</span>';
|
|
}
|
|
|
|
function sevBadge(sev) {
|
|
const map = { critical: 'bad', high: 'bad', medium: 'warn', low: 'neutral' };
|
|
return '<span class="badge ' + (map[sev] || 'neutral') + '">' + sev + '</span>';
|
|
}
|
|
|
|
function renderPosture(data) {
|
|
const p = data.posture || {};
|
|
els.grid.innerHTML = [
|
|
card('Reporting', badge(p.reportingCompliant, 'Compliant', 'Not compliant'), ''),
|
|
card('Attestation', badge(!p.attestationStale, 'Fresh', 'Stale'), ''),
|
|
card('Triple reconcile', badge(p.tripleAligned, 'Aligned', 'Breaks'), ''),
|
|
card('Notary gate', badge(p.requireNotarizedEvidence, 'Enforced', 'Not enforced'), 'requireNotarizedEvidence'),
|
|
card('Threshold', '<div class="metric">' + (p.complianceThreshold || '—') + '</div>', 'IPSAS / IFRS / US GAAP'),
|
|
card('Policy v', '<div class="metric">' + (p.jurisdictionPolicyVersion ?? '—') + '</div>', 'ID jurisdiction'),
|
|
].join('');
|
|
}
|
|
|
|
function card(title, body, sub) {
|
|
return (
|
|
'<div class="card"><h2>' +
|
|
title +
|
|
'</h2>' +
|
|
body +
|
|
(sub ? '<small style="color:var(--muted);font-size:0.78rem">' + sub + '</small>' : '') +
|
|
'</div>'
|
|
);
|
|
}
|
|
|
|
function renderActions(actions) {
|
|
if (!actions || !actions.length) {
|
|
els.actions.innerHTML = '<p style="color:var(--muted)">No pending actions.</p>';
|
|
return;
|
|
}
|
|
els.actions.innerHTML =
|
|
'<ul class="actions-list">' +
|
|
actions
|
|
.map(function (a) {
|
|
const links = (a.links || [])
|
|
.map(function (l) {
|
|
const href = l.href.startsWith('http') ? l.href : apiUrl(l.href);
|
|
const dl = l.href.indexOf('safe-notary-gate-tx') >= 0 ? ' download="omnl-safe-notary-gate-tx.json"' : '';
|
|
return '<a href="' + href + '"' + dl + '>' + l.label + '</a>';
|
|
})
|
|
.join('');
|
|
return (
|
|
'<li><h3>' +
|
|
sevBadge(a.severity) +
|
|
' ' +
|
|
esc(a.title) +
|
|
'</h3><p>' +
|
|
esc(a.detail) +
|
|
'</p><div class="link-row">' +
|
|
links +
|
|
'</div></li>'
|
|
);
|
|
})
|
|
.join('') +
|
|
'</ul>';
|
|
}
|
|
|
|
function kv(rows) {
|
|
return (
|
|
'<dl class="kv">' +
|
|
rows
|
|
.map(function (r) {
|
|
return '<dt>' + esc(r[0]) + '</dt><dd>' + esc(String(r[1] ?? '—')) + '</dd>';
|
|
})
|
|
.join('') +
|
|
'</dl>'
|
|
);
|
|
}
|
|
|
|
function renderEvidence(data) {
|
|
const pr = data.proofReport;
|
|
const gate = data.onChainGate || {};
|
|
const triple = data.tripleReconcile;
|
|
els.evidence.innerHTML = kv([
|
|
['Evidence hash', pr && pr.evidenceHash],
|
|
['Merkle root', pr && pr.merkleRoot],
|
|
['Package 3-of-3', pr && pr.packageNotarized3of3],
|
|
['Reserve 3-of-3', pr && pr.reserveAttested3of3],
|
|
['Reserve store', gate.reserveStore],
|
|
['Notary registry', gate.notaryRegistry],
|
|
['Attestation threshold', gate.attestationThreshold],
|
|
['On-chain R', triple && triple.onChain && triple.onChain.r],
|
|
]);
|
|
}
|
|
|
|
function renderSafe(data) {
|
|
const pr = data.proofReport;
|
|
const g = (pr && pr.gnosisSafe) || {};
|
|
const sw = data.safeWallet || {};
|
|
els.safe.innerHTML = kv([
|
|
['Admin Safe', g.address || data.web3.deployed.GnosisSafeAdmin],
|
|
['Threshold', g.threshold ? g.threshold + '-of-' + g.owners : '—'],
|
|
['Safe Wallet registry', sw.npmRegistryHas138 ? 'chain 138 listed' : 'pending PR #1568'],
|
|
['Registry status', sw.status],
|
|
]);
|
|
}
|
|
|
|
function renderExternal(data) {
|
|
const ev = data.externalVisibility || {};
|
|
els.external.innerHTML = kv([
|
|
['DefiLlama chain', ev.defiLlama && ev.defiLlama.chainPage],
|
|
['Pricing PR', ev.defiLlama && ev.defiLlama.pr12094],
|
|
['Bridge TVL PR', ev.defiLlama && ev.defiLlama.pr19451],
|
|
['Safe deployments PR', ev.safeDeployments && ev.safeDeployments.pr1568],
|
|
]);
|
|
}
|
|
|
|
function renderTriple(data) {
|
|
const t = data.tripleReconcile;
|
|
if (!t) {
|
|
els.triple.textContent = 'Triple-state reconcile unavailable.';
|
|
return;
|
|
}
|
|
const breaks = (t.breaks || []).length;
|
|
els.triple.innerHTML =
|
|
badge(t.aligned, 'Aligned', breaks + ' break(s)') +
|
|
' <span style="color:var(--muted);margin-left:0.5rem;font-size:0.88rem">line ' +
|
|
esc(t.lineId).slice(0, 18) +
|
|
'…</span>';
|
|
}
|
|
|
|
function renderSignoffs(data) {
|
|
const s = data.signoffs || {};
|
|
els.signoffs.innerHTML = '<pre class="raw" style="max-height:10rem">' + esc(JSON.stringify(s, null, 2)) + '</pre>';
|
|
}
|
|
|
|
function esc(s) {
|
|
return String(s)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|
|
|
|
function setStatus(kind, msg) {
|
|
els.status.className = 'status-bar ' + kind;
|
|
els.status.textContent = msg;
|
|
}
|
|
|
|
async function load() {
|
|
setStatus('loading', 'Loading compliance console…');
|
|
try {
|
|
const res = await fetch(apiUrl('/omnl/compliance/console'), { headers: apiHeaders() });
|
|
const text = await res.text();
|
|
let data;
|
|
try {
|
|
data = JSON.parse(text);
|
|
} catch {
|
|
throw new Error(res.status + ' — non-JSON response');
|
|
}
|
|
if (!res.ok) throw new Error(data.error || res.statusText);
|
|
|
|
renderPosture(data);
|
|
renderActions(data.pendingActions);
|
|
renderEvidence(data);
|
|
renderSafe(data);
|
|
renderExternal(data);
|
|
renderTriple(data);
|
|
renderSignoffs(data);
|
|
els.raw.textContent = JSON.stringify(data, null, 2);
|
|
els.refreshed.textContent = 'Updated ' + new Date(data.generatedAt).toLocaleString();
|
|
setStatus('ok', 'Live — ' + data.pendingActions.length + ' pending action(s)');
|
|
} catch (e) {
|
|
setStatus('error', 'Failed to load: ' + (e && e.message ? e.message : e));
|
|
}
|
|
}
|
|
|
|
document.getElementById('btn-refresh').addEventListener('click', load);
|
|
document.getElementById('btn-download-safe').addEventListener('click', function () {
|
|
window.location.href = apiUrl('/omnl/compliance/safe-notary-gate-tx');
|
|
});
|
|
document.getElementById('btn-toggle-raw').addEventListener('click', function () {
|
|
document.getElementById('raw-section').classList.toggle('hidden');
|
|
});
|
|
|
|
load();
|
|
})();
|