planr 1.10.0-alpha.9 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +11 -0
- package/CHANGELOG.md +20 -0
- package/README.md +27 -245
- package/docs/board.md +44 -0
- package/docs/cli.md +100 -0
- package/docs/concepts.md +133 -0
- package/docs/contract.md +150 -0
- package/docs/plugins/bb.md +49 -0
- package/docs/plugins/claude-code.md +84 -0
- package/docs/plugins/codex.md +77 -0
- package/docs/quickstart.md +307 -0
- package/docs/setup.md +103 -0
- package/package.json +21 -68
- package/plugin.json +12 -0
- package/skills/planr/SKILL.md +64 -0
- package/skills/planr/assets/board/fonts/LICENSE-mona-sans.txt +93 -0
- package/skills/planr/assets/board/fonts/LICENSE-monaspace.txt +93 -0
- package/skills/planr/assets/board/fonts/MonaSans-Variable.woff2 +0 -0
- package/skills/planr/assets/board/fonts/MonaspaceNeon-400.woff2 +0 -0
- package/skills/planr/assets/board/fonts/MonaspaceNeon-500.woff2 +0 -0
- package/skills/planr/assets/board/fonts/MonaspaceNeon-600.woff2 +0 -0
- package/skills/planr/assets/board/template.html +788 -0
- package/skills/planr/assets/templates/goal.md +13 -0
- package/skills/planr/assets/templates/hook.sh +4 -0
- package/skills/planr/assets/templates/in-repo.config.json +6 -0
- package/skills/planr/assets/templates/planr.config.json +6 -0
- package/skills/planr/assets/templates/tasks-README.md +14 -0
- package/skills/planr/references/format.md +88 -0
- package/skills/planr/references/work-a-goal.md +62 -0
- package/skills/planr/scripts/lib/board.mjs +39 -0
- package/skills/planr/scripts/lib/graph.mjs +345 -0
- package/skills/planr/scripts/lib/runs.mjs +127 -0
- package/skills/planr/scripts/lib/version.mjs +11 -0
- package/skills/planr/scripts/planr.mjs +386 -0
- package/docs/ARCHITECTURE.md +0 -136
- package/docs/RELEASE.md +0 -275
- package/docs/SWITCHLOOM_COMPATIBILITY.md +0 -14
- package/docs/contracts/BROWSER_HARNESS_ADAPTER_V1.md +0 -75
- package/docs/contracts/EVAL_CONTRACT_V1.md +0 -2072
- package/docs/contracts/EVIDENCE_ADAPTER_PROTOCOL_V1.md +0 -98
- package/docs/contracts/EVIDENCE_CONTRACT_V1.md +0 -382
- package/docs/contracts/GROK_INTEGRATION_V1.md +0 -139
- package/docs/contracts/LINUX_RELEASE_PORTABILITY.json +0 -9
- package/docs/contracts/PI_INTEGRATION_V1.md +0 -164
- package/docs/contracts/V1_1_DIFFERENTIATION_CONTRACT.md +0 -177
- package/docs/contracts/fixtures/evidence/v1/examples/coverage-verdict.json +0 -66
- package/docs/contracts/fixtures/evidence/v1/examples/evidence-attempt.json +0 -39
- package/docs/contracts/fixtures/evidence/v1/examples/evidence-policy.json +0 -108
- package/docs/contracts/fixtures/evidence/v1/examples/evidence-receipt.json +0 -93
- package/docs/contracts/fixtures/evidence/v1/examples/evidence-waiver.json +0 -31
- package/docs/contracts/fixtures/evidence/v1/examples/proof-obligation.json +0 -55
- package/docs/contracts/fixtures/evidence/v1/examples/proof-preset.json +0 -19
- package/docs/contracts/fixtures/evidence/v1/examples/untrusted-evidence-proposal.json +0 -20
- package/docs/contracts/fixtures/evidence/v1/examples/verification-capability-instance.json +0 -39
- package/docs/contracts/fixtures/evidence/v1/examples/verification-capability-manifest.json +0 -52
- package/docs/contracts/fixtures/evidence/v1/negative/aborted-zero-exit-attempt.json +0 -39
- package/docs/contracts/fixtures/evidence/v1/negative/adapter-registration-missing-execution-contract.json +0 -88
- package/docs/contracts/fixtures/evidence/v1/negative/available-empty-probe-capability.json +0 -27
- package/docs/contracts/fixtures/evidence/v1/negative/blocked-coverage-with-stale-reason.json +0 -27
- package/docs/contracts/fixtures/evidence/v1/negative/empty-trust-bindings-receipt.json +0 -72
- package/docs/contracts/fixtures/evidence/v1/negative/failed-zero-exit-attempt.json +0 -39
- package/docs/contracts/fixtures/evidence/v1/negative/forged-trusted-receipt.json +0 -35
- package/docs/contracts/fixtures/evidence/v1/negative/inconclusive-clean-exit-attempt.json +0 -39
- package/docs/contracts/fixtures/evidence/v1/negative/inconclusive-coverage-with-blocked-reason.json +0 -27
- package/docs/contracts/fixtures/evidence/v1/negative/minimal-policy-missing-owned-inputs.json +0 -5
- package/docs/contracts/fixtures/evidence/v1/negative/mismatched-probe-execution-id-capability.json +0 -42
- package/docs/contracts/fixtures/evidence/v1/negative/passed-empty-attempt.json +0 -22
- package/docs/contracts/fixtures/evidence/v1/negative/policy-default-preset-unregistered.json +0 -102
- package/docs/contracts/fixtures/evidence/v1/negative/policy-weakening-without-waiver.json +0 -102
- package/docs/contracts/fixtures/evidence/v1/negative/process-probe-missing-output-limits.json +0 -49
- package/docs/contracts/fixtures/evidence/v1/negative/satisfied-empty-coverage-verdict.json +0 -14
- package/docs/contracts/fixtures/evidence/v1/negative/skipped-zero-exit-attempt.json +0 -39
- package/docs/contracts/fixtures/evidence/v1/negative/stale-coverage-with-inconclusive-reason.json +0 -27
- package/docs/contracts/fixtures/evidence/v1/negative/timed-out-zero-exit-attempt.json +0 -39
- package/docs/contracts/fixtures/evidence/v1/negative/unavailable-zero-exit-attempt.json +0 -39
- package/docs/contracts/fixtures/evidence/v1/negative/unbound-waiver.json +0 -21
- package/docs/contracts/fixtures/evidence/v1/negative/unsatisfied-coverage-without-gap.json +0 -63
- package/docs/contracts/fixtures/evidence/v1/negative/waived-coverage-forbidden-gap-reason.json +0 -28
- package/docs/contracts/fixtures/evidence/v1/negative/waived-coverage-missing-waiver-id.json +0 -27
- package/docs/contracts/fixtures/evidence/v1/schemas/evidence-contract-v1.schema.json +0 -2685
- package/docs/contracts/schemas/com.planr.web.dom_state.v1.schema.json +0 -30
- package/docs/documentation/CONTRACT.md +0 -164
- package/docs/documentation/COVERAGE.md +0 -98
- package/docs/documentation/INFORMATION_ARCHITECTURE.md +0 -151
- package/docs/fixtures/mcp-contract.json +0 -153
- package/npm/bin/native-launcher.js +0 -67
- package/npm/bin/planr-browser-harness-adapter.js +0 -8
- package/npm/bin/planr.js +0 -8
- package/npm/native/darwin-arm64/planr +0 -0
- package/npm/native/darwin-arm64/planr-browser-harness-adapter +0 -0
- package/npm/native/darwin-arm64/planr-host-capability-validator +0 -0
- package/npm/native/darwin-x86_64/planr +0 -0
- package/npm/native/darwin-x86_64/planr-browser-harness-adapter +0 -0
- package/npm/native/darwin-x86_64/planr-host-capability-validator +0 -0
- package/npm/native/linux-arm64/planr +0 -0
- package/npm/native/linux-arm64/planr-browser-harness-adapter +0 -0
- package/npm/native/linux-arm64/planr-host-capability-validator +0 -0
- package/npm/native/linux-x86_64/planr +0 -0
- package/npm/native/linux-x86_64/planr-browser-harness-adapter +0 -0
- package/npm/native/linux-x86_64/planr-host-capability-validator +0 -0
- package/plugins/planr/.claude-plugin/plugin.json +0 -11
- package/plugins/planr/.codex-plugin/plugin.json +0 -25
- package/plugins/planr/agents/pi/planr-reviewer.md +0 -20
- package/plugins/planr/agents/pi/planr-worker.md +0 -35
- package/plugins/planr/agents/planr-reviewer.md +0 -21
- package/plugins/planr/agents/planr-worker.md +0 -40
- package/plugins/planr/skills/planr/SKILL.md +0 -60
- package/plugins/planr/skills/planr-goal/SKILL.md +0 -52
- package/plugins/planr/skills/planr-loop/SKILL.md +0 -126
- package/plugins/planr/skills/planr-loop/agents/planr-reviewer.md +0 -21
- package/plugins/planr/skills/planr-loop/agents/planr-worker.md +0 -36
- package/plugins/planr/skills/planr-loop/references/host-dispatch.md +0 -52
- package/plugins/planr/skills/planr-loop/references/recovery-and-verification.md +0 -26
- package/plugins/planr/skills/planr-plan/SKILL.md +0 -86
- package/plugins/planr/skills/planr-review/SKILL.md +0 -55
- package/plugins/planr/skills/planr-status/SKILL.md +0 -52
- package/plugins/planr/skills/planr-summary/SKILL.md +0 -30
- package/plugins/planr/skills/planr-task-graph/SKILL.md +0 -67
- package/plugins/planr/skills/planr-verify-web/SKILL.md +0 -62
- package/plugins/planr/skills/planr-work/SKILL.md +0 -93
- package/scripts/host-capability-experiment.mjs +0 -2481
- package/scripts/host-capability-runtime/v1/schemas/host-capability-expected-manifest.schema.json +0 -377
- package/scripts/host-capability-runtime/v1/schemas/host-capability-observed-raw.schema.json +0 -439
- package/scripts/host-capability-runtime/v1/schemas/host-capability-provenance.schema.json +0 -178
- package/scripts/planr-host-capability-validator +0 -57
- /package/{LICENSE.md → LICENSE} +0 -0
|
@@ -0,0 +1,788 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Taskboard</title>
|
|
7
|
+
<style>
|
|
8
|
+
@font-face { font-family: "Mona Sans"; src: url(data:font/woff2;base64,/*FONT:MonaSans-Variable.woff2*/) format("woff2"); font-weight: 200 900; font-display: block; }
|
|
9
|
+
@font-face { font-family: "Monaspace Neon"; src: url(data:font/woff2;base64,/*FONT:MonaspaceNeon-400.woff2*/) format("woff2"); font-weight: 400; font-display: block; }
|
|
10
|
+
@font-face { font-family: "Monaspace Neon"; src: url(data:font/woff2;base64,/*FONT:MonaspaceNeon-500.woff2*/) format("woff2"); font-weight: 500; font-display: block; }
|
|
11
|
+
@font-face { font-family: "Monaspace Neon"; src: url(data:font/woff2;base64,/*FONT:MonaspaceNeon-600.woff2*/) format("woff2"); font-weight: 600 700; font-display: block; }
|
|
12
|
+
|
|
13
|
+
:root {
|
|
14
|
+
color-scheme: light;
|
|
15
|
+
--bg: #f4f5f7; --panel: #ffffff; --sunken: #eceef1; --hover: #f1f3f6;
|
|
16
|
+
--line: #e2e5e9; --line-2: #cdd2d8;
|
|
17
|
+
--fg: #0e1116; --fg-2: #3f4753; --fg-3: #535b67; --fg-4: #646c78;
|
|
18
|
+
--sel: #0e1116; --sel-fg: #ffffff; --sel-soft: #e6e9ee;
|
|
19
|
+
--focus: #0e1116;
|
|
20
|
+
--s-done: #1f9d55; --s-review: #3366e0; --s-wip: #8152e6; --s-open: #7f8691;
|
|
21
|
+
--s-blocked: #e0464c; --s-decision: #b57300; --s-dropped: #7f8691;
|
|
22
|
+
--mark-ink: #ffffff;
|
|
23
|
+
--sans: "Mona Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
24
|
+
--mono: "Monaspace Neon", ui-monospace, "SF Mono", Menlo, monospace;
|
|
25
|
+
--r: 8px;
|
|
26
|
+
}
|
|
27
|
+
:root[data-theme="dark"] {
|
|
28
|
+
color-scheme: dark;
|
|
29
|
+
--bg: #0a0c0f; --panel: #111419; --sunken: #0d1014; --hover: #171b21;
|
|
30
|
+
--line: #1f242b; --line-2: #2c333c;
|
|
31
|
+
--fg: #e8ebef; --fg-2: #b0b8c3; --fg-3: #8d95a1; --fg-4: #7a838f;
|
|
32
|
+
--sel: #e8ebef; --sel-fg: #0a0c0f; --sel-soft: #1c2129;
|
|
33
|
+
--focus: #e8ebef;
|
|
34
|
+
--s-done: #3cc06e; --s-review: #6e9bff; --s-wip: #a684ff; --s-open: #68717c;
|
|
35
|
+
--s-blocked: #ff6268; --s-decision: #f2b23e; --s-dropped: #6a727d;
|
|
36
|
+
--mark-ink: #0a0c0f;
|
|
37
|
+
}
|
|
38
|
+
@media (prefers-color-scheme: dark) {
|
|
39
|
+
:root:not([data-theme="light"]) {
|
|
40
|
+
color-scheme: dark;
|
|
41
|
+
--bg: #0a0c0f; --panel: #111419; --sunken: #0d1014; --hover: #171b21;
|
|
42
|
+
--line: #1f242b; --line-2: #2c333c;
|
|
43
|
+
--fg: #e8ebef; --fg-2: #b0b8c3; --fg-3: #8d95a1; --fg-4: #7a838f;
|
|
44
|
+
--sel: #e8ebef; --sel-fg: #0a0c0f; --sel-soft: #1c2129;
|
|
45
|
+
--focus: #e8ebef;
|
|
46
|
+
--s-done: #3cc06e; --s-review: #6e9bff; --s-wip: #a684ff; --s-open: #68717c;
|
|
47
|
+
--s-blocked: #ff6268; --s-decision: #f2b23e; --s-dropped: #6a727d;
|
|
48
|
+
--mark-ink: #0a0c0f;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
* { box-sizing: border-box; }
|
|
53
|
+
html, body { margin: 0; height: 100%; }
|
|
54
|
+
body {
|
|
55
|
+
background: var(--bg); color: var(--fg);
|
|
56
|
+
font: 13px/1.45 var(--sans); letter-spacing: -0.005em;
|
|
57
|
+
-webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility;
|
|
58
|
+
overflow: hidden;
|
|
59
|
+
}
|
|
60
|
+
::selection { background: var(--sel); color: var(--sel-fg); }
|
|
61
|
+
button, input { font: inherit; color: inherit; }
|
|
62
|
+
button { background: none; border: 0; padding: 0; cursor: pointer; }
|
|
63
|
+
:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: 4px; }
|
|
64
|
+
.muted { color: var(--fg-3); }
|
|
65
|
+
* { scrollbar-width: thin; scrollbar-color: var(--line-2) transparent; }
|
|
66
|
+
::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
67
|
+
::-webkit-scrollbar-thumb { background: var(--line-2); border-radius: 8px; border: 2px solid var(--panel); }
|
|
68
|
+
|
|
69
|
+
/* ---------- shell ---------- */
|
|
70
|
+
.app { height: 100vh; display: grid; grid-template-columns: minmax(0, 1fr); grid-template-rows: auto auto minmax(0, 1fr) auto; container-type: inline-size; }
|
|
71
|
+
.top { display: flex; align-items: center; gap: 12px; padding: 10px 14px 8px; }
|
|
72
|
+
.proj { font-weight: 600; font-size: 14px; letter-spacing: -0.015em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
|
73
|
+
.head { font-family: var(--mono); color: var(--fg-3); font-size: 11.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; flex-shrink: 1000; }
|
|
74
|
+
.head b { color: var(--fg-2); font-weight: 500; }
|
|
75
|
+
.wip { color: var(--fg); font-weight: 600; }
|
|
76
|
+
.search { margin-left: auto; display: flex; align-items: center; gap: 6px; height: 28px; padding: 0 6px 0 8px; min-width: 0; width: min(260px, 40cqi); flex: none;
|
|
77
|
+
background: var(--panel); border: 1px solid var(--line); border-radius: 7px; }
|
|
78
|
+
.search:focus-within { border-color: var(--line-2); box-shadow: 0 0 0 3px var(--sel-soft); }
|
|
79
|
+
.search .p { color: var(--fg-4); font-family: var(--mono); }
|
|
80
|
+
.search input { border: 0; outline: 0; background: none; min-width: 0; flex: 1; font-size: 12.5px; }
|
|
81
|
+
.search input::placeholder { color: var(--fg-4); }
|
|
82
|
+
.search input::-webkit-search-cancel-button { display: none; }
|
|
83
|
+
kbd { font: 500 10.5px/1 var(--mono); color: var(--fg-3); padding: 3px 5px 2px; border: 1px solid var(--line); border-bottom-width: 2px; border-radius: 4px; background: var(--panel); }
|
|
84
|
+
.theme { width: 28px; height: 28px; display: grid; place-items: center; border-radius: 7px; color: var(--fg-3); flex: none; }
|
|
85
|
+
.theme:hover { background: var(--panel); color: var(--fg); }
|
|
86
|
+
|
|
87
|
+
.tabs { display: flex; align-items: end; gap: 2px; padding: 0 8px; border-bottom: 1px solid var(--line); }
|
|
88
|
+
.tab { position: relative; display: flex; align-items: center; gap: 7px; padding: 6px 8px 9px; color: var(--fg-3); font-weight: 500; }
|
|
89
|
+
.tab:hover { color: var(--fg); }
|
|
90
|
+
.tab[aria-selected="true"] { color: var(--fg); }
|
|
91
|
+
.tab[aria-selected="true"]::after { content: ""; position: absolute; left: 6px; right: 6px; bottom: -1px; height: 2px; background: var(--fg); border-radius: 2px; }
|
|
92
|
+
.tab .n { font: 500 10.5px/1 var(--mono); color: var(--fg-3); padding: 3px 5px; border-radius: 4px; background: var(--sunken); }
|
|
93
|
+
.shead { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; font: 11px var(--mono); color: var(--fg-3); }
|
|
94
|
+
.shead b { color: var(--fg); font-weight: 600; }
|
|
95
|
+
.shead .since { margin-left: auto; white-space: nowrap; }
|
|
96
|
+
|
|
97
|
+
main { min-height: 0; position: relative; }
|
|
98
|
+
.view { position: absolute; inset: 0; display: none; min-height: 0; }
|
|
99
|
+
.view.on { display: grid; grid-template-columns: minmax(0, 1fr); }
|
|
100
|
+
|
|
101
|
+
.foot { display: flex; gap: 14px; align-items: center; padding: 6px 14px 7px; border-top: 1px solid var(--line); font: 11px var(--mono); color: var(--fg-4); white-space: nowrap; overflow: hidden; }
|
|
102
|
+
.foot span { display: inline-flex; align-items: center; gap: 4px; }
|
|
103
|
+
.foot .gen { margin-left: auto; overflow: hidden; text-overflow: ellipsis; display: block; }
|
|
104
|
+
.foot .warn { margin: -2px -6px -2px auto; padding: 2px 6px; border-radius: 4px; color: var(--fg-3); white-space: nowrap; flex: none; }
|
|
105
|
+
.foot .warn b { color: var(--fg); font-weight: 600; }
|
|
106
|
+
.foot .warn:hover { background: var(--panel); color: var(--fg); }
|
|
107
|
+
.foot .warn:not([hidden]) + .gen { margin-left: 8px; }
|
|
108
|
+
|
|
109
|
+
/* ---------- shared ---------- */
|
|
110
|
+
.mark { width: 12px; height: 12px; flex: none; display: block; }
|
|
111
|
+
.panel { background: var(--panel); border: 1px solid var(--line); border-radius: var(--r); min-height: 0; display: grid; grid-template-rows: auto minmax(0, 1fr); overflow: hidden; }
|
|
112
|
+
.ph { display: flex; align-items: baseline; gap: 8px; padding: 10px 12px 8px; }
|
|
113
|
+
.ph h2 { margin: 0; font: 600 10.5px/1 var(--mono); text-transform: uppercase; letter-spacing: 0.06em; color: var(--fg-2); }
|
|
114
|
+
.ph .c { font: 11px/1 var(--mono); color: var(--fg-4); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
|
115
|
+
.ph .more { margin-left: auto; font: 11px/1 var(--mono); color: var(--fg-3); }
|
|
116
|
+
.ph .more:hover { color: var(--fg); }
|
|
117
|
+
.scroll { overflow: auto; min-height: 0; }
|
|
118
|
+
.empty { padding: 10px 12px 12px; color: var(--fg-3); }
|
|
119
|
+
.lnk { color: inherit; text-decoration: underline; text-decoration-color: var(--line-2); text-underline-offset: 3px; }
|
|
120
|
+
.lnk:hover { color: var(--fg); text-decoration-color: currentColor; }
|
|
121
|
+
.close { color: var(--fg-3); width: 24px; height: 24px; display: grid; place-items: center; border-radius: 6px; flex: none; }
|
|
122
|
+
.close:hover { background: var(--hover); color: var(--fg); }
|
|
123
|
+
|
|
124
|
+
.trow { display: grid; grid-template-columns: 12px calc(min(var(--idw, 10), 18) * 1ch) minmax(0, 1fr) auto; align-items: center; gap: 10px; padding: 5px 12px; width: 100%; text-align: left; border-top: 1px solid var(--line); font: 11.5px/1.45 var(--mono); }
|
|
125
|
+
.trow:hover { background: var(--hover); }
|
|
126
|
+
.trow .id { font-weight: 500; color: var(--fg); white-space: nowrap; }
|
|
127
|
+
.trow .tt { display: flex; align-items: center; gap: 6px; min-width: 0; color: var(--fg-2); font: 13px/1.4 var(--sans); }
|
|
128
|
+
.trow .tx { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
|
129
|
+
.trow .id { overflow: hidden; text-overflow: ellipsis; }
|
|
130
|
+
.mk { position: relative; width: 12px; height: 12px; }
|
|
131
|
+
.mv { position: absolute; right: -3px; top: -3px; width: 6px; height: 6px; border-radius: 50%; background: var(--fg); box-shadow: 0 0 0 1.5px var(--panel); }
|
|
132
|
+
@container (min-width: 1000px) {
|
|
133
|
+
.trow { grid-template-columns: 12px calc(var(--idw, 10) * 1ch) minmax(0, 1fr) auto; }
|
|
134
|
+
.wrap2 .trow { align-items: start; }
|
|
135
|
+
.wrap2 .trow .mark, .wrap2 .trow .id, .wrap2 .trow .meta, .wrap2 .trow .tag { margin-top: 2px; }
|
|
136
|
+
.wrap2 .trow .tt { align-items: start; }
|
|
137
|
+
.wrap2 .trow .tx { white-space: normal; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; text-wrap: pretty; }
|
|
138
|
+
}
|
|
139
|
+
.trow .meta { font-size: 11px; color: var(--fg-3); white-space: nowrap; text-align: right; }
|
|
140
|
+
|
|
141
|
+
.chip { display: inline-flex; align-items: center; gap: 6px; height: 26px; padding: 0 9px; border: 1px solid var(--line); border-radius: 6px; background: var(--panel); color: var(--fg-2); font-size: 12px; white-space: nowrap; flex: none; }
|
|
142
|
+
.chip:hover { border-color: var(--line-2); color: var(--fg); }
|
|
143
|
+
.chip .n { font: 11px var(--mono); color: var(--fg-3); }
|
|
144
|
+
.chip[aria-pressed="true"] { background: var(--sel); border-color: var(--sel); color: var(--sel-fg); }
|
|
145
|
+
.chip[aria-pressed="true"] .n { color: var(--sel-fg); opacity: .7; }
|
|
146
|
+
.tag { font: 500 9.5px/1 var(--mono); text-transform: uppercase; letter-spacing: .05em; padding: 3px 4px 2px; border-radius: 3px; color: var(--fg-2); background: var(--sunken); white-space: nowrap; flex: none; }
|
|
147
|
+
|
|
148
|
+
.cells { display: inline-flex; gap: 2px; align-items: center; }
|
|
149
|
+
.cells i { width: 5px; height: 10px; border-radius: 1.5px; display: block; }
|
|
150
|
+
|
|
151
|
+
/* ---------- overview ---------- */
|
|
152
|
+
#v-overview { grid-template-rows: auto minmax(0, 1fr); gap: 10px; padding: 10px; }
|
|
153
|
+
.summary { background: var(--panel); border: 1px solid var(--line); border-radius: var(--r); padding: 12px 12px 10px; display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; }
|
|
154
|
+
.strip { display: flex; gap: 2px; height: 8px; }
|
|
155
|
+
.strip i { display: block; height: 100%; min-width: 2px; }
|
|
156
|
+
.strip i:first-child { border-radius: 3px 0 0 3px; } .strip i:last-child { border-radius: 0 3px 3px 0; } .strip i:only-child { border-radius: 3px; }
|
|
157
|
+
.counts { display: grid; gap: 4px; }
|
|
158
|
+
.count { display: grid; grid-template-columns: auto 1fr; column-gap: 6px; row-gap: 3px; align-items: baseline; padding: 4px 6px; margin: -4px -6px; border-radius: 6px; text-align: left; }
|
|
159
|
+
.count:hover { background: var(--hover); }
|
|
160
|
+
.count .v { font: 500 19px/1 var(--mono); letter-spacing: -0.03em; color: var(--fg); }
|
|
161
|
+
.count .l { grid-column: 1 / -1; display: flex; align-items: center; gap: 5px; font-size: 11.5px; color: var(--fg-2); white-space: nowrap; }
|
|
162
|
+
.count .d { font: 11.5px var(--mono); color: var(--fg-4); white-space: nowrap; }
|
|
163
|
+
.count .d.up { color: var(--fg); font-weight: 600; }
|
|
164
|
+
.tracks { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px 20px; padding-top: 10px; border-top: 1px dashed var(--line-2); }
|
|
165
|
+
.track { display: grid; grid-template-columns: 10ch minmax(0, 1fr) 6ch; align-items: center; gap: 8px; text-align: left; padding: 1px 0; }
|
|
166
|
+
.track .nm { font: 11.5px var(--mono); color: var(--fg-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
167
|
+
.track .strip { height: 6px; }
|
|
168
|
+
.track .pc { font: 10.5px var(--mono); color: var(--fg-3); white-space: nowrap; text-align: right; }
|
|
169
|
+
.track:hover .nm { color: var(--fg); }
|
|
170
|
+
.goals { display: grid; gap: 6px; padding-top: 10px; border-top: 1px dashed var(--line-2); }
|
|
171
|
+
.goals .gh { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; margin: 0; font: 600 10px/1 var(--mono); text-transform: uppercase; letter-spacing: .06em; color: var(--fg-2); }
|
|
172
|
+
.goals .gh .c { font: 11px/1 var(--mono); color: var(--fg-4); text-transform: none; letter-spacing: 0; }
|
|
173
|
+
.goals .none { color: var(--fg-3); font-size: 12px; }
|
|
174
|
+
.gq { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 196px), 1fr)); gap: 2px 20px; }
|
|
175
|
+
.goal { display: grid; gap: 4px; min-width: 0; padding: 5px 6px 6px; margin: 0 -6px; border-radius: 6px; text-align: left; }
|
|
176
|
+
.goal:hover { background: var(--hover); }
|
|
177
|
+
.goal .g1 { display: flex; align-items: center; gap: 6px; min-width: 0; font: 11px var(--mono); color: var(--fg-3); white-space: nowrap; }
|
|
178
|
+
.goal .gs { color: var(--fg); font-weight: 500; }
|
|
179
|
+
.goal .gm { overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
|
180
|
+
.goal .gf { margin-left: auto; color: var(--fg-2); }
|
|
181
|
+
.goal .g2 { display: flex; align-items: baseline; gap: 7px; min-width: 0; white-space: nowrap; }
|
|
182
|
+
.goal .gn { font: 500 11.5px var(--mono); color: var(--fg); }
|
|
183
|
+
.goal .gt { overflow: hidden; text-overflow: ellipsis; min-width: 0; color: var(--fg-2); font-size: 12.5px; }
|
|
184
|
+
.goal .g3 { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; align-items: center; font: 10.5px var(--mono); color: var(--fg-3); white-space: nowrap; }
|
|
185
|
+
.goal .strip { height: 6px; }
|
|
186
|
+
.goal .strip:empty { background: var(--sunken); border-radius: 3px; }
|
|
187
|
+
/* Needs you is the approver's whole queue: it takes the left column; the other three panes stack on the right. */
|
|
188
|
+
.ogrid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-rows: minmax(0, 1.25fr) minmax(0, 1fr) minmax(0, 1fr); grid-template-areas: "needs runs" "needs next" "needs levers"; gap: 10px; min-height: 0; }
|
|
189
|
+
.p-needs { grid-area: needs; } .p-runs { grid-area: runs; } .p-next { grid-area: next; } .p-levers { grid-area: levers; }
|
|
190
|
+
|
|
191
|
+
.rrow { display: grid; grid-template-columns: auto auto minmax(0, 1fr); column-gap: 8px; row-gap: 4px; padding: 7px 12px; width: 100%; text-align: left; border-top: 1px solid var(--line); align-items: baseline; }
|
|
192
|
+
.rrow:hover { background: var(--hover); }
|
|
193
|
+
.rrow .t { font: 500 11.5px var(--mono); color: var(--fg); white-space: nowrap; }
|
|
194
|
+
.rrow .h { font: 11px var(--mono); color: var(--fg-4); }
|
|
195
|
+
.rrow .s { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--fg-2); }
|
|
196
|
+
.rrow .x { grid-column: 1 / -1; display: flex; gap: 8px; align-items: center; font: 10.5px var(--mono); color: var(--fg-3); }
|
|
197
|
+
|
|
198
|
+
/* ---------- runs ---------- */
|
|
199
|
+
#v-runs { grid-template-rows: auto minmax(0, 1fr); gap: 10px; padding: 10px; }
|
|
200
|
+
.trend { background: var(--panel); border: 1px solid var(--line); border-radius: var(--r); padding: 12px 12px 8px; }
|
|
201
|
+
.bars { display: flex; align-items: flex-end; gap: 3px; height: 60px; }
|
|
202
|
+
.bars button { flex: 1; height: 100%; display: flex; flex-direction: column-reverse; gap: 1px; border-radius: 3px; padding: 0; }
|
|
203
|
+
.bars button i { display: block; width: 100%; }
|
|
204
|
+
.bars button i:last-child { border-radius: 2px 2px 0 0; }
|
|
205
|
+
.bars button:hover, .bars button[aria-pressed="true"] { outline: 1.5px solid var(--fg); outline-offset: 2px; }
|
|
206
|
+
.days { display: flex; gap: 3px; margin-top: 7px; font: 10.5px var(--mono); color: var(--fg-4); }
|
|
207
|
+
.days span { overflow: hidden; white-space: nowrap; border-left: 1px solid var(--line-2); padding-left: 4px; min-width: 0; }
|
|
208
|
+
.rsplit { display: grid; grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr); gap: 10px; min-height: 0; }
|
|
209
|
+
.daygrp, .qg { position: sticky; top: 0; z-index: 1; background: var(--panel); padding: 7px 12px 5px; font: 600 10px var(--mono); color: var(--fg-3); text-transform: uppercase; letter-spacing: .06em; border-top: 1px solid var(--line); }
|
|
210
|
+
.qg { display: flex; align-items: baseline; gap: 6px; }
|
|
211
|
+
.qg .c { color: var(--fg-4); font-weight: 400; }
|
|
212
|
+
.qg .more { margin-left: auto; font: 11px/1 var(--mono); text-transform: none; letter-spacing: 0; color: var(--fg-3); }
|
|
213
|
+
.qg .more:hover, .qmore:hover { color: var(--fg); }
|
|
214
|
+
.qmore { display: block; width: 100%; padding: 6px 12px 8px; border-top: 1px solid var(--line); text-align: left; font: 11px var(--mono); color: var(--fg-3); }
|
|
215
|
+
.qmore:hover { background: var(--hover); }
|
|
216
|
+
.rrow[aria-current="true"] { background: var(--sel); }
|
|
217
|
+
.rrow[aria-current="true"] .t, .rrow[aria-current="true"] .h, .rrow[aria-current="true"] .s, .rrow[aria-current="true"] .x { color: var(--sel-fg); }
|
|
218
|
+
.rd { padding: 0 12px 12px; }
|
|
219
|
+
.rd h3 { margin: 0 0 3px; font-size: 13.5px; font-weight: 600; letter-spacing: -0.012em; line-height: 1.35; text-wrap: pretty; }
|
|
220
|
+
.rd .sub { font: 11px var(--mono); color: var(--fg-3); }
|
|
221
|
+
.grp { margin-top: 12px; }
|
|
222
|
+
.grp > .gh { display: flex; align-items: center; gap: 6px; font: 600 10px var(--mono); text-transform: uppercase; letter-spacing: .06em; color: var(--fg-2); margin-bottom: 3px; }
|
|
223
|
+
.grp > .gh .c { color: var(--fg-4); font-weight: 400; }
|
|
224
|
+
.grp .trow, .ds .trow { padding-left: 0; padding-right: 0; }
|
|
225
|
+
|
|
226
|
+
/* ---------- tasks ---------- */
|
|
227
|
+
#v-tasks { grid-template-rows: auto minmax(0, 1fr); }
|
|
228
|
+
.tbar { display: grid; gap: 6px; padding: 10px 10px 8px; }
|
|
229
|
+
.tbar .fr { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
|
|
230
|
+
.tbar .sep { width: 1px; height: 18px; background: var(--line-2); margin: 0 3px; flex: none; }
|
|
231
|
+
.tsplit { display: grid; grid-template-columns: minmax(0, 1fr) minmax(300px, 0.85fr); gap: 10px; padding: 0 10px 10px; min-height: 0; }
|
|
232
|
+
.tsplit.nodetail { grid-template-columns: minmax(0, 1fr); }
|
|
233
|
+
.tsplit.nodetail .detail { display: none; }
|
|
234
|
+
.table { display: grid; grid-template-rows: auto minmax(0, 1fr); }
|
|
235
|
+
.thead, .tr { display: grid; grid-template-columns: 12px minmax(10ch, 18ch) minmax(0, 1fr) 10ch 4ch 6ch; gap: 10px; align-items: center; padding: 0 12px; }
|
|
236
|
+
.thead { height: 32px; font: 600 10px var(--mono); text-transform: uppercase; letter-spacing: .06em; color: var(--fg-3); border-bottom: 1px solid var(--line); }
|
|
237
|
+
.thead button { text-align: left; font: inherit; color: inherit; letter-spacing: inherit; text-transform: inherit; white-space: nowrap; }
|
|
238
|
+
.thead button:hover, .thead button[aria-sort] { color: var(--fg); }
|
|
239
|
+
.thead .r, .tr .r { text-align: right; justify-self: end; }
|
|
240
|
+
.tr { min-height: 31px; width: 100%; text-align: left; border-bottom: 1px solid var(--line); }
|
|
241
|
+
.tr:hover { background: var(--hover); }
|
|
242
|
+
.tr[aria-selected="true"] { background: var(--sel); }
|
|
243
|
+
.tr[aria-selected="true"] .id, .tr[aria-selected="true"] .tt, .tr[aria-selected="true"] .tk, .tr[aria-selected="true"] .w, .tr[aria-selected="true"] .u { color: var(--sel-fg); }
|
|
244
|
+
.tr[aria-selected="true"] .tag { background: transparent; color: var(--sel-fg); box-shadow: inset 0 0 0 1px currentColor; }
|
|
245
|
+
.tr.rel { background: var(--sel-soft); }
|
|
246
|
+
.tr .id { font: 500 11.5px var(--mono); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--fg); }
|
|
247
|
+
.tr .tt { display: flex; align-items: center; gap: 6px; min-width: 0; color: var(--fg-2); }
|
|
248
|
+
.tr .tt span:first-child { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
|
249
|
+
.tr .tk, .tr .w, .tr .u { font: 11px var(--mono); color: var(--fg-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
250
|
+
.tr .w.hot { color: var(--fg); font-weight: 600; }
|
|
251
|
+
.tr[aria-selected="true"] .w.hot { color: var(--sel-fg); }
|
|
252
|
+
.detail { display: grid; grid-template-rows: auto minmax(0, 1fr); }
|
|
253
|
+
.dt { padding: 12px 12px 11px; border-bottom: 1px solid var(--line); }
|
|
254
|
+
.dt .row1 { display: flex; align-items: center; gap: 8px; font: 500 12px var(--mono); }
|
|
255
|
+
.dt .row1 .close { margin: -4px -4px 0 auto; }
|
|
256
|
+
.dt h3 { margin: 7px 0 8px; font-size: 14.5px; line-height: 1.35; font-weight: 600; letter-spacing: -0.015em; text-wrap: pretty; }
|
|
257
|
+
.dt .facts { display: flex; flex-wrap: wrap; gap: 4px 12px; font: 11px var(--mono); color: var(--fg-3); align-items: center; }
|
|
258
|
+
.dt .facts b { color: var(--fg-2); font-weight: 500; }
|
|
259
|
+
.dt .notes { margin-top: 7px; display: grid; gap: 2px; font-size: 12px; color: var(--fg-3); }
|
|
260
|
+
.dt .notes b { color: var(--fg-2); font: 600 10px var(--mono); text-transform: uppercase; letter-spacing: .06em; margin-right: 6px; }
|
|
261
|
+
.ds { padding: 2px 12px 14px; }
|
|
262
|
+
.ds h4 { margin: 12px 0 3px; font: 600 10px var(--mono); text-transform: uppercase; letter-spacing: .06em; color: var(--fg-3); display: flex; gap: 6px; }
|
|
263
|
+
.ds h4 .c { color: var(--fg-4); font-weight: 400; }
|
|
264
|
+
.ds .none { color: var(--fg-3); font-size: 12px; padding: 2px 0; }
|
|
265
|
+
.hist { list-style: none; margin: 6px 0 0 5px; padding: 0; border-left: 1px solid var(--line-2); }
|
|
266
|
+
.hist li { position: relative; padding: 3px 0 3px 13px; font: 11.5px var(--mono); color: var(--fg-2); display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
|
|
267
|
+
.hist li::before { content: ""; position: absolute; left: -4px; top: 50%; width: 7px; height: 7px; margin-top: -3.5px; border-radius: 50%; background: var(--panel); border: 1.5px solid var(--fg-4); }
|
|
268
|
+
.hist li:first-child::before { background: var(--fg-2); border-color: var(--fg-2); }
|
|
269
|
+
.hist li .when { color: var(--fg-3); }
|
|
270
|
+
.hist li button { color: var(--fg-3); text-decoration: underline; text-decoration-color: var(--line-2); text-underline-offset: 3px; }
|
|
271
|
+
.hist li button:hover { color: var(--fg); text-decoration-color: currentColor; }
|
|
272
|
+
.arrow { color: var(--fg-4); }
|
|
273
|
+
|
|
274
|
+
/* ---------- warnings popover ---------- */
|
|
275
|
+
.pop { position: fixed; inset: auto 10px 40px auto; margin: 0; padding: 0; width: min(420px, calc(100vw - 20px)); max-height: min(70vh, 520px); overflow: hidden;
|
|
276
|
+
background: var(--panel); color: var(--fg); border: 1px solid var(--line-2); border-radius: var(--r); box-shadow: 0 0 0 4px var(--bg); }
|
|
277
|
+
.pop:popover-open { display: grid; grid-template-rows: auto minmax(0, 1fr); }
|
|
278
|
+
.pop .ph { align-items: center; padding-top: 7px; padding-bottom: 5px; }
|
|
279
|
+
.pop .close { margin: 0 -6px 0 auto; }
|
|
280
|
+
.wg { padding: 8px 12px 10px; border-top: 1px solid var(--line); }
|
|
281
|
+
.wm { color: var(--fg-2); font-size: 12.5px; }
|
|
282
|
+
.wm .c { font: 11px var(--mono); color: var(--fg-4); margin-left: 4px; }
|
|
283
|
+
.wids { display: flex; flex-wrap: wrap; gap: 2px 10px; margin-top: 4px; font: 11.5px var(--mono); color: var(--fg-2); }
|
|
284
|
+
|
|
285
|
+
/* ---------- tooltip ---------- */
|
|
286
|
+
#tip { position: fixed; z-index: 20; pointer-events: none; display: none; max-width: 300px; padding: 7px 9px; border-radius: 7px; background: var(--fg); color: var(--bg); font: 11.5px/1.45 var(--sans); box-shadow: 0 8px 24px rgba(0,0,0,.2); }
|
|
287
|
+
#tip .m { font-family: var(--mono); font-size: 11px; opacity: .75; }
|
|
288
|
+
|
|
289
|
+
/* ---------- responsive ---------- */
|
|
290
|
+
@container (max-width: 1080px) {
|
|
291
|
+
.tsplit { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1.15fr) minmax(0, 1fr); }
|
|
292
|
+
.tsplit.nodetail { grid-template-rows: minmax(0, 1fr); }
|
|
293
|
+
}
|
|
294
|
+
@container (max-width: 640px) {
|
|
295
|
+
.thead, .tr { grid-template-columns: 12px minmax(9ch, 12ch) minmax(0, 1fr) 4ch 6ch; }
|
|
296
|
+
.thead .c-track, .tr .tk { display: none; }
|
|
297
|
+
.tbar .fr { flex-wrap: nowrap; overflow-x: auto; margin: 0 -10px; padding: 0 10px; scrollbar-width: none; }
|
|
298
|
+
}
|
|
299
|
+
@container (max-width: 760px) {
|
|
300
|
+
.rsplit { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 0.9fr) minmax(0, 1.1fr); }
|
|
301
|
+
.foot .gen { display: none; }
|
|
302
|
+
}
|
|
303
|
+
@container (max-width: 640px) {
|
|
304
|
+
.counts { grid-template-columns: repeat(3, minmax(0, 1fr)) !important; row-gap: 12px; }
|
|
305
|
+
}
|
|
306
|
+
@container (max-width: 520px) {
|
|
307
|
+
.head { display: none; }
|
|
308
|
+
.tr .tag.opt { display: none; }
|
|
309
|
+
.foot span:nth-child(3) { display: none; }
|
|
310
|
+
.tracks { grid-template-columns: minmax(0, 1fr); }
|
|
311
|
+
.foot span:nth-child(4) { display: none; }
|
|
312
|
+
.ogrid { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(360px, auto) repeat(3, minmax(220px, 1fr)); grid-template-areas: "needs" "runs" "next" "levers"; }
|
|
313
|
+
#v-overview { overflow: auto; }
|
|
314
|
+
}
|
|
315
|
+
@container (min-width: 1240px) {
|
|
316
|
+
.tracks { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
317
|
+
.counts { gap: 16px; }
|
|
318
|
+
}
|
|
319
|
+
@container (min-width: 1680px) {
|
|
320
|
+
.ogrid { grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-rows: minmax(0, 1fr); grid-template-areas: "needs runs next levers"; }
|
|
321
|
+
}
|
|
322
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
323
|
+
.tr, .trow, .rrow, .chip, .tab, .count, .goal, .theme, .foot .warn { transition: background-color .12s ease-out, color .12s ease-out; }
|
|
324
|
+
}
|
|
325
|
+
</style>
|
|
326
|
+
</head>
|
|
327
|
+
<body>
|
|
328
|
+
<div class="app" id="app">
|
|
329
|
+
<header class="top">
|
|
330
|
+
<span class="proj" id="proj"></span>
|
|
331
|
+
<span class="head" id="head"></span>
|
|
332
|
+
<label class="search" title="Filter tasks (/)">
|
|
333
|
+
<span class="p" aria-hidden="true">›</span>
|
|
334
|
+
<input id="q" type="search" placeholder="Filter tasks" autocomplete="off" spellcheck="false" aria-label="Filter tasks by ID or title">
|
|
335
|
+
<kbd>/</kbd>
|
|
336
|
+
</label>
|
|
337
|
+
<button class="theme" id="theme" aria-label="Toggle light or dark theme" title="Theme"></button>
|
|
338
|
+
</header>
|
|
339
|
+
<nav class="tabs" role="tablist" aria-label="Views">
|
|
340
|
+
<button class="tab" role="tab" data-view="overview" aria-selected="true">Overview</button>
|
|
341
|
+
<button class="tab" role="tab" data-view="runs" aria-selected="false">Runs <span class="n" id="n-runs"></span></button>
|
|
342
|
+
<button class="tab" role="tab" data-view="tasks" aria-selected="false">Tasks <span class="n" id="n-tasks"></span></button>
|
|
343
|
+
</nav>
|
|
344
|
+
<main>
|
|
345
|
+
<section class="view on" id="v-overview" role="tabpanel" aria-label="Overview"></section>
|
|
346
|
+
<section class="view" id="v-runs" role="tabpanel" aria-label="Runs"></section>
|
|
347
|
+
<section class="view" id="v-tasks" role="tabpanel" aria-label="Tasks"></section>
|
|
348
|
+
</main>
|
|
349
|
+
<footer class="foot">
|
|
350
|
+
<span><kbd>1</kbd><kbd>2</kbd><kbd>3</kbd> views</span>
|
|
351
|
+
<span><kbd>/</kbd> filter</span>
|
|
352
|
+
<span><kbd>j</kbd><kbd>k</kbd> move</span>
|
|
353
|
+
<span><kbd>esc</kbd> clear</span>
|
|
354
|
+
<button class="warn" id="warnbtn" popovertarget="warnpop" hidden></button>
|
|
355
|
+
<span class="gen" id="gen"></span>
|
|
356
|
+
</footer>
|
|
357
|
+
</div>
|
|
358
|
+
<div class="pop" id="warnpop" popover aria-label="Warnings"></div>
|
|
359
|
+
<div id="tip" role="tooltip"></div>
|
|
360
|
+
|
|
361
|
+
<script>
|
|
362
|
+
const DATA = /*DATA*/null;
|
|
363
|
+
|
|
364
|
+
// ---------- model ----------
|
|
365
|
+
// ready, openDeps, waitingOn, unlocks and goal come from the data (lib/graph.mjs owns those rules).
|
|
366
|
+
// This page only sorts and groups them for presentation.
|
|
367
|
+
const STATUS = {
|
|
368
|
+
"blocked": { label: "Blocked", short: "blocked" },
|
|
369
|
+
"decision": { label: "Decision", short: "decide" },
|
|
370
|
+
"in-progress": { label: "In progress", short: "wip" },
|
|
371
|
+
"review": { label: "Review", short: "review" },
|
|
372
|
+
"open": { label: "Open", short: "open" },
|
|
373
|
+
"done": { label: "Done", short: "done" },
|
|
374
|
+
"dropped": { label: "Dropped", short: "dropped" },
|
|
375
|
+
"removed": { label: "Removed", short: "removed" },
|
|
376
|
+
};
|
|
377
|
+
const PROGRESS = ["done", "review", "in-progress", "open", "decision", "blocked", "dropped"];
|
|
378
|
+
const RANK = { blocked: 0, decision: 1, "in-progress": 2, review: 3, open: 4, done: 5, dropped: 6 };
|
|
379
|
+
// Goal states reuse the task mark with the same meaning: running works, approved waits to start, draft waits on the owner.
|
|
380
|
+
const GOAL_MARK = { running: "in-progress", approved: "open", draft: "decision", review: "review", done: "done", legacy: "removed" };
|
|
381
|
+
const COLOR = (s) => `var(--s-${s === "in-progress" ? "wip" : Object.hasOwn(STATUS, s) && s !== "removed" ? s : "open"})`;
|
|
382
|
+
const tasks = DATA.tasks, runs = DATA.runs, goals = DATA.goals ?? [], warnings = (DATA.warnings ?? []).map(String), lastRun = runs.length - 1;
|
|
383
|
+
const byId = new Map(tasks.map((t) => [t.id, t]));
|
|
384
|
+
const goalByName = new Map(goals.map((g) => [g.name, g]));
|
|
385
|
+
// Every data value goes through esc() before it enters markup. Tooltip markup is escaped twice:
|
|
386
|
+
// esc() each value inside it, then esc() the whole string for the data-tip attribute (showTip parses it as HTML).
|
|
387
|
+
const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
388
|
+
const dayKey = (iso) => { const d = new Date(iso); return `${String(d.getDate()).padStart(2, "0")}.${String(d.getMonth() + 1).padStart(2, "0")}.`; };
|
|
389
|
+
const hm = (iso) => new Date(iso).toLocaleTimeString("de-AT", { hour: "2-digit", minute: "2-digit" });
|
|
390
|
+
const lab = (s) => (Object.hasOwn(STATUS, s) ? STATUS[s].label : String(s));
|
|
391
|
+
const short = (s) => (s === null ? "new" : Object.hasOwn(STATUS, s) ? STATUS[s].short : String(s));
|
|
392
|
+
const cnt = (arr, s) => arr.filter((t) => t.status === s).length;
|
|
393
|
+
const idCmp = (a, b) => a.id.localeCompare(b.id, undefined, { numeric: true });
|
|
394
|
+
const byLever = (a, b) => b.unlocks - a.unlocks || b.waitingOn.length - a.waitingOn.length;
|
|
395
|
+
const statuses = PROGRESS.filter((s) => tasks.some((t) => t.status === s));
|
|
396
|
+
const trackOrder = [...new Set(tasks.map((t) => t.track))].sort((a, b) => tasks.filter((t) => t.track === b).length - tasks.filter((t) => t.track === a).length);
|
|
397
|
+
const today = dayKey(runs.at(-1)?.date ?? DATA.generatedAt);
|
|
398
|
+
const baseIdx = (() => { for (let i = lastRun; i >= 0; i--) if (dayKey(runs[i].date) !== today) return i; return -1; })();
|
|
399
|
+
const baseRun = baseIdx >= 0 ? runs[baseIdx] : null;
|
|
400
|
+
const changedSinceBase = new Set(tasks.filter((t) => t.history.some((h) => h.run > baseIdx)).map((t) => t.id));
|
|
401
|
+
const queued = goals.filter((g) => g.state !== "done" && g.state !== "legacy");
|
|
402
|
+
// The approver's queue: goals to approve (draft) or check (review), decisions and blocked tasks, tasks to check (review).
|
|
403
|
+
const ASK = { draft: "approve", review: "check" };
|
|
404
|
+
const askGoals = goals.filter((g) => Object.hasOwn(ASK, g.state));
|
|
405
|
+
const goalTasks = (name) => tasks.filter((t) => t.goal.includes(name));
|
|
406
|
+
const inActiveGoal = (t) => t.goal.some((n) => ["running", "approved"].includes(goalByName.get(n)?.state));
|
|
407
|
+
const needsYou = tasks.filter((t) => t.status === "blocked" || t.status === "decision").sort((a, b) => byLever(a, b) || RANK[a.status] - RANK[b.status] || idCmp(a, b));
|
|
408
|
+
const upNext = tasks.filter((t) => t.ready).sort((a, b) => inActiveGoal(b) - inActiveGoal(a) || byLever(a, b) || idCmp(a, b));
|
|
409
|
+
const toCheck = tasks.filter((t) => t.status === "review").sort((a, b) => (b.lastRun ?? -1) - (a.lastRun ?? -1) || idCmp(a, b));
|
|
410
|
+
const CHECK_ROWS = 5;
|
|
411
|
+
// review and done already satisfy dependencies, so only unfinished work can hold others up.
|
|
412
|
+
const levers = tasks.filter((t) => !["review", "done", "dropped"].includes(t.status) && t.waitingOn.length).sort((a, b) => b.waitingOn.length - a.waitingOn.length || b.unlocks - a.unlocks || idCmp(a, b));
|
|
413
|
+
const runLabel = (r) => `${dayKey(r.date) === today ? "" : dayKey(r.date) + " "}${hm(r.date)}`;
|
|
414
|
+
const goalReview = (g) => Number(g.review) || 0;
|
|
415
|
+
const goalBuilt = (g) => (Number(g.done) || 0) + goalReview(g);
|
|
416
|
+
const runAt = (t) => (t.lastRun === null || t.lastRun === undefined ? null : runs[t.lastRun]);
|
|
417
|
+
|
|
418
|
+
// ---------- marks ----------
|
|
419
|
+
function mark(s) {
|
|
420
|
+
const c = COLOR(s);
|
|
421
|
+
const body = {
|
|
422
|
+
"open": `<circle cx="6" cy="6" r="4.6" fill="none" stroke="${c}" stroke-width="1.5"/>`,
|
|
423
|
+
"in-progress": `<circle cx="6" cy="6" r="4.6" fill="none" stroke="${c}" stroke-width="1.5"/><path d="M6 2.6a3.4 3.4 0 0 1 0 6.8z" fill="${c}"/>`,
|
|
424
|
+
"review": `<circle cx="6" cy="6" r="4.6" fill="none" stroke="${c}" stroke-width="1.5"/><circle cx="6" cy="6" r="2" fill="${c}"/>`,
|
|
425
|
+
"done": `<circle cx="6" cy="6" r="5.4" fill="${c}"/><path d="M3.6 6.1l1.6 1.6 3.2-3.3" fill="none" stroke="var(--mark-ink)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>`,
|
|
426
|
+
"blocked": `<circle cx="6" cy="6" r="5.4" fill="${c}"/><rect x="3.3" y="5.25" width="5.4" height="1.5" rx=".75" fill="var(--mark-ink)"/>`,
|
|
427
|
+
"decision": `<rect x="2.1" y="2.1" width="7.8" height="7.8" rx="1.6" transform="rotate(45 6 6)" fill="${c}"/><rect x="5.3" y="3.3" width="1.4" height="3.3" rx=".7" fill="var(--mark-ink)"/><circle cx="6" cy="8.2" r=".8" fill="var(--mark-ink)"/>`,
|
|
428
|
+
"dropped": `<circle cx="6" cy="6" r="4.6" fill="none" stroke="${c}" stroke-width="1.5"/><path d="M3.2 8.8l5.6-5.6" stroke="${c}" stroke-width="1.5" stroke-linecap="round"/>`,
|
|
429
|
+
"removed": `<circle cx="6" cy="6" r="4.6" fill="none" stroke="${c}" stroke-width="1.5" stroke-dasharray="2 2"/>`,
|
|
430
|
+
"new": `<path d="M6 2.5v7M2.5 6h7" stroke="var(--fg-2)" stroke-width="1.6" stroke-linecap="round"/>`,
|
|
431
|
+
}[s];
|
|
432
|
+
return `<svg class="mark" viewBox="0 0 12 12" aria-hidden="true">${typeof body === "string" ? body : ""}</svg>`;
|
|
433
|
+
}
|
|
434
|
+
const gmark = (g) => mark(Object.hasOwn(GOAL_MARK, g.state) ? GOAL_MARK[g.state] : "open");
|
|
435
|
+
const strip = (items, label = "") => { const total = items.reduce((a, [, n]) => a + n, 0) || 1; return `<span class="strip" role="img" aria-label="${esc(label + items.filter(([, n]) => n).map(([s, n]) => `${lab(s)} ${n}`).join(", "))}">${items.filter(([, n]) => n).map(([s, n]) => `<i style="flex:${n};background:${COLOR(s)}" data-tip="${esc(`${esc(label)}${esc(lab(s))} · ${esc(n)} <span class='m'>${Math.round((n / total) * 100)}%</span>`)}"></i>`).join("")}</span>`; };
|
|
436
|
+
const cells = (changes, max = 16) => `<span class="cells" aria-hidden="true">${changes.slice(0, max).map((c) => `<i style="background:${c.from === null ? "var(--fg-4)" : COLOR(c.to)}"></i>`).join("")}</span>${changes.length > max ? `<span>+${changes.length - max}</span>` : ""}`;
|
|
437
|
+
const taskTip = (t) => esc(
|
|
438
|
+
`<b>${esc(t.id)}</b> <span class='m'>${esc(lab(t.status))} · ${esc(t.track)}</span><br>` +
|
|
439
|
+
`${esc(t.title)}` +
|
|
440
|
+
(t.goal.length ? `<br><span class='m'>goal ${esc(t.goal.join(", "))}</span>` : "")
|
|
441
|
+
);
|
|
442
|
+
const goalTip = (g) => esc(`<b>${esc(g.name)}</b> <span class='m'>${esc(g.state)}${g.stream ? ` · ${esc(g.stream)}` : ""}${g.budget ? ` · ${esc(g.budget)}` : ""}</span><br>${esc(g.title ?? g.name)}${g.state === "draft" ? "<br>Draft: waiting for your approval." : g.state === "review" ? "<br>Review: over budget or waiting for your sign-off." : ""}<br><span class='m'>${esc(goalBuilt(g))}/${esc(g.total)} built · ${esc(g.done)} done · ${esc(goalReview(g))} to check · ${esc(g.ready.length)} ready · ${esc(g.waiting.length)} waiting${g.thread ? ` · ${esc(g.thread)}` : ""}</span>`);
|
|
443
|
+
const trow = (t, meta = "") => `<button class="trow" data-task="${esc(t.id)}" data-tip="${taskTip(t)}">
|
|
444
|
+
<span class="mk">${mark(t.status)}${changedSinceBase.has(t.id) ? `<span class="mv"
|
|
445
|
+
title="moved since ${baseRun ? dayKey(baseRun.date) : "start"}"></span>` : ""}</span>
|
|
446
|
+
<span class="id">${esc(t.id)}</span><span class="tt"><span class="tx">${esc(t.title)}</span></span>
|
|
447
|
+
<span class="meta">${esc(meta)}</span></button>`;
|
|
448
|
+
const grow = (g) => `<button class="trow" data-goal="${esc(g.name)}" data-tip="${goalTip(g)}"><span class="mk">${gmark(g)}</span><span class="id">${esc(g.name)}</span><span class="tt"><span class="tx">${esc(g.title ?? g.name)}</span><span class="tag">goal</span></span><span class="meta">${esc(ASK[g.state] ?? g.state)}</span></button>`;
|
|
449
|
+
const idw = (list) => `--idw:${Math.max(6, ...list.map((t) => t.id.length))}`;
|
|
450
|
+
const lever = (t) => [t.unlocks && `unlocks ${t.unlocks}`, t.waitingOn.length && `blocks ${t.waitingOn.length}`].filter(Boolean).join(" · ") || "·";
|
|
451
|
+
|
|
452
|
+
// ---------- state ----------
|
|
453
|
+
const state = { view: "overview", run: lastRun, filter: "all", track: null, goal: null, q: "", sel: null, sort: "status" };
|
|
454
|
+
const clearFilters = () => { state.filter = "all"; state.track = null; state.goal = null; state.q = ""; q.value = ""; };
|
|
455
|
+
|
|
456
|
+
// ---------- header ----------
|
|
457
|
+
const projName = DATA.project ?? "planning";
|
|
458
|
+
document.getElementById("proj").textContent = projName;
|
|
459
|
+
document.getElementById("proj").title = projName;
|
|
460
|
+
document.title = `${projName} · taskboard`;
|
|
461
|
+
document.getElementById("head").innerHTML = `<b>${esc(runs.at(-1) ? runs.at(-1).commit ?? "working tree" : String(DATA.commit ?? "").split(" · ")[0])}</b>${DATA.dirty ? ` · <span class="wip">uncommitted</span>` : ""} · ${today} ${hm(runs.at(-1)?.date ?? DATA.generatedAt)}`;
|
|
462
|
+
document.getElementById("n-runs").textContent = runs.length;
|
|
463
|
+
document.getElementById("n-tasks").textContent = tasks.length;
|
|
464
|
+
document.getElementById("gen").textContent = `generated from ${DATA.commit}`;
|
|
465
|
+
|
|
466
|
+
// ---------- warnings ----------
|
|
467
|
+
const warnBtn = document.getElementById("warnbtn"), warnPop = document.getElementById("warnpop");
|
|
468
|
+
if (warnings.length) {
|
|
469
|
+
const groups = new Map();
|
|
470
|
+
for (const w of warnings) {
|
|
471
|
+
const m = w.match(/^(\S+?):\s+(.+)$/);
|
|
472
|
+
const [key, msg] = m ? [m[1], m[2]] : [null, w];
|
|
473
|
+
if (!groups.has(msg)) groups.set(msg, []);
|
|
474
|
+
groups.get(msg).push(key);
|
|
475
|
+
}
|
|
476
|
+
warnBtn.hidden = false;
|
|
477
|
+
warnBtn.innerHTML = `<b>${warnings.length}</b> warning${warnings.length === 1 ? "" : "s"}`;
|
|
478
|
+
warnPop.innerHTML = `
|
|
479
|
+
<div class="ph"><h2>Warnings</h2><span class="c">${warnings.length}</span><button class="close" popovertarget="warnpop" popovertargetaction="hide" aria-label="Close warnings" title="Close (esc)"><svg width="12" height="12" viewBox="0 0 12 12" aria-hidden="true"><path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></button></div>
|
|
480
|
+
<div class="scroll">${[...groups].map(([msg, keys]) => `<div class="wg"><div class="wm">${esc(msg)}<span class="c">${keys.length}</span></div>${keys.some(Boolean) ? `<div class="wids">${keys.filter(Boolean).map((k) => (byId.has(k) ? `<button class="lnk" data-task="${esc(k)}">${esc(k)}</button>` : `<span>${esc(k)}</span>`)).join("")}</div>` : ""}</div>`).join("")}</div>`;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// ---------- overview ----------
|
|
484
|
+
function delta(s) {
|
|
485
|
+
if (!baseRun) return "";
|
|
486
|
+
const d = cnt(tasks, s) - (baseRun.counts[s] ?? 0);
|
|
487
|
+
return `<span class="d ${d ? "up" : ""}">${d > 0 ? "+" : d < 0 ? "−" : "±"}${Math.abs(d)}</span>`;
|
|
488
|
+
}
|
|
489
|
+
function runRow(r, current = false) {
|
|
490
|
+
const i = runs.indexOf(r);
|
|
491
|
+
const news = r.changes.filter((c) => c.from === null).length;
|
|
492
|
+
const done = r.changes.filter((c) => c.from !== null && c.to === "done").length;
|
|
493
|
+
const moved = r.changes.length - done - news;
|
|
494
|
+
const summary = [done && `${done} done`, news && `${news} new`, moved && `${moved} moved`].filter(Boolean).join(" · ") || (r.initial ? `baseline · ${esc(r.total)} tasks` : "no status change");
|
|
495
|
+
return `<button class="rrow" data-run="${i}" ${current ? 'aria-current="true"' : ""}><span class="t">${runLabel(r)}</span><span class="h">${esc(r.commit ?? "uncommitted")}</span><span class="s">${esc(r.subject)}</span><span class="x">${cells(r.changes)}<span>${summary}</span></span></button>`;
|
|
496
|
+
}
|
|
497
|
+
function goalTile(g) {
|
|
498
|
+
const scope = g.scope.map((id) => byId.get(id)).filter(Boolean);
|
|
499
|
+
const note = [
|
|
500
|
+
goalReview(g) && `${goalReview(g)} to check`,
|
|
501
|
+
g.ready.length && `${g.ready.length} ready`,
|
|
502
|
+
g.waiting.length && `${g.waiting.length} waiting`,
|
|
503
|
+
].filter(Boolean).join(" · ") || "·";
|
|
504
|
+
return `<button class="goal" data-goal="${esc(g.name)}" data-tip="${goalTip(g)}">
|
|
505
|
+
<span class="g1">${gmark(g)}<span class="gs">${esc(g.state)}</span><span class="gm">${esc([g.stream, g.budget].filter(Boolean).join(" · "))}</span><span class="gf">${esc(goalBuilt(g))}/${esc(g.total)}</span></span>
|
|
506
|
+
<span class="g2"><span class="gn">${esc(g.name)}</span><span class="gt">${esc(g.title ?? "")}</span></span>
|
|
507
|
+
<span class="g3">${scope.length ? strip(PROGRESS.map((s) => [s, cnt(scope, s)]), `${g.name} · `) : `<span class="strip"></span>`}<span>${esc(note)}</span></span>
|
|
508
|
+
</button>`;
|
|
509
|
+
}
|
|
510
|
+
function goalQueue() {
|
|
511
|
+
const of = (s) => goals.filter((g) => g.state === s);
|
|
512
|
+
const counts = ["running", "approved", "draft", "review"].filter((s) => of(s).length).map((s) => `${of(s).length} ${s}`).join(" · ");
|
|
513
|
+
const rest = ["legacy", "done"].filter((s) => of(s).length).map((s) => `<span class="c" tabindex="0" data-tip="${esc(of(s).map((g) => `<b>${esc(g.name)}</b> <span class='m'>${esc(g.file)}</span>`).join("<br>"))}">+${of(s).length} ${s}</span>`).join("");
|
|
514
|
+
return `<div class="goals">
|
|
515
|
+
<h3 class="gh">Goals${counts ? `<span class="c">${counts}</span>` : ""}${rest}</h3>
|
|
516
|
+
${queued.length ? `<div class="gq">${queued.map(goalTile).join("")}</div>` : `<div class="none">${goals.length ? "No goal is running, approved or waiting for approval." : "No goal files yet."}</div>`}
|
|
517
|
+
</div>`;
|
|
518
|
+
}
|
|
519
|
+
function renderOverview() {
|
|
520
|
+
const el = document.getElementById("v-overview");
|
|
521
|
+
const shown = ["done", "review", "in-progress", "open", "decision", "blocked"].filter((s) => cnt(tasks, s) || runs.some((r) => r.counts[s]));
|
|
522
|
+
const recent = [...runs].reverse().filter((r) => !r.initial).slice(0, 10);
|
|
523
|
+
const checkRows = toCheck.slice(0, CHECK_ROWS);
|
|
524
|
+
const needs = [...askGoals.map((g) => ({ id: g.name })), ...needsYou, ...checkRows];
|
|
525
|
+
const queueCount = [askGoals.length && `${askGoals.length} goal${askGoals.length === 1 ? "" : "s"}`, needsYou.length && `${needsYou.length} to decide`, toCheck.length && `${toCheck.length} to check`].filter(Boolean).join(" · ") || "0";
|
|
526
|
+
const group = (label, n, filter, rows) => `<div class="qg">${label} <span class="c">${n}</span>${filter ? `<button class="more" data-filter="${filter}">all →</button>` : ""}</div>${rows}`;
|
|
527
|
+
el.innerHTML = `
|
|
528
|
+
<div class="summary">
|
|
529
|
+
<div class="shead">
|
|
530
|
+
<span><b>${tasks.length}</b> tasks · <b>${trackOrder.length}</b> tracks</span>
|
|
531
|
+
${baseRun ? `<span class="since">
|
|
532
|
+
Δ since ${dayKey(baseRun.date)} · <b>${changedSinceBase.size}</b> moved</span>` : ""}
|
|
533
|
+
</div>
|
|
534
|
+
${strip(PROGRESS.map((s) => [s, cnt(tasks, s)]))}
|
|
535
|
+
<div class="counts" style="grid-template-columns:repeat(${shown.length}, minmax(0,1fr))">
|
|
536
|
+
${shown.map((s) => `<button class="count" data-filter="status:${s}" title="Show ${lab(s).toLowerCase()} tasks"><span class="v">${cnt(tasks, s)}</span>${delta(s)}<span class="l">${mark(s)}${lab(s)}</span></button>`).join("")}
|
|
537
|
+
</div>
|
|
538
|
+
${goalQueue()}
|
|
539
|
+
<div class="tracks">
|
|
540
|
+
${trackOrder.map((tr) => { const ts = tasks.filter((t) => t.track === tr); return `<button class="track" data-track="${esc(tr)}" title="Show ${esc(tr)} tasks"><span class="nm">${esc(tr)}</span>${strip(PROGRESS.map((s) => [s, cnt(ts, s)]), tr + " · ")}<span class="pc">${cnt(ts, "done")}/${ts.length}</span></button>`; }).join("")}
|
|
541
|
+
</div>
|
|
542
|
+
</div>
|
|
543
|
+
<div class="ogrid">
|
|
544
|
+
<section class="panel p-needs">
|
|
545
|
+
<div class="ph"><h2>Needs you</h2><span class="c">${queueCount}</span></div>
|
|
546
|
+
<div class="scroll" style="${idw(needs)}">${[
|
|
547
|
+
askGoals.length && group("Goals", askGoals.length, "", askGoals.map(grow).join("")),
|
|
548
|
+
needsYou.length && group("To decide", needsYou.length, "needs", needsYou.map((t) => trow(t, lever(t))).join("")),
|
|
549
|
+
toCheck.length && group("To check", toCheck.length, "check", checkRows.map((t) => trow(t, runAt(t) ? runLabel(runAt(t)) : "·")).join("") + (toCheck.length > CHECK_ROWS ? `<button class="qmore" data-filter="check">${toCheck.length - CHECK_ROWS} more in Tasks →</button>` : "")),
|
|
550
|
+
].filter(Boolean).join("") || `<div class="empty">Nothing waits for you: no goal to approve or check, no decision, nothing blocked or to check.</div>`}</div>
|
|
551
|
+
</section>
|
|
552
|
+
<section class="panel p-runs">
|
|
553
|
+
<div class="ph"><h2>Recent runs</h2><span class="c">${changedSinceBase.size} tasks moved${baseRun ? ` since ${dayKey(baseRun.date)}` : ""}</span><button class="more" data-goview="runs">all →</button></div>
|
|
554
|
+
<div class="scroll">${recent.map((r) => runRow(r)).join("") || `<div class="empty">Only the baseline commit so far.</div>`}</div>
|
|
555
|
+
</section>
|
|
556
|
+
<section class="panel p-next">
|
|
557
|
+
<div class="ph"><h2>Up next</h2><span class="c">${upNext.length} ready</span>${upNext.length ? `<button class="more" data-filter="ready">all →</button>` : ""}</div>
|
|
558
|
+
<div class="scroll" style="${idw(upNext)}">${upNext.map((t) => trow(t, [t.goal[0], t.unlocks && `unlocks ${t.unlocks}`, t.waitingOn.length && `blocks ${t.waitingOn.length}`].filter(Boolean).join(" · ") || "·")).join("") || `<div class="empty">No open task has all of its dependencies done.</div>`}</div>
|
|
559
|
+
</section>
|
|
560
|
+
<section class="panel p-levers">
|
|
561
|
+
<div class="ph"><h2>Bottlenecks</h2><span class="c">open tasks that block the most</span></div>
|
|
562
|
+
<div class="scroll" style="${idw(levers.slice(0, 12))}">${levers.slice(0, 12).map((t) => trow(t, lever(t))).join("") || `<div class="empty">No open task holds others up.</div>`}</div>
|
|
563
|
+
</section>
|
|
564
|
+
</div>`;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// ---------- runs ----------
|
|
568
|
+
function renderRuns() {
|
|
569
|
+
const el = document.getElementById("v-runs");
|
|
570
|
+
if (!runs.length) { el.innerHTML = `<section class="panel"><div class="ph"><h2>Runs</h2></div><div class="empty">No commit has touched tasks/index.json yet.</div></section>`; return; }
|
|
571
|
+
const max = Math.max(...runs.map((r) => r.total));
|
|
572
|
+
const r = runs[state.run];
|
|
573
|
+
const groups = new Map();
|
|
574
|
+
for (const c of r.changes) { const k = c.from === null ? "new" : c.to; if (!groups.has(k)) groups.set(k, []); groups.get(k).push(c); }
|
|
575
|
+
const order = ["new", "done", "review", "in-progress", "open", "decision", "blocked", "dropped", "removed"];
|
|
576
|
+
let lastDay = null;
|
|
577
|
+
const list = runs.map((x, i) => ({ x, i })).reverse().map(({ x, i }) => {
|
|
578
|
+
const d = dayKey(x.date);
|
|
579
|
+
const head = d !== lastDay ? `<div class="daygrp">${d === today ? "Today" : d} · ${runs.filter((y) => dayKey(y.date) === d).length} runs</div>` : "";
|
|
580
|
+
lastDay = d;
|
|
581
|
+
return head + runRow(x, i === state.run);
|
|
582
|
+
}).join("");
|
|
583
|
+
const days = []; for (const x of runs) { const d = dayKey(x.date); if (days.at(-1)?.d === d) days.at(-1).n++; else days.push({ d, n: 1 }); }
|
|
584
|
+
el.innerHTML = `
|
|
585
|
+
<div class="trend">
|
|
586
|
+
<div class="bars">${runs.map((x, i) => `<button data-run="${i}" aria-pressed="${i === state.run}" aria-label="Run ${dayKey(x.date)} ${hm(x.date)}" data-tip="${esc(`<b>${dayKey(x.date)} ${hm(x.date)}</b> <span class='m'>${esc(x.commit ?? "uncommitted")}</span><br>${esc(x.subject)}<br><span class='m'>${PROGRESS.filter((s) => x.counts[s]).map((s) => `${short(s)} ${esc(x.counts[s])}`).join(" · ")}</span>`)}">${PROGRESS.filter((s) => x.counts[s]).map((s) => `<i style="height:calc(${(x.counts[s] / max) * 100}% - 1px);background:${COLOR(s)}"></i>`).join("")}</button>`).join("")}</div>
|
|
587
|
+
<div class="days">${days.map((d) => `<span style="flex:${d.n}">${d.n > 1 || days.length < 8 ? d.d : d.d.slice(0, 2)}</span>`).join("")}</div>
|
|
588
|
+
</div>
|
|
589
|
+
<div class="rsplit">
|
|
590
|
+
<section class="panel"><div class="ph"><h2>Runs</h2><span class="c">commits to tasks/index.json</span></div><div class="scroll" id="runlist">${list}</div></section>
|
|
591
|
+
<section class="panel"><div class="ph"><h2>Changes</h2><span class="c">${r.changes.length}</span>${r.changes.length ? `<button class="more" data-filter="run:${state.run}">in tasks →</button>` : ""}</div>
|
|
592
|
+
<div class="scroll"><div class="rd">
|
|
593
|
+
<h3>${esc(r.subject)}</h3>
|
|
594
|
+
<div class="sub">${dayKey(r.date)} ${hm(r.date)} · ${esc(r.commit ?? "uncommitted (git status)")} · ${esc(r.total)} tasks</div>
|
|
595
|
+
${r.initial ? `<div class="grp muted">Baseline: the first commit of the planning repository.</div>` : r.changes.length ? order.filter((k) => groups.has(k)).map((k) => `<div class="grp"><div class="gh">${mark(k)}${k === "new" ? "Added" : "To " + lab(k).toLowerCase()} <span class="c">${groups.get(k).length}</span></div>${(() => { const ts = groups.get(k).map((c) => byId.get(c.id)).filter(Boolean); return `<div style="${idw(ts.length ? ts : [{ id: "" }])}">`; })()}${groups.get(k).map((c) => { const t = byId.get(c.id); return t ? trow(t, c.from === null ? short(c.to) : `${short(c.from)} → ${short(c.to)}`) : `<div class="trow"><span></span><span class="id">${esc(c.id)}</span><span class="tt muted">no longer in the index</span><span></span></div>`; }).join("")}</div></div>`).join("") : `<div class="grp muted">No status changed in this run.</div>`}
|
|
596
|
+
</div></div></section>
|
|
597
|
+
</div>`;
|
|
598
|
+
el.querySelector('#runlist [aria-current="true"]')?.scrollIntoView({ block: "nearest" });
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// ---------- tasks ----------
|
|
602
|
+
function filterChips() {
|
|
603
|
+
const primary = [
|
|
604
|
+
["all", "All", tasks.length],
|
|
605
|
+
["needs", "Needs you", needsYou.length],
|
|
606
|
+
["check", "To check", toCheck.length],
|
|
607
|
+
["ready", "Up next", upNext.length],
|
|
608
|
+
["changed", baseRun ? `Moved since ${dayKey(baseRun.date)}` : "Moved", changedSinceBase.size],
|
|
609
|
+
];
|
|
610
|
+
if (state.filter.startsWith("run:")) primary.push([state.filter, `Run ${runLabel(runs[+state.filter.slice(4)])}`, tasks.filter((t) => t.history.some((h) => h.run === +state.filter.slice(4))).length]);
|
|
611
|
+
const byStatus = statuses.map((s) => [`status:${s}`, lab(s), cnt(tasks, s), s]);
|
|
612
|
+
const chip = ([k, l, n, s]) => `<button class="chip" data-filter="${k}" aria-pressed="${state.filter === k}">${s ? mark(s) : ""}${l} <span class="n">${n}</span></button>`;
|
|
613
|
+
const goalChips = queued.filter((g) => goalTasks(g.name).length).map((g) => `<button class="chip" data-goal="${esc(g.name)}" aria-pressed="${state.goal === g.name}" data-tip="${goalTip(g)}">${gmark(g)}${esc(g.name)} <span class="n">${goalTasks(g.name).length}</span></button>`).join("");
|
|
614
|
+
const trackChips = trackOrder.map((tr) => `<button class="chip" data-track="${esc(tr)}" aria-pressed="${state.track === tr}">${esc(tr)} <span class="n">${tasks.filter((t) => t.track === tr).length}</span></button>`).join("");
|
|
615
|
+
return `<div class="fr">${primary.map(chip).join("")}</div><div class="fr">${goalChips ? `${goalChips}<span class="sep"></span>` : ""}${trackChips}</div><div class="fr">${byStatus.map(chip).join("")}</div>`;
|
|
616
|
+
}
|
|
617
|
+
function matches(t) {
|
|
618
|
+
const f = state.filter;
|
|
619
|
+
if (f === "needs" && !(t.status === "blocked" || t.status === "decision")) return false;
|
|
620
|
+
if (f === "check" && t.status !== "review") return false;
|
|
621
|
+
if (f === "ready" && !t.ready) return false;
|
|
622
|
+
if (f === "changed" && !changedSinceBase.has(t.id)) return false;
|
|
623
|
+
if (f.startsWith("status:") && t.status !== f.slice(7)) return false;
|
|
624
|
+
if (f.startsWith("run:") && !t.history.some((h) => h.run === +f.slice(4))) return false;
|
|
625
|
+
if (state.track && t.track !== state.track) return false;
|
|
626
|
+
if (state.goal && !t.goal.includes(state.goal)) return false;
|
|
627
|
+
if (state.q && !(t.id.toLowerCase().includes(state.q) || t.title.toLowerCase().includes(state.q))) return false;
|
|
628
|
+
return true;
|
|
629
|
+
}
|
|
630
|
+
const SORTS = {
|
|
631
|
+
status: (a, b) => RANK[a.status] - RANK[b.status] || byLever(a, b) || idCmp(a, b),
|
|
632
|
+
id: idCmp,
|
|
633
|
+
track: (a, b) => a.track.localeCompare(b.track) || RANK[a.status] - RANK[b.status],
|
|
634
|
+
waiting: (a, b) => b.waitingOn.length - a.waitingOn.length || RANK[a.status] - RANK[b.status],
|
|
635
|
+
moved: (a, b) => (b.lastRun ?? -1) - (a.lastRun ?? -1),
|
|
636
|
+
};
|
|
637
|
+
let visibleIds = [];
|
|
638
|
+
function renderTasks() {
|
|
639
|
+
const el = document.getElementById("v-tasks");
|
|
640
|
+
const list = tasks.filter(matches).sort(SORTS[state.sort]);
|
|
641
|
+
visibleIds = list.map((t) => t.id);
|
|
642
|
+
const sel = state.sel && byId.get(state.sel);
|
|
643
|
+
const rel = new Set(sel ? [...sel.depends, ...sel.waitingOn] : []);
|
|
644
|
+
const th = (k, l, cls = "") => `<button class="${cls}" data-sort="${k}" ${state.sort === k ? 'aria-sort="descending"' : ""}>${l}${state.sort === k ? " ↓" : ""}</button>`;
|
|
645
|
+
el.innerHTML = `
|
|
646
|
+
<div class="tbar" role="toolbar" aria-label="Filters">${filterChips()}</div>
|
|
647
|
+
<div class="tsplit ${sel ? "" : "nodetail"}">
|
|
648
|
+
<section class="panel table">
|
|
649
|
+
<div class="thead"><span></span>${th("id", "ID")}${th("status", "Task")}${th("track", "Track", "c-track")}${th("waiting", "Wait", "r")}${th("moved", "Moved", "r")}</div>
|
|
650
|
+
<div class="scroll" id="tlist" role="listbox" aria-label="Tasks">
|
|
651
|
+
${list.map((t) => {
|
|
652
|
+
const r = runAt(t);
|
|
653
|
+
return `<button class="tr ${rel.has(t.id) ? "rel" : ""}" role="option"
|
|
654
|
+
data-task="${esc(t.id)}" aria-selected="${t.id === state.sel}" data-tip="${taskTip(t)}">
|
|
655
|
+
${mark(t.status)}<span class="id">${esc(t.id)}</span>
|
|
656
|
+
<span class="tt"><span>${esc(t.title)}</span>
|
|
657
|
+
${t.goal.length ? `<span class="tag opt">goal</span>` : ""}
|
|
658
|
+
${t.ready ? `<span class="tag opt">ready</span>` : ""}
|
|
659
|
+
</span><span class="tk">${esc(t.track)}</span>
|
|
660
|
+
<span class="w r ${t.waitingOn.length >= 3 ? "hot" : ""}">${t.waitingOn.length || "·"}</span>
|
|
661
|
+
<span class="u r">${r ? dayKey(r.date) : "·"}</span></button>`;
|
|
662
|
+
}).join("") || `<div class="empty">No task matches.
|
|
663
|
+
<button class="lnk" data-reset>Clear filters</button></div>`}
|
|
664
|
+
</div>
|
|
665
|
+
</section>
|
|
666
|
+
<section class="panel detail" aria-label="Task detail">${sel ? detail(sel) : ""}</section>
|
|
667
|
+
</div>`;
|
|
668
|
+
el.querySelector('#tlist [aria-selected="true"]')?.scrollIntoView({ block: "nearest" });
|
|
669
|
+
}
|
|
670
|
+
function detail(t) {
|
|
671
|
+
const r = runAt(t);
|
|
672
|
+
const list = (ids, none) => { const ts = ids.map((id) => byId.get(id)).filter(Boolean); return ts.length ? `<div style="${idw(ts)}">${ts.map((d) => trow(d, short(d.status))).join("")}</div>` : `<div class="none">${none}</div>`; };
|
|
673
|
+
const warn = warnings.filter((w) => w.startsWith(`${t.id}:`)).map((w) => w.slice(t.id.length + 1).trim());
|
|
674
|
+
const notes = [
|
|
675
|
+
...(t.status === "review" ? [
|
|
676
|
+
"<div><b>check</b>Built and tested by agents; waiting for your manual check.</div>",
|
|
677
|
+
] : []),
|
|
678
|
+
...warn.map((w) => `<div><b>warning</b>${esc(w)}</div>`),
|
|
679
|
+
];
|
|
680
|
+
return `
|
|
681
|
+
<div class="dt">
|
|
682
|
+
<div class="row1">${mark(t.status)}<span>${esc(t.id)}</span><span class="muted">${esc(lab(t.status))}</span><button class="close" data-close aria-label="Close detail" title="Close (esc)"><svg width="12" height="12" viewBox="0 0 12 12" aria-hidden="true"><path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></button></div>
|
|
683
|
+
<h3>${esc(t.title)}</h3>
|
|
684
|
+
<div class="facts">
|
|
685
|
+
<span>track <b>${esc(t.track)}</b></span>
|
|
686
|
+
${t.owner ? `<span>owner <b>${esc(t.owner)}</b></span>` : ""}
|
|
687
|
+
${t.goal.length ? `<span>goal ${t.goal.map((n) =>
|
|
688
|
+
`<button class="lnk" data-goal="${esc(n)}" title="Show tasks of goal ${esc(n)}">
|
|
689
|
+
<b>${esc(n)}</b></button>`
|
|
690
|
+
).join(", ")}</span>` : ""}
|
|
691
|
+
${t.unlocks ? `<span>unlocks <b>${esc(t.unlocks)}</b></span>` : ""}
|
|
692
|
+
${r ? `<span>moved <b>${dayKey(r.date)} ${hm(r.date)}</b></span>` : ""}
|
|
693
|
+
<span>${esc(t.file)}</span>${t.ready ? `<span class="tag">ready</span>` : ""}
|
|
694
|
+
</div>
|
|
695
|
+
${notes.length ? `<div class="notes">${notes.join("")}</div>` : ""}
|
|
696
|
+
</div>
|
|
697
|
+
<div class="scroll"><div class="ds">
|
|
698
|
+
<h4>Needs <span class="c">${t.depends.length}${t.openDeps.length ? ` · ${t.openDeps.length} not done` : ""}</span></h4>
|
|
699
|
+
${list(t.depends, "No dependencies.")}
|
|
700
|
+
<h4>Waiting on this <span class="c">${t.waitingOn.length}${t.unlocks ? ` · ${esc(t.unlocks)} ready once done` : ""}</span></h4>
|
|
701
|
+
${list(t.waitingOn, "Nothing open depends on it.")}
|
|
702
|
+
<h4>History <span class="c">${t.history.length}</span></h4>
|
|
703
|
+
${t.history.length ? `<ul class="hist">${[...t.history].reverse().map((x) => `<li><span class="when">${dayKey(runs[x.run].date)} ${hm(runs[x.run].date)}</span><span>${esc(short(x.from))} <span class="arrow">→</span> ${esc(short(x.to))}</span><button data-run="${esc(x.run)}" title="${esc(runs[x.run].subject)}">${esc(runs[x.run].commit ?? "uncommitted")}</button></li>`).join("")}</ul>` : `<div class="none">Unchanged since the first commit.</div>`}
|
|
704
|
+
</div></div>`;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// ---------- navigation ----------
|
|
708
|
+
function setView(v) {
|
|
709
|
+
state.view = v;
|
|
710
|
+
document.querySelectorAll(".tab").forEach((b) => b.setAttribute("aria-selected", String(b.dataset.view === v)));
|
|
711
|
+
document.querySelectorAll(".view").forEach((s) => s.classList.toggle("on", s.id === `v-${v}`));
|
|
712
|
+
render();
|
|
713
|
+
}
|
|
714
|
+
function render() { ({ overview: renderOverview, runs: renderRuns, tasks: renderTasks })[state.view](); }
|
|
715
|
+
function openTask(id) {
|
|
716
|
+
state.sel = id;
|
|
717
|
+
if (state.view !== "tasks") { if (!matches(byId.get(id))) clearFilters(); return setView("tasks"); }
|
|
718
|
+
if (!matches(byId.get(id))) clearFilters();
|
|
719
|
+
renderTasks();
|
|
720
|
+
}
|
|
721
|
+
function toTasks(change) { change(); if (state.view !== "tasks") { state.sel = null; setView("tasks"); } else renderTasks(); }
|
|
722
|
+
|
|
723
|
+
document.addEventListener("click", (e) => {
|
|
724
|
+
if (e.target.closest("#warnpop [data-task]")) warnPop.hidePopover();
|
|
725
|
+
const tab = e.target.closest(".tab"); if (tab) return setView(tab.dataset.view);
|
|
726
|
+
if (e.target.closest("[data-close]")) { state.sel = null; return renderTasks(); }
|
|
727
|
+
if (e.target.closest("[data-reset]")) { clearFilters(); return renderTasks(); }
|
|
728
|
+
const f = e.target.closest("[data-filter]");
|
|
729
|
+
if (f) return toTasks(() => { const k = f.dataset.filter; const inTasks = state.view === "tasks"; state.filter = inTasks && state.filter === k ? "all" : k; if (!inTasks) { state.track = null; state.goal = null; } });
|
|
730
|
+
const tr = e.target.closest("[data-track]");
|
|
731
|
+
if (tr) return toTasks(() => { const k = tr.dataset.track; const inTasks = state.view === "tasks"; state.track = inTasks && state.track === k ? null : k; if (!inTasks) { state.filter = "all"; state.goal = null; } });
|
|
732
|
+
const g = e.target.closest("[data-goal]");
|
|
733
|
+
if (g) return toTasks(() => { const k = g.dataset.goal; const inTasks = state.view === "tasks"; state.goal = inTasks && state.goal === k ? null : k; if (!inTasks) { state.filter = "all"; state.track = null; } });
|
|
734
|
+
const s = e.target.closest("[data-sort]"); if (s) { state.sort = s.dataset.sort; return renderTasks(); }
|
|
735
|
+
const run = e.target.closest("[data-run]");
|
|
736
|
+
if (run) { state.run = +run.dataset.run; return state.view === "runs" ? renderRuns() : setView("runs"); }
|
|
737
|
+
const gv = e.target.closest("[data-goview]"); if (gv) return setView(gv.dataset.goview);
|
|
738
|
+
const t = e.target.closest("[data-task]");
|
|
739
|
+
if (t) { if (state.view === "tasks" && state.sel === t.dataset.task && t.classList.contains("tr")) { state.sel = null; return renderTasks(); } return openTask(t.dataset.task); }
|
|
740
|
+
});
|
|
741
|
+
const q = document.getElementById("q");
|
|
742
|
+
q.addEventListener("input", () => { state.q = q.value.trim().toLowerCase(); if (state.view !== "tasks") setView("tasks"); else renderTasks(); });
|
|
743
|
+
document.addEventListener("keydown", (e) => {
|
|
744
|
+
const typing = e.target === q;
|
|
745
|
+
if (e.key === "Escape") {
|
|
746
|
+
if (warnPop.matches(":popover-open")) { warnPop.hidePopover(); warnBtn.focus(); return; }
|
|
747
|
+
if (typing) { if (q.value) { q.value = ""; state.q = ""; renderTasks(); } else q.blur(); return; }
|
|
748
|
+
if (state.view === "tasks" && state.sel) { state.sel = null; return renderTasks(); }
|
|
749
|
+
if (state.filter !== "all" || state.track || state.goal) { state.filter = "all"; state.track = null; state.goal = null; return render(); }
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
if (typing) { if (e.key === "Enter" && visibleIds.length) { openTask(visibleIds[0]); q.blur(); } return; }
|
|
753
|
+
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
754
|
+
if (e.key === "/") { e.preventDefault(); return q.focus(); }
|
|
755
|
+
if (["1", "2", "3"].includes(e.key)) return setView(["overview", "runs", "tasks"][+e.key - 1]);
|
|
756
|
+
const dir = { j: 1, ArrowDown: 1, k: -1, ArrowUp: -1 }[e.key];
|
|
757
|
+
if (dir) {
|
|
758
|
+
if (state.view === "tasks" && visibleIds.length) { e.preventDefault(); const i = visibleIds.indexOf(state.sel); state.sel = visibleIds[i < 0 ? 0 : Math.max(0, Math.min(visibleIds.length - 1, i + dir))]; return renderTasks(); }
|
|
759
|
+
if (state.view === "runs" && runs.length) { e.preventDefault(); state.run = Math.max(0, Math.min(lastRun, state.run - dir)); return renderRuns(); }
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
// ---------- theme ----------
|
|
764
|
+
const themeBtn = document.getElementById("theme");
|
|
765
|
+
const sun = `<svg width="15" height="15" viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="3.2" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M8 1.5v1.6M8 12.9v1.6M1.5 8h1.6M12.9 8h1.6M3.4 3.4l1.1 1.1M11.5 11.5l1.1 1.1M3.4 12.6l1.1-1.1M11.5 4.5l1.1-1.1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>`;
|
|
766
|
+
const moon = `<svg width="15" height="15" viewBox="0 0 16 16" aria-hidden="true"><path d="M13.2 10.2A5.6 5.6 0 0 1 5.8 2.8a5.6 5.6 0 1 0 7.4 7.4z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>`;
|
|
767
|
+
const isDark = () => document.documentElement.dataset.theme ? document.documentElement.dataset.theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
768
|
+
const paintTheme = () => { themeBtn.innerHTML = isDark() ? sun : moon; };
|
|
769
|
+
themeBtn.addEventListener("click", () => { document.documentElement.dataset.theme = isDark() ? "light" : "dark"; paintTheme(); });
|
|
770
|
+
paintTheme();
|
|
771
|
+
|
|
772
|
+
// ---------- tooltip ----------
|
|
773
|
+
const tip = document.getElementById("tip");
|
|
774
|
+
function showTip(el, x, y) {
|
|
775
|
+
if (!el) { tip.style.display = "none"; return; }
|
|
776
|
+
tip.innerHTML = el.dataset.tip; tip.style.display = "block";
|
|
777
|
+
const w = tip.offsetWidth, h = tip.offsetHeight;
|
|
778
|
+
tip.style.left = Math.max(8, Math.min(x + 12, innerWidth - w - 8)) + "px";
|
|
779
|
+
tip.style.top = (y + 14 + h > innerHeight ? y - h - 10 : y + 14) + "px";
|
|
780
|
+
}
|
|
781
|
+
document.addEventListener("mousemove", (e) => showTip(e.target.closest("[data-tip]"), e.clientX, e.clientY));
|
|
782
|
+
document.addEventListener("focusin", (e) => { const el = e.target.closest("[data-tip]"); if (!el || !el.matches(":focus-visible")) return showTip(null); const r = el.getBoundingClientRect(); showTip(el, r.left + 8, r.bottom - 6); });
|
|
783
|
+
document.addEventListener("focusout", () => showTip(null));
|
|
784
|
+
|
|
785
|
+
render();
|
|
786
|
+
</script>
|
|
787
|
+
</body>
|
|
788
|
+
</html>
|