master-zapier-plan-draft/packages/verae-zapier-simulator/public/index.html
George Lambert f3dc0e6eee
Some checks are pending
offline / test (push) Waiting to run
Add Zapier interface simulator, tree-node Merkle lookups, and user docs
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.
2026-09-11 12:58:03 -04:00

206 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>