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>
81 lines
3.5 KiB
JavaScript
81 lines
3.5 KiB
JavaScript
async function act(action, label, payload) {
|
|
ui.busy = true;
|
|
const result = await sendJson(`/api/board/${action}`, payload);
|
|
ui.busy = false;
|
|
if (result.data && result.data.runs) {
|
|
toast(`${label}: ${result.data.ok ? "done" : "failed"}`, runLines(result.data), result.data.ok);
|
|
} else {
|
|
toast(`${label}: failed`, [`${result.status} ${result.text}`.trim()], false);
|
|
}
|
|
await load();
|
|
return result.ok && result.data && result.data.ok;
|
|
}
|
|
|
|
async function confirmAct(action) {
|
|
const [, label, question] = ACTIONS.find(([id]) => id === action);
|
|
if (!window.confirm(question)) {
|
|
return;
|
|
}
|
|
const signed = await signedProof("/api/board/sign", {action}, label);
|
|
if (signed) {
|
|
await act(action, label, {proof: signed.proof});
|
|
}
|
|
}
|
|
|
|
function choice(name, options, picked) {
|
|
return h("div", {class: "classes", role: "radiogroup", "aria-label": "Class"}, options.map(([id, label]) => h("label", {class: "check"},
|
|
h("input", {type: "radio", name, value: id, checked: id === picked}), label)));
|
|
}
|
|
|
|
const picked = (group) => group.querySelector("input:checked").value;
|
|
|
|
function toAll() {
|
|
const text = h("textarea", {"aria-label": "Radio to all", placeholder: "Message to all agents …", on: {input: (event) => {
|
|
ui.draft = event.target.value;
|
|
}}});
|
|
text.value = ui.draft;
|
|
const classes = choice("to-all", TO_ALL, "newsletter");
|
|
const send = async () => {
|
|
const cls = picked(classes);
|
|
if (!ui.draft.trim() || (cls === "mayday" && !confirmMayday())) {
|
|
return;
|
|
}
|
|
if (await radioSend({to: "*", text: ui.draft, upload: "", cls, voice: ""})) {
|
|
ui.draft = "";
|
|
text.value = "";
|
|
}
|
|
};
|
|
return h("details", {class: "menu", id: "to-all"}, h("summary", named("Radio to all"), icon("bi-broadcast-pin"), caret()), h("div", {class: "menu-box"},
|
|
text, classes, h("p", {class: "muted"}, "Newsletter arrives as post at the end of the turn; Broadcast, Pan-Pan, Security and Mayday at once. Your passkey signs every message."),
|
|
h("div", {class: "actions"}, h("span", {class: "spacer"}), iconBtn("bi-send", "Send", "btn soft", send, "Send to all"))));
|
|
}
|
|
|
|
function openToAll() {
|
|
go("agents", null);
|
|
window.setTimeout(() => {
|
|
const menu = document.getElementById("to-all");
|
|
if (menu) {
|
|
menu.open = true;
|
|
menu.querySelector("textarea").focus();
|
|
}
|
|
}, 50);
|
|
}
|
|
|
|
function agentsToolbar() {
|
|
const clear = iconBtn("bi-life-preserver", "Cancel mayday", "btn small danger", () => confirmAct("clear-mayday"));
|
|
const mode = h("span", {});
|
|
const night = h("details", {class: "menu"}, h("summary", named("Day/Night"), icon("bi-moon-stars"), caret()), h("div", {class: "menu-box"}, mode,
|
|
h("p", {class: "muted"}, "Evening: agents finish their task. Sunset: they sleep. Sunrise: all wake up."),
|
|
h("div", {class: "actions"}, ACTIONS.slice(1).map(([id, label]) => btn(label, "btn small", () => confirmAct(id))))));
|
|
return {bar: h("div", {class: "toolbar", role: "toolbar", "aria-label": "Controls"}, toAll(), clear, iconBtn("bi-sort-down", "Priority", "btn small", openAgentOrder, "Priority: agent order in the hierarchy"), night), clear, mode};
|
|
}
|
|
|
|
function drawToolbar() {
|
|
if (!graphDom || !graphDom.toolbar) {
|
|
return;
|
|
}
|
|
const [mode, tone] = NIGHT[state.night] || [state.night, "neutral"];
|
|
graphDom.toolbar.clear.disabled = !state.silence;
|
|
graphDom.toolbar.mode.replaceChildren(pill(mode, tone));
|
|
}
|