Some checks are pending
offline / test (push) Waiting to run
In-process trace console validates hops, faults, recoveries, and suggested changes before zapier-platform push. User guide covers signup through central-chain and bulk-summary tree-node hash lookup.
206 lines
11 KiB
HTML
206 lines
11 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8"/>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||
<title>Verae × Zapier simulator</title>
|
||
<style>
|
||
:root {
|
||
--ink: #12202c; --muted: #5b6d78; --line: #d5dee4; --bg: #f3efe8;
|
||
--panel: #fff; --accent: #ff4a00; --ok: #0f6e56; --err: #a32020;
|
||
--warn: #8a5a00; --silent: #6a7a86; --nats: #1d4f91;
|
||
}
|
||
* { box-sizing: border-box; }
|
||
body { margin: 0; font: 14px/1.45 system-ui, sans-serif; color: var(--ink); background: var(--bg); }
|
||
header { background: var(--ink); color: #f6f3ee; padding: .9rem 1.1rem; display: flex; gap: 1rem; align-items: baseline; flex-wrap: wrap; }
|
||
header h1 { font-size: 1.05rem; margin: 0; font-weight: 650; }
|
||
header .kicker { letter-spacing: .12em; text-transform: uppercase; font: 700 10px system-ui; opacity: .7; }
|
||
header p { margin: 0; color: #c5d0d8; font-size: 13px; }
|
||
.layout { display: grid; grid-template-columns: minmax(280px, 360px) 1fr; min-height: calc(100vh - 52px); }
|
||
@media (max-width: 900px) { .layout { grid-template-columns: 1fr; } }
|
||
aside { background: var(--panel); border-right: 1px solid var(--line); padding: 1rem; }
|
||
h2 { font-size: 13px; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); margin: 1.1rem 0 .4rem; }
|
||
label { display: block; font-size: 12px; color: var(--muted); margin: .45rem 0 .15rem; }
|
||
input, select, textarea { width: 100%; padding: .45rem .5rem; border: 1px solid var(--line); border-radius: 6px; font: 13px ui-monospace, Menlo, monospace; }
|
||
textarea { min-height: 72px; }
|
||
.row { display: flex; gap: .5rem; flex-wrap: wrap; }
|
||
.checks label { display: flex; gap: .4rem; align-items: center; font-size: 13px; color: var(--ink); margin: .2rem 0; }
|
||
.checks input { width: auto; }
|
||
button { background: var(--accent); color: #fff; border: 0; border-radius: 6px; padding: .55rem .9rem; font: 650 13px system-ui; cursor: pointer; }
|
||
button.ghost { background: #e8eeef; color: var(--ink); }
|
||
.main { display: grid; grid-template-rows: auto 1fr auto; min-height: 0; }
|
||
.response, .monitors { background: var(--panel); border-bottom: 1px solid var(--line); padding: .8rem 1rem; }
|
||
.monitors { border-bottom: 0; border-top: 1px solid var(--line); max-height: 34vh; overflow: auto; }
|
||
pre { margin: 0; font: 12px/1.4 ui-monospace, Menlo, monospace; white-space: pre-wrap; }
|
||
.console { background: #0f171d; color: #d7e2ea; padding: .6rem 0; overflow: auto; min-height: 220px; }
|
||
.hop { display: grid; grid-template-columns: 52px 18px 150px 1fr; gap: .45rem; padding: .28rem .8rem; font: 12px/1.35 ui-monospace, Menlo, monospace; border-left: 3px solid transparent; }
|
||
.hop:hover { background: #18232c; }
|
||
.hop.ok { border-color: var(--ok); }
|
||
.hop.error { border-color: var(--err); color: #ffb4b0; }
|
||
.hop.silent { border-color: var(--silent); color: #8fa0ab; }
|
||
.hop.retry { border-color: #d39b00; }
|
||
.hop.delay { border-color: #c27a00; }
|
||
.mod { color: #9ad7c2; }
|
||
.addr { color: #8cb4ea; }
|
||
.pill { display: inline-block; font: 700 10px system-ui; letter-spacing: .06em; text-transform: uppercase; padding: .1rem .35rem; border-radius: 4px; background: #e8eeef; margin-right: .3rem; }
|
||
.pill.bad { background: #f8d4d4; color: var(--err); }
|
||
.pill.good { background: #d4efe6; color: var(--ok); }
|
||
.pill.warn { background: #f5e6c8; color: var(--warn); }
|
||
.sug { margin: .35rem 0; padding: .45rem .55rem; background: #faf6ee; border: 1px solid var(--line); border-radius: 6px; }
|
||
.find { margin: .25rem 0; font-size: 13px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<div>
|
||
<div class="kicker">simulator · not live Zapier</div>
|
||
<h1>Verae Time × Zapier trace console</h1>
|
||
</div>
|
||
<p>Every hop from Zap input → zappier-edge → middleware → splitter → chain / WORM / tree nodes → Zap output. NATS addresses are in-process. Loopback only.</p>
|
||
</header>
|
||
<div class="layout">
|
||
<aside>
|
||
<h2>Zapier step</h2>
|
||
<label>Action</label>
|
||
<select id="action"></select>
|
||
<label>Data / items (batch = one per line)</label>
|
||
<textarea id="data" placeholder="hello from zapier">hello from zapier</textarea>
|
||
<label>SHA256 (lookup; blank = hash Data)</label>
|
||
<input id="sha256" placeholder="64 hex chars"/>
|
||
<label>Public metadata JSON</label>
|
||
<input id="publicMeta" placeholder='{"source":"zap"}'/>
|
||
<label>Certificate (verify)</label>
|
||
<input id="certificate"/>
|
||
<label>Email (signup)</label>
|
||
<input id="email" value="ada@example.com"/>
|
||
<div class="checks">
|
||
<label><input type="checkbox" id="includeAttached"/> includeAttached (query WORM)</label>
|
||
<label><input type="checkbox" id="includeTree"/> includeTree (query tree nodes)</label>
|
||
</div>
|
||
<h2>Inject faults</h2>
|
||
<div class="checks">
|
||
<label><input type="checkbox" id="failEdge401"/> edge 401</label>
|
||
<label><input type="checkbox" id="failEdge402"/> edge 402 quota</label>
|
||
<label><input type="checkbox" id="failChainTimeout"/> chain timeout</label>
|
||
<label><input type="checkbox" id="failNatsDropWatch"/> drop jobs.watch</label>
|
||
<label><input type="checkbox" id="failNatsDropEvents"/> drop jobs.events</label>
|
||
<label><input type="checkbox" id="failArchiveAllSilent"/> all archives silent</label>
|
||
<label><input type="checkbox" id="failArchiveOneDown"/> one tree node down</label>
|
||
<label><input type="checkbox" id="recover"/> recover (retry)</label>
|
||
</div>
|
||
<label>Delay chain ms</label>
|
||
<input id="chainMs" type="number" min="0" value="0"/>
|
||
<label>Delay archive ms</label>
|
||
<input id="archiveMs" type="number" min="0" value="0"/>
|
||
<div class="row" style="margin-top:.8rem">
|
||
<button id="run">Run step</button>
|
||
<button class="ghost" id="reset">Reset world</button>
|
||
</div>
|
||
<h2>Scenarios</h2>
|
||
<div class="row">
|
||
<button class="ghost" data-sc="register">Register</button>
|
||
<button class="ghost" data-sc="lookup">Lookup chain</button>
|
||
<button class="ghost" data-sc="batch">Batch Merkle</button>
|
||
<button class="ghost" data-sc="leaf">Lookup leaf</button>
|
||
<button class="ghost" data-sc="tree">Lookup tree</button>
|
||
</div>
|
||
<p style="color:var(--muted);font-size:12px;margin-top:.8rem">World is sticky: batch first, then lookup a leaf. Central lookup should miss; tree lookup should prove inclusion.</p>
|
||
</aside>
|
||
<div class="main">
|
||
<section class="response">
|
||
<h2 style="margin-top:0">Response to Zapier</h2>
|
||
<pre id="response">Run a step to see output.</pre>
|
||
</section>
|
||
<section class="console" id="console" aria-label="trace console"></section>
|
||
<section class="monitors">
|
||
<h2 style="margin-top:0">Monitors & suggested modifications</h2>
|
||
<div id="monitors"></div>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
<script>
|
||
const $ = (id) => document.getElementById(id);
|
||
async function j(url, opts) {
|
||
const r = await fetch(url, opts);
|
||
return r.json();
|
||
}
|
||
function faults() {
|
||
const f = {};
|
||
for (const id of ['failEdge401','failEdge402','failChainTimeout','failNatsDropWatch','failNatsDropEvents','failArchiveAllSilent','failArchiveOneDown','recover']) {
|
||
if ($(id).checked) f[id] = true;
|
||
}
|
||
const chainMs = Number($('chainMs').value || 0);
|
||
const archiveMs = Number($('archiveMs').value || 0);
|
||
if (chainMs) f.chainMs = chainMs;
|
||
if (archiveMs) f.archiveMs = archiveMs;
|
||
return f;
|
||
}
|
||
function inputFor(action) {
|
||
const data = $('data').value;
|
||
const sha = $('sha256').value.trim();
|
||
let publicMetadata;
|
||
try { publicMetadata = $('publicMeta').value ? JSON.parse($('publicMeta').value) : undefined; } catch { publicMetadata = { raw: $('publicMeta').value }; }
|
||
const base = { data, sha256: sha || undefined, publicMetadata, certificate: $('certificate').value, email: $('email').value, items: data };
|
||
return base;
|
||
}
|
||
function renderTrace(events) {
|
||
const el = $('console');
|
||
el.innerHTML = events.map((e) => {
|
||
const arrow = e.dir === 'out' ? '←' : e.dir === 'in' ? '→' : '·';
|
||
return `<div class="hop ${e.status}">
|
||
<span>${String(e.tMs).padStart(4,' ')}ms</span>
|
||
<span>${arrow}</span>
|
||
<span class="mod">${e.module}</span>
|
||
<span><strong>${e.hop}</strong> <span class="addr">${e.address || ''}</span> ${e.note ? '— ' + e.note : ''}</span>
|
||
</div>`;
|
||
}).join('');
|
||
el.scrollTop = el.scrollHeight;
|
||
}
|
||
function renderMonitor(m) {
|
||
const el = $('monitors');
|
||
const findings = (m.findings || []).map((f) => {
|
||
const pill = f.ok ? 'good' : (f.kind === 'delay' ? 'warn' : 'bad');
|
||
return `<div class="find"><span class="pill ${pill}">${f.kind}</span><strong>${f.title}</strong> — ${f.detail}</div>`;
|
||
}).join('');
|
||
const sug = (m.suggestions || []).map((s) =>
|
||
`<div class="sug"><span class="pill ${s.severity === 'high' ? 'bad' : 'warn'}">${s.severity}</span>${s.text}</div>`
|
||
).join('');
|
||
el.innerHTML = `<div class="find"><span class="pill ${m.ok ? 'good' : 'bad'}">${m.ok ? 'pass' : 'issues'}</span>${m.stats.hops} hops · ${m.stats.errors} errors · ${m.stats.retries} retries · ${m.stats.delays} delays · ${m.stats.natsHops} NATS-address hops</div>` + findings + (sug || '<p style="color:#5b6d78">No modifications suggested for this run.</p>');
|
||
}
|
||
async function run() {
|
||
const action = $('action').value;
|
||
const body = {
|
||
action,
|
||
input: inputFor(action),
|
||
includeAttached: $('includeAttached').checked,
|
||
includeTree: $('includeTree').checked,
|
||
faults: faults(),
|
||
};
|
||
const out = await j('/api/run', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
|
||
$('response').textContent = JSON.stringify(out.response, null, 2);
|
||
if (out.response?.certificate) $('certificate').value = out.response.certificate;
|
||
if (out.response?.sha256 && action === 'create_timestamp') $('sha256').value = out.response.sha256;
|
||
if (out.response?.leaves) $('sha256').value = out.response.leaves[0];
|
||
renderTrace(out.trace);
|
||
renderMonitor(out.monitor);
|
||
}
|
||
async function boot() {
|
||
const { actions } = await j('/api/actions');
|
||
$('action').innerHTML = actions.map((a) => `<option value="${a.key}">${a.label}</option>`).join('');
|
||
$('action').value = 'timestamp_and_wait';
|
||
}
|
||
$('run').onclick = run;
|
||
$('reset').onclick = async () => { await j('/api/reset', { method: 'POST' }); $('console').innerHTML = ''; $('response').textContent = 'World reset.'; $('monitors').textContent = ''; };
|
||
document.querySelectorAll('[data-sc]').forEach((b) => b.addEventListener('click', async () => {
|
||
const sc = b.dataset.sc;
|
||
if (sc === 'register') { $('action').value = 'timestamp_and_wait'; $('includeAttached').checked = true; }
|
||
if (sc === 'lookup') { $('action').value = 'hash_lookup'; }
|
||
if (sc === 'batch') { $('action').value = 'batch_timestamp'; $('data').value = 'invoice-1001\ninvoice-1002\ninvoice-1003'; }
|
||
if (sc === 'leaf') { $('action').value = 'hash_lookup'; $('includeTree').checked = false; }
|
||
if (sc === 'tree') { $('action').value = 'tree_lookup'; $('includeTree').checked = true; }
|
||
await run();
|
||
}));
|
||
boot();
|
||
</script>
|
||
</body>
|
||
</html>
|