Add Zapier interface simulator, tree-node Merkle lookups, and user docs
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.
This commit is contained in:
George Lambert 2026-09-11 12:58:03 -04:00
parent 3daa88866d
commit f3dc0e6eee
56 changed files with 2341 additions and 8 deletions

View file

@ -0,0 +1,206 @@
<!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 &amp; 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>