Files
skills/orchestrator/templates/page-flow.js
T
Kevin Veen-BirkenbachandClaude Opus 5.5 c1310c3322 feat(orchestrator): tasks tab with register, inbox and queue
Add a Tasks tab that lists the kvb-org-rasci register next to the inbox
and the heavy-job queue, in list, board and RASCI matrix views, each
task with its roles, WSJF, open work and the merge flow of its worktree.

- Task priority pins and WSJF estimates are signed with the operator's
  passkey and applied by the runner through rasci.py; the hub verifies
  the signature itself before it queues a job or logs the change, the
  runner verifies it again and runs only the checked argv, from /, with
  a pinned PATH and a 30 s process-group limit.
- The runner refuses task changes into a council that is a symlink, not
  owned by its uid or given by a relative path, and while the installed
  council.py, nofollow.py or rasci.py differ from the tree.
- Only tasks the hub can sign whole are editable (task slug and length).
- Page lookups keyed by register text use Object.hasOwn, so a status or
  party named like an Object.prototype member cannot break the views.
- The inbox screen renders through the tasks screen, so its task filter
  shows with "Wartet auf Operator" pressed.

Tests: 604 unit tests OK, e2e 15/15 OK (Chromium and Firefox).

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 23:43:36 +00:00

69 lines
3.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const WORK_CHIPS = {dirty: ["bi-pencil-square", "nicht committet", "warn"], unpushed: ["bi-cloud-upload", "ungepusht", "info"], unmerged: ["bi-signpost-split", "nicht gemergt", "neutral"], task: ["bi-card-checklist", "Aufgabe offen", "violet"]};
const FLOW_SIDES = {pull: "zieht vor dem Gate", push: "mergt fertig in"};
function workChips(reasons) {
if (!reasons || !reasons.length) {
return null;
}
return h("span", {class: "states work", role: "group", "aria-label": "Offene Arbeit"}, reasons.map((reason) => {
const [name, label, tone] = Object.hasOwn(WORK_CHIPS, reason) ? WORK_CHIPS[reason] : ["bi-question-circle", reason, "neutral"];
return h("span", {class: "state", "data-tone": tone, title: `offene Arbeit: ${label}`}, icon(name), label);
}));
}
function flowLine(flow, branch) {
if (!flow) {
return null;
}
if (flow.source === "invalid") {
return h("div", {class: "flow invalid", "data-flow": "invalid"}, icon("bi-exclamation-triangle"), "Merge-Flow ungültig: pkgmgr kann die Flow-Datei nicht lesen");
}
const refs = (side) => (flow[side].length ? flow[side].map((ref) => h("code", {class: "flow-ref", title: `${FLOW_SIDES[side]}: ${ref}`}, ref)) : h("span", {class: "muted"}, "–"));
const arrow = () => h("span", {class: "flow-arrow", ...named("fließt nach")}, icon("bi-arrow-right"));
return h("div", {class: "flow", "data-flow": flow.source, role: "group", "aria-label": `Merge-Flow: ${flow.pull.join(", ") || "keine Quelle"} nach ${branch || "–"} nach ${flow.push.join(", ") || "kein Ziel"}`},
h("span", {class: "flow-label"}, icon("bi-sign-merge-right"), "Flow"),
h("span", {class: "flow-side"}, refs("pull")), arrow(),
h("code", {class: "flow-ref here", title: "dieser Branch"}, branch || "–"), arrow(),
h("span", {class: "flow-side"}, refs("push")));
}
function flowDetails(flow, branch) {
if (!flow) {
return h("p", {class: "muted"}, "Kein Merge-Flow gesetzt.");
}
if (flow.source === "invalid") {
return flowLine(flow, branch);
}
return [h("h3", {}, "Merge-Flow"), flowLine(flow, branch), meta([["Zieht vor dem Gate", flow.pull.join(", ")], ["Mergt fertig in", flow.push.join(", ")], ["Quelle", flow.source]])];
}
function flowTarget(names, ref) {
if (names.has(ref)) {
return ref;
}
const bare = ref.includes("/") ? ref.slice(ref.indexOf("/") + 1) : "";
return names.has(bare) ? bare : null;
}
function flowEdges(repo) {
const names = new Set(repo.branches.map((entry) => entry.name));
const edges = [];
for (const branch of repo.branches) {
for (const flow of branch.worktrees.map((wt) => wt.flow).filter((found) => found && found.source !== "invalid")) {
for (const ref of flow.pull) {
const from = flowTarget(names, ref);
if (from && from !== branch.name) {
edges.push({from, into: branch.name, side: "pull", ref});
}
}
for (const ref of flow.push) {
const into = flowTarget(names, ref);
if (into && into !== branch.name) {
edges.push({from: branch.name, into, side: "push", ref});
}
}
}
}
return edges;
}