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>
41 lines
2.6 KiB
JavaScript
41 lines
2.6 KiB
JavaScript
const DENY_CHANNELS = [["post", true, "when its turn ends"], ["call", true, "at once, wakes it"], ["newsletter", false, "when their turn ends"], ["broadcast", false, "at once"], ["pan-pan", false, "at once, urgent"], ["security", false, "at once, as a safety notice"]];
|
|
const DENY_REASON = 2000;
|
|
const requesterOf = (item) => item.to || "nobody (recipient unclear)";
|
|
|
|
function denyNow(item, channel, reason) {
|
|
return submit([{...entryOf(item, "reject", ""), channel, reason, to: item.to}], `Denial (${CLASSES[channel]}): ${short(item.title)}`);
|
|
}
|
|
|
|
function denySheet(item) {
|
|
const name = `deny-${keyOf(item)}`;
|
|
const choices = DENY_CHANNELS.map(([id, direct, hint]) => h("label", {class: "deny-channel"},
|
|
h("input", {type: "radio", name, value: id, checked: id === "post"}),
|
|
h("span", {}, h("strong", {}, CLASSES[id]), h("small", {}, `to ${direct ? requesterOf(item) : "all"}, ${hint}`))));
|
|
const reason = h("textarea", {class: "field", maxlength: DENY_REASON, rows: 3, placeholder: "e.g. make the gate green first"});
|
|
const confirm = h("button", {type: "button", class: "btn danger"}, icon("bi-x-lg"), "Deny");
|
|
confirm.addEventListener("click", () => {
|
|
const picked = choices.map((label) => label.querySelector("input")).find((input) => input.checked);
|
|
closeSheet();
|
|
denyNow(item, picked.value, reason.value);
|
|
});
|
|
openSheet("Deny", [
|
|
h("div", {class: "who"}, pill(ITEM_LABELS[item.kind], KIND_TONES[item.kind]), item.agent ? person(item.agent) : null),
|
|
h("p", {class: "text"}, item.title),
|
|
h("fieldset", {class: "deny-channels"}, h("legend", {}, "Channel"), choices),
|
|
h("label", {class: "deny-reason"}, "Reason (optional, the agent reads it too)", reason),
|
|
h("p", {class: "muted"}, "The passkey signs the denial with channel, recipient and reason; then 3 s remain to undo it.",
|
|
" Mayday stays reserved for the operator."),
|
|
h("div", {class: "actions"}, h("span", {class: "spacer"}), btn("Cancel", "btn ghost", closeSheet), confirm)], false);
|
|
reason.focus();
|
|
}
|
|
|
|
function denySplit(item) {
|
|
const main = iconBtn("bi-x-lg", "Deny", "btn small danger", () => denyNow(item, "post", ""), `Deny: by post to ${requesterOf(item)}, 3 s to undo`);
|
|
const more = h("button", {type: "button", class: "btn small danger icon-only", "aria-haspopup": "dialog", ...named("Deny via …", "Deny via another channel or with a reason")}, caret());
|
|
more.addEventListener("click", (event) => {
|
|
event.stopPropagation();
|
|
denySheet(item);
|
|
});
|
|
return h("div", {class: "deny-split", role: "group", "aria-label": "Deny"}, main, more);
|
|
}
|