forgetest's page moves onto Bootstrap with forgectrl's theme; forgectrl pinned at the panel overhaul

The acceptance page is assembled by page.py from forgetest/forgetest/ui/
(index.html, page.css, help.js, app.js) plus theme.css and the vendored
Bootstrap files, which are byte for byte the ones forgectrl's panel
carries, so the two pages look like one product and share the light and
dark themes (same localStorage key). A plain file is read in a checkout;
on the dev image the recipe installs ui/ gzipped and page.py reads the
.gz sibling, inflating once at first request: the rootfs is raw ext4, so
bytes in the package are bytes on the image. The explanatory prose
(campaign rules, the queues, the campaign actions, the prerequisites
switch, the bench intro) is a "?" popover with a link into the
documentation site; operator steps, prompts, notices and the live-laser
acknowledgment stay in the page, and confirmLive() stays a blocking
dialog. The page's own rules hold: rows, prompt buttons and tool entries
are built once and updated in place, and the popovers sit on static
markup only, so no rebuild orphans one. On a phone the Run pane goes to
the top for the duration of a run.

scripts/check-ui-vendor.py compares the shared files against forgectrl
at its pinned revision (or a local checkout with --forgectrl); it runs
in forgetest-ci.yml, so the copies cannot drift.

Tests: test_page.py (the gzipped install assembles to the same bytes as
a checkout, one self-contained response, the token placeholder once, a
missing marker refused); test_server asserts the served page's
invariants; test_responsiveness keeps its rules with needles pointed at
the new files, its ASCII rule applied to our own sources (Bootstrap's
CSS carries an em dash of its own), and its self-contained rule testing
asset tags rather than the presence of https:// (the documentation links
are meant to be there). forgectrl.panel-serves gains two needles for the
panel's theme attribute and save bar. Proof: the unit suite, and the
page in Chrome against a fake catalog (both themes, popovers, the bench
tab, a full operator run with its prompt, abort).

