Files
skills/orchestrator/templates/page-console.js
T
Kevin Veen-BirkenbachandClaude Opus 5.5 c035373518 feat(orchestrator)!: S0 English everywhere, Sécurité, NATO hashes, ICAO digits
Requirement 001 phase 6, S0:

- Speech: the security class shows as "Sécurité" and is spoken as in
  maritime radio, "say-cure-ee-tay" three times. A hex run of 7 or more
  characters with a letter and a digit (commit, session, message ids) is
  spelled in the NATO alphabet; every other digit is spoken on its own
  in ICAO pronunciation (42305: four two tree zero fife). The spelling
  runs inside clip() before the length cut, so a phrase still fits
  SPEECH_LIMIT; the displayed text keeps its digits and hashes.
- English: every orchestrator label, tooltip, message, error, script
  output, document and test string is English (Regeln -> Firewall,
  Hierarchie -> Hierarchy, Steuerung -> Controls, Ausführen -> Run, ...);
  the page declares lang="en" and formats dates as en-GB. Task filter
  ids are English. German stays only in the skill trigger phrases that
  quote the operator, and as identifiers: the view keys (view=) and the
  leitstand id.
- tests/test_english.py scans every tracked text file for umlauts and
  German words, with an allowlist of the quoted trigger phrases.

BREAKING CHANGE: the Stream search tokens art: and seit: are now kind:
and since:; an old art: or seit: token counts as a plain word.

Evidence: make test 657 OK, ruff and markdownlint clean
(/tmp/s0-make-test-1.log); e2e 17 OK, none skipped (/tmp/s0-e2e-1.log);
MEDUSA 86 files, 24 findings on reworded strings accepted after an
independent skeptic (21 path:line, 0 rejected, no behaviour change in
signing, approving, rules, denying, offices, events), rescan 0 open.

🎯 Fix: 93 %
💥 Break: 4 %
💸 Cost: 100 %
⏱️ Time: –
🛡️ Security: 100 %
🏆 Score: 89
🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 %

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 17:26:51 +00:00

100 lines
3.2 KiB
JavaScript

const consoles = new Map();
const FOLLOW_SLACK = 12;
const CONSOLE_POLL = 2000;
const jobOpen = (job) => job.status === "queued" || job.status === "running";
const withoutOutput = (item) => (item.job ? {...item, job: {...item.job, output: null}} : item);
const consoleSizes = new ResizeObserver((entries) => {
for (const entry of entries) {
const view = consoles.get(entry.target.dataset.job);
if (view && view.follow) {
view.out.scrollTop = view.out.scrollHeight;
}
}
});
function lasting(seconds) {
const total = Math.max(0, Math.round(seconds));
if (total < 60) {
return `${total} s`;
}
if (total < 3600) {
return `${Math.floor(total / 60)} min ${total % 60} s`;
}
return `${Math.floor(total / 3600)} h ${Math.floor((total % 3600) / 60)} min`;
}
function consoleWords(job) {
if (job.status === "queued") {
return `waiting for the runner for ${lasting(nowSeconds() - job.created_ts)}`;
}
if (job.status === "running") {
return `running for ${lasting(nowSeconds() - job.started)}`;
}
if (job.status === "refused") {
return "the runner did not accept the signature";
}
return `Exit code ${job.code} after ${lasting(job.finished - job.started)}`;
}
function paintConsole(view, job) {
const text = job.output || (jobOpen(job) ? "No output yet." : "No output.");
if (view.out.textContent !== text) {
view.out.textContent = text;
}
view.box.dataset.status = job.status;
view.line.replaceChildren(status(job.status), h("span", {}, consoleWords(job)));
}
function consoleOf(job) {
let view = consoles.get(job.id);
if (!view) {
const out = h("pre", {class: "console-out", tabindex: "0", "aria-label": "Command output", "data-job": job.id});
const line = h("div", {class: "console-status"});
view = {box: h("div", {class: "console", "data-job": job.id}, line, out), out, line, follow: true, top: 0};
out.addEventListener("scroll", () => {
view.top = out.scrollTop;
view.follow = out.scrollHeight - out.scrollTop - out.clientHeight <= FOLLOW_SLACK;
});
consoles.set(job.id, view);
consoleSizes.observe(out);
}
paintConsole(view, job);
return view.box;
}
function settleConsoles(items) {
const shown = new Set(items.filter((item) => item.job).map((item) => item.job.id));
for (const [id, view] of consoles) {
if (!shown.has(id)) {
consoleSizes.unobserve(view.out);
consoles.delete(id);
} else if (view.box.isConnected) {
view.out.scrollTop = view.follow ? view.out.scrollHeight : view.top;
}
}
}
function repaintConsoles(items) {
for (const item of items) {
const view = item.job ? consoles.get(item.job.id) : null;
if (view) {
paintConsole(view, item.job);
}
}
settleConsoles(items);
}
async function dismissJob(job) {
const result = await sendJson("/api/inbox/dismiss", {id: job.id});
if (!result.ok) {
toast("Cannot close", fail(result), false);
}
await load();
}
window.setInterval(() => {
if (ui.screen === "inbox" && state.inbox.some((item) => item.held || (item.job && jobOpen(item.job)))) {
refresh();
}
}, CONSOLE_POLL);