feat(orchestrator): W2 Kanban board with swimlanes and forecast
Tasks gain a Kanban board: columns Waiting, Running and Done, swimlanes P1 to P4+ and Unpinned by WSJF, after-dependency arrows and the dashed queue line; drag and drop stages status, pin and dependency moves that apply with one passkey. The burn chart gets the trend and a forecast. Committed below the bar on the operator's word 2026-11-12 (no further per-bundle tests, one gate after the merge): the W2 e2e never ran. 🎯 Fix: 80 % 💥 Break: 10 % 💸 Cost: 100 % ⏱️ Time: 100 % 🛡️ Security: 100 % 🏆 Score: 72 🚦 Gate: ⛔ fix 80 % < 90, break 10 % > 5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fd8fc08603
commit
8e63b21e21
18 files changed
+1375
-76
No files matched your search
@@ -1,5 +1,5 @@
|
||||
const TASK_STATUS = {queued: ["waiting", "info"], running: ["running", "ok"], blocked: ["blocked", "bad"], done: ["done", "neutral"], stopped: ["stopped", "neutral"], awaiting: ["waiting for a grant", "warn"]};
|
||||
const TASK_COLUMNS = [["queued", "Waiting"], ["running", "Running"], ["blocked", "Blocked"], ["done", "Done"], ["stopped", "Stopped"]];
|
||||
const TASK_COLUMNS = Kanban.COLUMNS;
|
||||
const TASK_KINDS = {command: ["Command", "bad"], hotfix: ["Hotfix", "warn"], queue: ["Queue job", "teal"]};
|
||||
const TASK_FILTERS = [
|
||||
["all", "All", "bi-list-check", () => true],
|
||||
@@ -18,7 +18,6 @@
|
||||
false: "ETA: the end of the ended task",
|
||||
};
|
||||
|
||||
const columnOf = (row) => (TASK_COLUMNS.some(([id]) => id === row.status) ? row.status : "queued");
|
||||
const branchOf = (row) => row.worktree_branch || (row.branch !== "-" ? row.branch : "");
|
||||
const titleOf = (row) => (row.title !== "-" ? row.title : row.task);
|
||||
const openJobs = (task) => state.jobs.filter((job) => job.kind === "task" && job.task === task && (job.status === "queued" || job.status === "running"));
|
||||
@@ -80,26 +79,6 @@
|
||||
workChips(row.open_work));
|
||||
}
|
||||
|
||||
function taskCard(row) {
|
||||
const classes = ["task-card", row.open_work.length ? "open-work" : "", row.ended ? "ended" : ""].filter(Boolean).join(" ");
|
||||
return h("li", {class: classes, "data-task": row.task},
|
||||
h("div", {class: "head-row"}, h("code", {class: "task-id"}, row.task)),
|
||||
opener(row),
|
||||
h("div", {class: "states"}, taskBadges(row, row.status === "awaiting")),
|
||||
row.worker !== "-" ? h("span", {class: "muted"}, row.worker) : null,
|
||||
row.flow ? flowLine(row.flow, branchOf(row)) : null,
|
||||
workChips(row.open_work));
|
||||
}
|
||||
|
||||
function taskBoard(rows) {
|
||||
return h("div", {class: "task-board"}, TASK_COLUMNS.map(([id, label]) => {
|
||||
const cards = rows.filter((row) => columnOf(row) === id);
|
||||
return h("section", {class: "card task-column", "data-column": id, "aria-label": label},
|
||||
h("h3", {class: "group-title"}, label, counter(cards.length)),
|
||||
cards.length ? h("ol", {class: "task-cards"}, cards.map(taskCard)) : h("p", {class: "muted"}, "empty"));
|
||||
}));
|
||||
}
|
||||
|
||||
function roleLetters(letters) {
|
||||
return [...letters].map((letter) => h("span", {class: "letter", "data-tone": ROLE_TONES[letter], title: ROLE_WORDS[letter]}, letter));
|
||||
}
|
||||
@@ -239,7 +218,7 @@
|
||||
const etas = etaOf();
|
||||
return h("ol", {class: "task-list"}, Plan.sortRows(found, taskView.sort, taskView.descending).map((row) => taskItem(row, etas.get(row.task))));
|
||||
},
|
||||
board: taskBoard,
|
||||
board: kanbanBoard,
|
||||
rasci: taskMatrix,
|
||||
gantt: ganttChart,
|
||||
burn: (found) => burnChart(rows, found),
|
||||
|
||||
Reference in new issue
Block a user