forgectrl pinned at 9d1f6f2 (the panel on Bootstrap, one save bar, help
popovers, themes, the gzipped page); PV unchanged. The pin moves only
forgectrl's fingerprint. The forgetest changes are the harness's own and
have no catalog consequence.
This commit is contained in:
ScottW514
2026-08-24 12:13:53 -04:00
parent bb186247d1
commit e99888753f
18 changed files with 2002 additions and 489 deletions
+130
View File
@@ -0,0 +1,130 @@
<!--
index.html - forgetest: the acceptance page
Copyright (c) 2026 Scott Wiederhold <s.e.wiederhold@gmail.com>
SPDX-License-Identifier: MIT
Served at "/" as one self-contained page: page.py inlines every
stylesheet and script linked below, in order, in place of its
link/script tag. No external assets. Bootstrap (vendor/) is the UI
library; theme.css is the OpenGlow theme forgectrl's panel uses, byte
for byte, in a light and a dark variant selected by the data-bs-theme
attribute the head script sets before first paint; page.css is this
page's own layout. Design notes are at the top of app.js.
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>ForgeFIRM acceptance</title>
<script>
(function () {
var t = 'light';
try {
var s = localStorage.getItem('ff_theme');
if (s === 'light' || s === 'dark') t = s;
else if (window.matchMedia && matchMedia('(prefers-color-scheme: dark)').matches)
t = 'dark';
} catch (e) {}
document.documentElement.setAttribute('data-bs-theme', t);
})();
</script>
<link rel="stylesheet" href="vendor/bootstrap.min.css" />
<link rel="stylesheet" href="theme.css" />
<link rel="stylesheet" href="page.css" />
</head>
<body>
<header>
<div class="hrow">
<span class="appname">ForgeFIRM acceptance</span><span class="sub" id="hdrsub"></span
><span class="ver" id="hdrver"></span
><button id="themebtn" type="button" onclick="cycleTheme()" title="Theme">Theme</button>
</div>
</header>
<nav>
<div class="nrow">
<a id="tab-acceptance" class="on" onclick='showTab("acceptance")'>Release acceptance</a
><a id="tab-bench" onclick='showTab("bench")'>Bench diagnostics</a>
</div>
</nav>
<main>
<div id="left">
<div id="pane-acceptance">
<div class="card">
<h2>Campaign<button class="hlp" data-help="campaign" type="button">?</button></h2>
<div class="banner">
<div class="auth" id="auth">?</div>
<div class="info" id="banner"></div>
</div>
<div id="invnote"></div>
<div class="card queue">
<h2>Run what is left<button class="hlp" data-help="queues" type="button">?</button></h2>
<div class="actions">
<button class="btn btn-sm btn-primary" id="q-unattended" onclick='startBatch("unattended")'>Unattended</button>
<button class="btn btn-sm btn-primary" id="q-attended" onclick='startBatch("attended")'>Operator and live</button>
<button class="btn btn-sm btn-danger" id="q-stop" onclick="stopBatch()" disabled>Stop the queue</button>
</div>
<div id="qstate"></div>
<div id="qmsg"></div>
<div id="fixture" class="hint"></div>
</div>
<div class="actions" style="margin-top: 10px">
<button class="btn btn-sm btn-primary" id="exportbtn" onclick="doExport()">Export release artifact</button>
<a class="btn btn-sm btn-outline-secondary" id="dljson" href="/export/acceptance.json" style="display: none">acceptance.json</a>
<a class="btn btn-sm btn-outline-secondary" id="dlmd" href="/export/acceptance.md" style="display: none">acceptance.md</a>
<a class="btn btn-sm btn-outline-secondary" href="/log">Raw log</a>
<a class="btn btn-sm btn-outline-secondary" href="/journal">Runner journal</a>
<button class="btn btn-sm btn-outline-secondary" onclick="toggleInv()">Invalidate all&hellip;</button>
<button class="btn btn-sm btn-outline-secondary" onclick="doReset()">Reset campaign</button>
<button class="hlp" data-help="actions" type="button">?</button>
</div>
<div id="invform" style="display: none">
<input type="text" class="form-control form-control-sm" id="invreason" placeholder="reason (required): what changed on the bench" />
<button class="btn btn-sm btn-danger" onclick="doInvalidate()">Invalidate all results</button>
</div>
<div id="actmsg"></div>
<div class="switch">
<input class="form-check-input" type="checkbox" id="ignreq" onchange="setIgnoreReq(this.checked)" />
<label for="ignreq">Ignore prerequisites: start any test on its own, whatever its <i>requires</i> list says</label>
<span id="ignreqon"></span>
<button class="hlp" data-help="ignore" type="button">?</button>
</div>
</div>
<div id="groups"></div>
</div>
<div id="pane-bench" style="display: none">
<div class="card">
<h2>Bench diagnostics<button class="hlp" data-help="bench" type="button">?</button></h2>
<p class="hint">
The bench tools (scripts/bench), run on the board with their output in the Run
pane. Runs here never enter a campaign; live tools need the operator
acknowledgment, and takeover tools stop forgectrl for the duration.
</p>
<div id="benchmsg"></div>
</div>
<div id="tools"></div>
</div>
</div>
<div id="right">
<div class="card">
<h2 id="runtitle">Run</h2>
<div id="runhead" class="info">idle</div>
<div id="steps" style="display: none"></div>
<div id="notice" style="display: none"></div>
<div id="prompt" style="display: none">
<div class="q" id="promptq"></div>
<div class="actions" id="promptb"></div>
</div>
<pre id="log"></pre>
<div class="actions">
<button class="btn btn-sm btn-danger" id="abortbtn" onclick="doAbort()" disabled>Abort</button
><span class="hint" id="runfoot"></span>
</div>
</div>
</div>
</main>
<script src="vendor/bootstrap.bundle.min.js"></script>
<script src="help.js"></script>
<script src="app.js"></script>
</body>
</html>