Tokens (light + dark, neumorphism, 54/54 WCAG AA contrast pairs), the self-hosted JetBrains Mono fonts, and interactive mockups of every screen across three iterations. Iteration 3 is final: phone, tablet and desktop tiers, device preview, and "empty spool weight" terminology. The build scripts regenerate the screens byte-identically from source. Implementation is tracked in Maestro feature UI Refresh - V3.0 (224b72a1-8d84-4970-97ea-5dafa4d8ea3b). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
54 lines
7.1 KiB
HTML
54 lines
7.1 KiB
HTML
<style>
|
|
.sysgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr)); gap: var(--space-5); align-items: start; }
|
|
.syscard { padding: var(--space-5); display: flex; flex-direction: column; gap: var(--space-4); }
|
|
.syscard h2 { margin: 0; font-size: var(--fs-h3); display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
|
|
.afilters { display: flex; gap: var(--space-3); flex-wrap: wrap; align-items: center; padding: var(--space-4); }
|
|
.afilters .select, .afilters .input { width: auto; min-height: 36px; padding: 6px 10px; font-size: 13px; }
|
|
.act-code { font-size: 12px; }
|
|
</style>
|
|
<script>
|
|
(function () {
|
|
var x = X;
|
|
var S = { who: '', type: '', sent: false, update: false, installed: false };
|
|
var LINK = { spool: function (id) { return '<a href="#spool=' + id + '" data-spool="' + id + '">spool #' + id + '</a>'; }, print: function (id) { return '<a href="#print=' + id + '" data-print="' + id + '">print #' + id + '</a>'; }, printer: function (id) { return '<a href="04-printers.html#printer=' + id + '">printer #' + id + '</a>'; }, spool_type: function (id) { return '<a href="05-lists.html?list=spool_types">spool type #' + id + '</a>'; }, principal: function (id) { return '<a href="07-access.html">person #' + id + '</a>'; } };
|
|
var WST = { delivered: ['ok', 'Delivered'], no_low: ['idle', 'Nothing low'], pending: ['warn', 'Retrying'], expired: ['err', 'Expired'] };
|
|
function webhook() {
|
|
var lowNow = SPOOLS.filter(x.low).length;
|
|
return '<section class="syscard nm-raised" aria-labelledby="wh"><h2 id="wh">Low-filament report <span class="st ok" style="font-size:12px">On</span><span class="prov warn" style="font-size:11px"><i>⚠</i>API exists · no screen on dev yet</span></h2>' +
|
|
'<dl class="kv"><dt>Sends</dt><dd>' + WEBHOOK.schedule + '</dd><dt>Includes</dt><dd>spools under ' + WEBHOOK.threshold + ' g (' + lowNow + ' right now)</dd><dt>Destination</dt><dd class="muted">' + WEBHOOK.target + '</dd></dl>' +
|
|
'<table class="tbl"><thead><tr><th>Week</th><th>Result</th><th class="r">Spools</th><th class="hide-m">Detail</th></tr></thead><tbody>' + WEBHOOK.events.map(function (ev) { var w = WST[ev.state]; return '<tr><td><b>' + ev.period + '</b><span class="sub">' + x.when(ev.observed) + '</span></td><td><span class="st ' + w[0] + '">' + w[1] + '</span>' + (ev.attempts > 1 ? '<span class="sub">' + ev.attempts + ' attempts</span>' : '') + '</td><td class="r">' + (ev.spools || '—') + '</td><td class="hide-m small">' + (ev.lastError ? '<span class="prov warn" style="white-space:normal"><i>!</i>' + x.esc(ev.lastError) + '</span>' : ev.delivered ? 'at ' + x.when(ev.delivered).split(' ').pop() : '<span class="muted">nothing sent</span>') + '</td></tr>'; }).join('') + '</tbody></table>' +
|
|
(S.sent ? '<div class="banner" style="border-color:var(--status-success);background:var(--wash-success)"><b style="color:var(--status-success)">✓</b><span>This week\'s report (2026-W38) was already delivered Monday 08:00, so nothing was sent again. A week that\'s still retrying would be retried now.</span></div>' : '') +
|
|
'<div class="row"><button class="btn btn-sm" type="button" data-send>Run this week\'s report now</button><span class="sub" style="display:inline">One report per week. Running it again never sends a duplicate.</span></div></section>';
|
|
}
|
|
function app() {
|
|
return '<section class="syscard nm-raised" aria-labelledby="ap"><h2 id="ap">This device</h2>' +
|
|
'<dl class="kv"><dt>Version</dt><dd>dev <code>321ce0d</code></dd><dt>Offline use</dt><dd><span class="st ok">Ready</span><span class="sub">App shell is cached; data still needs the server</span></dd><dt>Installed</dt><dd>' + (S.installed ? '<span class="st ok">Yes</span>' : '<span class="st idle">No</span>') + '</dd></dl>' +
|
|
(S.update ? '<div class="banner"><b>↻</b><span style="flex:1">An update is ready. Finish any open form, then reload.</span><button class="btn btn-sm btn-primary" type="button" data-fake="Reloading with the update">Reload now</button></div>' : '') +
|
|
'<div class="row" style="gap:8px">' + (S.installed ? '' : '<button class="btn btn-sm btn-primary" type="button" data-install>Install Extrudex</button>') + '<button class="btn btn-sm" type="button" data-update>Demo: update ready</button></div>' +
|
|
'<p class="sub" style="margin:0">On iPhone or iPad: open in Safari, tap Share, then <b>Add to Home Screen</b>.</p></section>';
|
|
}
|
|
function audit() {
|
|
var who = {}, types = {}; AUDIT.forEach(function (a) { who[a.who] = 1; types[a.type] = 1; });
|
|
var l = AUDIT.filter(function (a) { return (!S.who || a.who === S.who) && (!S.type || a.type === S.type); });
|
|
return '<section class="nm-raised table-card" aria-labelledby="au"><div class="afilters"><h2 id="au" style="margin:0;font-size:var(--fs-h3);flex:1">Audit log <span class="prov warn" style="font-size:11px"><i>⚠</i>API exists · no screen on dev yet</span></h2>' +
|
|
'<select class="select" id="aw" aria-label="Who"><option value="">Anyone</option>' + Object.keys(who).map(function (w) { return '<option' + (S.who === w ? ' selected' : '') + '>' + x.esc(w) + '</option>'; }).join('') + '</select>' +
|
|
'<select class="select" id="at" aria-label="Resource"><option value="">Any resource</option>' + Object.keys(types).map(function (w) { return '<option' + (S.type === w ? ' selected' : '') + '>' + w + '</option>'; }).join('') + '</select></div>' +
|
|
'<div class="table-scroll"><table class="tbl"><thead><tr><th>When</th><th>Who</th><th>What</th><th class="hide-m">Detail</th></tr></thead><tbody>' + (l.length ? l.map(function (a) { return '<tr><td class="nowrap">' + x.when(a.at) + '</td><td>' + x.esc(a.who) + '</td><td><code class="act-code">' + a.action + '</code><span class="sub">' + (LINK[a.type] ? LINK[a.type](a.rid) : a.type + ' #' + a.rid) + '</span></td><td class="hide-m small">' + x.esc(a.ctx) + '</td></tr>'; }).join('') : '<tr><td colspan="4"><div class="empty"><h3>No events match</h3></div></td></tr>') + '</tbody></table></div>' +
|
|
'<div class="table-foot"><span class="muted">Newest first · every write records who, what and when</span><button class="btn btn-sm" type="button" data-fake="Loaded older events (paged by id)">Older →</button></div></section>';
|
|
}
|
|
function render() {
|
|
return '<header class="page-head"><div><h1>System</h1><p class="sum">Admin only. Scheduled reports, the audit trail, and this device.</p></div></header>' +
|
|
'<div class="sysgrid">' + webhook() + app() + '</div>' + audit();
|
|
}
|
|
function redraw() { document.getElementById('page').innerHTML = render(); }
|
|
document.addEventListener('click', function (e) {
|
|
var t = e.target;
|
|
if (t.closest('[data-send]')) { S.sent = true; return redraw(); }
|
|
if (t.closest('[data-update]')) { S.update = true; return redraw(); }
|
|
if (t.closest('[data-install]')) { S.installed = true; X.toast('Installed. Extrudex opens in its own window now.'); return redraw(); }
|
|
});
|
|
document.addEventListener('change', function (e) { if (e.target.id === 'aw') { S.who = e.target.value; redraw(); } if (e.target.id === 'at') { S.type = e.target.value; redraw(); } });
|
|
X.init({ name: 'system' }, render);
|
|
})();
|
|
</script>
|