master-zapier-plan-draft/packages/verae-zapier-simulator/public/index.html
George Lambert b68fefdea8
Some checks are pending
offline / test (push) Waiting to run
Close the UI-review follow-ups: names typeahead, fleet form, overflow flip.
Staff pages typeahead customers by name (Ada, not cust_1). Add-machine is
two rows with a wide identity path and filename picker. Overflow menus
flip up near the viewport edge. Customer list no longer leaks password
hashes. UI-REVIEW.pdf remaining list is the three leftover items.
2026-09-11 18:18:37 -04:00

215 lines
12 KiB
HTML
Raw Permalink 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>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%234f46e5'/%3E%3C/svg%3E"/>
<style>
:root {
--ink: #171a26; --muted: #6b7186; --line: #e5e7f0; --bg: #f4f5fb;
--panel: #fff; --accent: #4f46e5; --fault: #ea580c; --ok: #047857; --err: #dc2626;
--warn: #8a5a00; --silent: #6a7a86; --nats: #4f46e5;
}
* { box-sizing: border-box; }
body { margin: 0; font: 14px/1.45 -apple-system,"SF Pro Text","Segoe UI",sans-serif; color: var(--ink); background: var(--bg); }
.skip { position:absolute; left:-999px; }
.skip:focus { left:1rem; top:1rem; z-index:20; background:#fff; color:var(--accent); padding:.5rem .9rem; border-radius:8px; }
header { background: linear-gradient(160deg,#312e81 0%,#4f46e5 60%,#7c74f0 100%); color: #eef0fe; 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: .75; }
header p { margin: 0; color: #e4e7ff; 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: 8px; padding: .55rem .9rem; font: 650 13px system-ui; cursor: pointer; }
button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
button.ghost { background: #fff; color: var(--ink); border: 1px solid var(--line); }
button.fault { background: var(--fault); }
.faults { border: 1px solid #fed7aa; background: #fff7ed; border-radius: 10px; padding: .55rem .7rem; }
.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: 4.5rem 1.2rem minmax(7rem,11rem) minmax(0,1fr); gap: .45rem; padding: .28rem .8rem; font: 12px/1.35 ui-monospace, Menlo, monospace; border-left: 3px solid transparent; }
.hop .mod, .hop .addr, .hop strong { overflow-wrap: anywhere; word-break: break-word; }
.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>
<a class="skip" href="#main">Skip to content</a>
<header>
<div>
<div class="kicker">simulator · not live Zapier · binds 0.0.0.0:3847</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. HTTP on all interfaces; NATS stays private.</p>
</header>
<div class="layout" id="main">
<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="faults checks">
<p style="color:var(--muted);font-size:12px;margin:.1rem 0 .4rem">Orange is the fault accent only.</p>
<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">Run a step to fill this trace.</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>