🧪 Test / 🧪 Lock + lint (push) Canceled after 0s
Requirement 001 decisions 52 to 58 (operator 2026-11-09): - Radio spot: the Agents toolbar radio button expands inline with the one composer (every channel, post and call recipients, Record, Send), a gear with the radio settings (tones, squelch, read aloud, moved out of the general settings) and Cancel mayday, which leaves the toolbar; the Radio tab keeps speakers, stations and the log plus a jump to the spot. - Day/Night menu: the badge and Drain, Suspend and Resume only; each explanation is the button's tooltip. - Tasks toolbar: views, filter, estimation, sort and business value in one wrapping row with a Request choices dropdown; the status chips stay the second row. - Burndown and burnup: a trend selector (off, linear regression, velocity over N days) stored in the hub settings, the trend lines and the forecast date on the chart, and a tooltip per point with the work packages that started, ended or joined there. - ETA: every list row shows its ETA (end when done, else start or now plus the forecast length behind its predecessors), sharing one predecessor walk with the Gantt. - Tasks 3D: a sixth view on the 3D engine with the dependency edges in the Gantt colours, layouts Force and Time. - Steady 3D camera: the view fits once per graph build, refreshes keep the operator's camera. - e2e: Firefox runs with its profile as cwd, so its crashreporter no longer drops core dumps into the repository. - A forecast beyond the last second a Date can hold is no forecast, so a nearly flat regression no longer aborts the Tasks render. Evidence: make test Ran 717 OK, ruff clean, markdownlint 0 errors; e2e 30 of 30 OK in Chromium and Firefox at 1920, 1366 and 390 px; MEDUSA 0 findings after one skeptic-accepted lexical EXA006. 🎯 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>
106 lines
4.7 KiB
JavaScript
106 lines
4.7 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;
|
|
}
|
|
|
|
const actionOf = (action) => ACTIONS.find(([id]) => id === action);
|
|
|
|
async function confirmAct(action) {
|
|
const [, label, question] = actionOf(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 actionButton(action, cls, name) {
|
|
const [, label, , hint] = actionOf(action);
|
|
return h("button", {type: "button", class: cls, ...named(label, hint), on: {click: () => confirmAct(action)}}, name ? icon(name) : null, label);
|
|
}
|
|
|
|
function openSpot(spot, open) {
|
|
ui.spot = open;
|
|
spot.panel.hidden = !open;
|
|
spot.toggle.setAttribute("aria-expanded", String(open));
|
|
window.requestAnimationFrame(refresh3d);
|
|
if (open) {
|
|
spot.text.focus();
|
|
}
|
|
}
|
|
|
|
function toggleGear(spot) {
|
|
const open = spot.gearBox.hidden;
|
|
spot.gearBox.replaceChildren(...(open ? [radioSettings(() => toggleGear(spot))] : []));
|
|
spot.gearBox.hidden = !open;
|
|
spot.gear.setAttribute("aria-expanded", String(open));
|
|
window.requestAnimationFrame(refresh3d);
|
|
}
|
|
|
|
function radioSpot() {
|
|
const {form, recipient, text} = composerForm();
|
|
const gearBox = h("div", {class: "radio-gear", id: "radio-gear", hidden: true});
|
|
const gear = h("button", {type: "button", class: "btn small ghost icon-only spot-toggle", "aria-expanded": "false", "aria-controls": "radio-gear", ...named("Radio settings", "Radio settings: tones, squelch and read aloud")}, icon("bi-gear"));
|
|
const clear = actionButton("clear-mayday", "btn small danger", "bi-life-preserver");
|
|
const panel = h("section", {class: "card radio-spot", id: "radio-spot", "aria-label": "Radio", hidden: !ui.spot},
|
|
h("div", {class: "spot-head"}, h("h3", {class: "group-title"}, "Radio"), h("span", {class: "spacer"}), gear, clear), h("div", {class: "spot-body"}, form, gearBox));
|
|
const toggle = h("button", {type: "button", class: "btn small spot-toggle", "aria-expanded": String(ui.spot), "aria-controls": "radio-spot", ...named("Radio", "Radio: post, call or a message to all, with Record and the radio settings")}, icon("bi-broadcast-pin"), caret());
|
|
const spot = {panel, toggle, gear, gearBox, text};
|
|
toggle.addEventListener("click", () => openSpot(spot, !ui.spot));
|
|
gear.addEventListener("click", () => toggleGear(spot));
|
|
return {toggle, panel, clear, recipient};
|
|
}
|
|
|
|
function openRadio() {
|
|
ui.spot = true;
|
|
go("agents", null);
|
|
window.setTimeout(() => {
|
|
const field = document.querySelector("#radio-spot textarea");
|
|
if (field) {
|
|
field.focus();
|
|
}
|
|
}, 50);
|
|
}
|
|
|
|
function agentsToolbar() {
|
|
const spot = radioSpot();
|
|
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("div", {class: "actions"}, ACTIONS.slice(1).map(([id]) => actionButton(id, "btn small")))));
|
|
const bar = h("div", {class: "toolbar", role: "toolbar", "aria-label": "Controls"}, spot.toggle, iconBtn("bi-sort-down", "Priority", "btn small", openAgentOrder, "Priority: agent order in the hierarchy"), night);
|
|
return {bar, spot: spot.panel, clear: spot.clear, recipient: spot.recipient, mode, names: ""};
|
|
}
|
|
|
|
function drawToolbar() {
|
|
const toolbar = graphDom && graphDom.toolbar;
|
|
if (!toolbar) {
|
|
return;
|
|
}
|
|
const [mode, tone] = NIGHT[state.night] || [state.night, "neutral"];
|
|
toolbar.clear.disabled = !state.silence;
|
|
toolbar.mode.replaceChildren(pill(mode, tone));
|
|
const names = agentNames();
|
|
if (JSON.stringify(names) !== toolbar.names) {
|
|
toolbar.names = JSON.stringify(names);
|
|
const chosen = toolbar.recipient.value;
|
|
toolbar.recipient.replaceChildren(...names.map((name) => h("option", {value: name, selected: name === chosen}, name)));
|
|
}
|
|
}
|