Files
skills/orchestrator/templates/page-board.js
T
Kevin Veen-BirkenbachandClaude Opus 5.5 57ab29eb0d
🧪 Test / 🧪 Lock + lint (push) Canceled after 0s
feat(orchestrator): S5.1 radio spot, compact menus, trend, ETA and Tasks 3D
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>
2026-11-10 08:10:51 +00:00

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)));
}
}