super-backlog 0.9.0 → 1.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/README.md +5 -5
- package/dist/cli.js +32 -13
- package/dist/commands/backlog-alias.js +21 -0
- package/dist/commands/dashboard.js +36 -35
- package/dist/commands/init.js +1 -7
- package/dist/commands/serve.js +3 -0
- package/dist/commands/update.js +1 -14
- package/dist/dashboard/data.js +26 -2
- package/dist/dashboard/server.js +182 -19
- package/dist/init/execute.js +1 -32
- package/dist/init/planner.js +0 -4
- package/dist/templates/dashboard.html +214 -231
- package/package.json +1 -1
- package/dist/dashboard/regen.js +0 -28
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>__PROJECT_NAME__ · Project Dashboard</title>
|
|
7
|
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='50' r='42' fill='%235cc8ff'/%3E%3C/svg%3E">
|
|
7
8
|
<style>
|
|
8
9
|
:root {
|
|
9
10
|
--bg:#0a0e16;
|
|
@@ -26,10 +27,25 @@
|
|
|
26
27
|
--danger-bg:#331718;
|
|
27
28
|
--mono:"Cascadia Code",Consolas,"Courier New",monospace;
|
|
28
29
|
}
|
|
30
|
+
@font-face {
|
|
31
|
+
font-family: 'Inter';
|
|
32
|
+
src: local('Inter'), local('Inter-Regular');
|
|
33
|
+
font-weight: 400; font-style: normal;
|
|
34
|
+
}
|
|
35
|
+
@font-face {
|
|
36
|
+
font-family: 'Inter';
|
|
37
|
+
src: local('Inter Medium'), local('Inter-Medium');
|
|
38
|
+
font-weight: 500; font-style: normal;
|
|
39
|
+
}
|
|
40
|
+
@font-face {
|
|
41
|
+
font-family: 'Inter';
|
|
42
|
+
src: local('Inter SemiBold'), local('Inter-SemiBold');
|
|
43
|
+
font-weight: 600; font-style: normal;
|
|
44
|
+
}
|
|
29
45
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
30
46
|
html { scroll-behavior: smooth; }
|
|
31
47
|
body {
|
|
32
|
-
font-family: "Segoe UI",
|
|
48
|
+
font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
33
49
|
background:
|
|
34
50
|
radial-gradient(1100px 500px at 85% -10%, var(--bg-glow-1), transparent 60%),
|
|
35
51
|
radial-gradient(900px 500px at -10% 30%, var(--bg-glow-2), transparent 55%),
|
|
@@ -91,15 +107,22 @@
|
|
|
91
107
|
}
|
|
92
108
|
|
|
93
109
|
/* ---------- Detail panel ---------- */
|
|
94
|
-
#
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
110
|
+
#task-dialog {
|
|
111
|
+
width: min(640px, 92vw); max-height: 85vh; overflow-y: auto; padding: 0;
|
|
112
|
+
border: 1px solid var(--line-strong); border-radius: 14px;
|
|
113
|
+
background: var(--surface); color: var(--text);
|
|
114
|
+
box-shadow: 0 24px 80px rgba(0,0,0,.55);
|
|
115
|
+
}
|
|
116
|
+
#task-dialog::backdrop {
|
|
117
|
+
background: rgba(4,7,12,.65);
|
|
118
|
+
backdrop-filter: blur(2px);
|
|
119
|
+
}
|
|
120
|
+
#task-dialog[open] { animation: sbl-dialog-in .18s ease-out; }
|
|
121
|
+
@media (prefers-reduced-motion: reduce) {
|
|
122
|
+
#task-dialog[open] { animation: none; }
|
|
123
|
+
}
|
|
124
|
+
@keyframes sbl-dialog-in { from { transform: scale(.96); opacity: 0; } to { transform: none; opacity: 1; } }
|
|
125
|
+
.dialog-content { padding: 22px 24px 28px; }
|
|
103
126
|
.detail-head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
|
|
104
127
|
.detail-id { font-family: var(--mono); color: var(--accent); font-weight: 700; }
|
|
105
128
|
.status-chip { font-family: var(--mono); font-size: .68rem; padding: 2px 10px; border-radius: 999px; border: 1px solid var(--line-strong); background: var(--surface-2); color: var(--muted); white-space: nowrap; }
|
|
@@ -109,9 +132,12 @@
|
|
|
109
132
|
.status-chip[data-tone="danger"] { color: var(--danger); border-color: #5c2c2c; background: var(--danger-bg); }
|
|
110
133
|
.detail-close { margin-left: auto; background: none; border: none; color: var(--dim); font-size: 1.3rem; cursor: pointer; line-height: 1; }
|
|
111
134
|
.detail-close:hover { color: var(--danger); }
|
|
112
|
-
.detail-title { font-size: 1.
|
|
113
|
-
#
|
|
114
|
-
#
|
|
135
|
+
.detail-title { font-size: 1.18rem; font-weight: 600; margin-bottom: 8px; overflow-wrap: anywhere; }
|
|
136
|
+
#task-dialog .detail-desc { color: var(--muted); margin-bottom: 14px; white-space: normal; }
|
|
137
|
+
#task-dialog h4 { color: var(--dim); font-size: .7rem; letter-spacing: 1.3px; text-transform: uppercase; margin: 16px 0 6px; }
|
|
138
|
+
.meta-row { display: flex; flex-wrap: wrap; gap: 14px; font-size: .85rem; color: var(--muted); }
|
|
139
|
+
.meta-row span { display: flex; align-items: center; gap: 4px; }
|
|
140
|
+
.meta-label { color: var(--dim); }
|
|
115
141
|
.dep-row { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
116
142
|
.dep-link {
|
|
117
143
|
font-family: var(--mono); font-size: .74rem; padding: 2px 10px; border-radius: 999px;
|
|
@@ -137,12 +163,21 @@
|
|
|
137
163
|
|
|
138
164
|
/* ---------- Quick actions ---------- */
|
|
139
165
|
.cmd-row { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
|
|
140
|
-
.cmd {
|
|
141
|
-
|
|
166
|
+
.cmd-row { display: flex; flex-wrap: wrap; gap: 10px; }
|
|
167
|
+
.cmd-btn {
|
|
168
|
+
display: flex; flex-direction: column; align-items: flex-start; gap: 3px;
|
|
169
|
+
padding: 10px 18px; border-radius: 10px; min-width: 180px; text-align: left;
|
|
142
170
|
background: var(--surface); border: 1px solid var(--line-strong); color: var(--text);
|
|
171
|
+
cursor: pointer;
|
|
143
172
|
}
|
|
144
|
-
.cmd
|
|
173
|
+
.cmd-btn:hover { background: var(--surface-2); border-color: var(--accent); }
|
|
174
|
+
.cmd-btn:active { transform: translateY(1px); }
|
|
175
|
+
.cmd-title { font-size: .95rem; font-weight: 600; color: var(--text); }
|
|
176
|
+
.cmd-btn:hover .cmd-title { color: var(--accent); }
|
|
177
|
+
.cmd-line { font-family: var(--mono); font-size: .72rem; color: var(--dim); }
|
|
145
178
|
.hint { color: var(--dim); font-size: .8rem; margin-top: 12px; }
|
|
179
|
+
.drafts-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
|
|
180
|
+
.drafts-list li { background: var(--surface); border: 1px solid var(--line); border-radius: 8px; padding: 8px 12px; font-size: .88rem; }
|
|
146
181
|
code.inline {
|
|
147
182
|
font-family: var(--mono); font-size: .85em;
|
|
148
183
|
background: var(--surface-2); border: 1px solid var(--line);
|
|
@@ -170,14 +205,8 @@
|
|
|
170
205
|
.task-row { cursor: pointer; }
|
|
171
206
|
.task-row:hover td { background: var(--surface-2); }
|
|
172
207
|
.cell-id { color: var(--accent); font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
|
173
|
-
.cell-title { font-weight:
|
|
208
|
+
.cell-title { font-weight: 400; white-space: normal; min-width: 220px; }
|
|
174
209
|
.cell-updated { font-family: var(--mono); color: var(--muted); }
|
|
175
|
-
.task-detail td { white-space: normal; background: var(--surface-2); border-top: none; }
|
|
176
|
-
.detail-desc { margin: 0 0 8px; color: var(--muted); }
|
|
177
|
-
.detail-empty { margin: 0; color: var(--dim); }
|
|
178
|
-
.ac-list { margin: 0; padding: 0; list-style: none; }
|
|
179
|
-
.ac { display: flex; align-items: baseline; gap: 8px; padding: 2px 0; }
|
|
180
|
-
.ac.done span { text-decoration: line-through; color: var(--dim); }
|
|
181
210
|
|
|
182
211
|
/* ---------- Footer ---------- */
|
|
183
212
|
.footer {
|
|
@@ -234,24 +263,26 @@
|
|
|
234
263
|
.step-label b { display: block; color: var(--text); font-size: .71rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
235
264
|
.step.gate .step-label b { color: var(--warn); }
|
|
236
265
|
|
|
237
|
-
/* ---------- Dependency
|
|
266
|
+
/* ---------- Dependency flow ---------- */
|
|
238
267
|
.sub-head {
|
|
239
268
|
font-size: .78rem; letter-spacing: 1.4px; text-transform: uppercase;
|
|
240
269
|
color: var(--dim); margin: 30px 0 12px; font-weight: 600;
|
|
241
270
|
}
|
|
242
|
-
.
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
.
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
.
|
|
251
|
-
.
|
|
252
|
-
.
|
|
253
|
-
.
|
|
254
|
-
.
|
|
271
|
+
.flow-wrap { display: grid; gap: 20px; }
|
|
272
|
+
.flow-block { background: var(--surface); border: 1px solid var(--line); border-radius: 12px; padding: 14px 16px 18px; }
|
|
273
|
+
.flow-block h4 { margin: 0 0 10px; color: var(--text); font-size: .82rem; font-weight: 500; letter-spacing: .4px; }
|
|
274
|
+
.flow-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
|
|
275
|
+
.flow-card {
|
|
276
|
+
background: var(--surface-2); border: 1px solid var(--line-strong); border-radius: 10px;
|
|
277
|
+
padding: 10px 12px; cursor: pointer; transition: background .12s ease, border-color .12s ease;
|
|
278
|
+
}
|
|
279
|
+
.flow-card:hover { background: var(--surface); border-color: var(--accent); }
|
|
280
|
+
.flow-card-head { display: flex; align-items: center; gap: 10px; margin-bottom: 5px; }
|
|
281
|
+
.flow-card-id { font-family: var(--mono); color: var(--accent); font-weight: 700; font-size: .78rem; }
|
|
282
|
+
.flow-card-title { color: var(--text); font-size: .92rem; }
|
|
283
|
+
.flow-card-blockers { margin-top: 7px; font-size: .8rem; color: var(--dim); }
|
|
284
|
+
.flow-card-blockers .dep-link { margin-left: 4px; }
|
|
285
|
+
.flow-empty { color: var(--dim); font-size: .85rem; margin: 0; }
|
|
255
286
|
|
|
256
287
|
@media (max-width:900px) {
|
|
257
288
|
.layout { grid-template-columns: 1fr; }
|
|
@@ -293,13 +324,17 @@
|
|
|
293
324
|
<section id="sec-01">
|
|
294
325
|
<div class="sec-head"><span class="sec-num">01</span><h2>Board & Quick Actions</h2><span class="tagline">commands · board mirror only</span></div>
|
|
295
326
|
<div id="quickactions" class="mount">
|
|
296
|
-
<div class="cmd-row">
|
|
297
|
-
<
|
|
298
|
-
<
|
|
299
|
-
<
|
|
327
|
+
<div class="cmd-row" id="cmd-buttons">
|
|
328
|
+
<button type="button" class="cmd-btn" data-cmd="browser"><span class="cmd-title">Backlog Browser</span><span class="cmd-line">backlog browser</span></button>
|
|
329
|
+
<button type="button" class="cmd-btn" data-cmd="board"><span class="cmd-title">Backlog Board</span><span class="cmd-line">backlog board</span></button>
|
|
330
|
+
<button type="button" class="cmd-btn" data-copy="sbl dashboard"><span class="cmd-title">Live Dashboard</span><span class="cmd-line">sbl dashboard</span></button>
|
|
300
331
|
</div>
|
|
301
332
|
<p class="hint">The board mirrors Backlog.md — it never writes execution state.</p>
|
|
302
333
|
</div>
|
|
334
|
+
<div id="drafts" class="mount">
|
|
335
|
+
<h3 class="sub-head">Drafts</h3>
|
|
336
|
+
<ul id="drafts-list" class="drafts-list"></ul>
|
|
337
|
+
</div>
|
|
303
338
|
</section>
|
|
304
339
|
|
|
305
340
|
<section id="sec-02">
|
|
@@ -313,7 +348,7 @@
|
|
|
313
348
|
</section>
|
|
314
349
|
|
|
315
350
|
<section id="sec-04">
|
|
316
|
-
<div class="sec-head"><span class="sec-num">04</span><h2>Tasks</h2><span class="tagline">sort · filter ·
|
|
351
|
+
<div class="sec-head"><span class="sec-num">04</span><h2>Tasks</h2><span class="tagline">sort · filter · click row for details</span></div>
|
|
317
352
|
<div id="tasks" class="mount">
|
|
318
353
|
<div class="toolbar">
|
|
319
354
|
<input id="taskfilter" type="search" placeholder="Filter tasks…" aria-label="Filter tasks">
|
|
@@ -340,7 +375,7 @@
|
|
|
340
375
|
<section id="sec-05">
|
|
341
376
|
<div class="sec-head"><span class="sec-num">05</span><h2>Feature Cycle</h2><span class="tagline">idea → merge · every <span class="term" data-term="Review Gate">Review Gate</span> included</span></div>
|
|
342
377
|
<div id="stepper" class="mount"></div>
|
|
343
|
-
<h3 class="sub-head">
|
|
378
|
+
<h3 class="sub-head">Flow</h3>
|
|
344
379
|
<div id="depgraph" class="mount"></div>
|
|
345
380
|
</section>
|
|
346
381
|
|
|
@@ -357,14 +392,13 @@
|
|
|
357
392
|
|
|
358
393
|
<footer class="footer">
|
|
359
394
|
Generated __GENERATED_AT__ · super-backlog v__KIT_VERSION__ · <span id="source-badge"></span> ·
|
|
360
|
-
|
|
395
|
+
served live by <code class="inline">sbl dashboard</code> — reloads automatically while the server is running.
|
|
361
396
|
</footer>
|
|
362
397
|
|
|
363
398
|
</main>
|
|
364
399
|
</div>
|
|
365
400
|
|
|
366
|
-
<
|
|
367
|
-
<aside id="sbl-detail" hidden aria-label="Task details"></aside>
|
|
401
|
+
<dialog id="task-dialog" aria-label="Task details"></dialog>
|
|
368
402
|
<div id="sbl-tip" role="tooltip" hidden></div>
|
|
369
403
|
|
|
370
404
|
<script type="application/json" id="sbl-data">__SBL_DATA_JSON__</script>
|
|
@@ -425,37 +459,12 @@
|
|
|
425
459
|
}
|
|
426
460
|
rows.forEach(function (task) {
|
|
427
461
|
var tr = el('tr', 'task-row');
|
|
462
|
+
tr.setAttribute('data-task', task.id);
|
|
428
463
|
KEYS.forEach(function (k) {
|
|
429
464
|
tr.appendChild(el('td', 'cell-' + k, field(task, k)));
|
|
430
465
|
});
|
|
431
|
-
|
|
432
|
-
var td = el('td');
|
|
433
|
-
td.colSpan = KEYS.length;
|
|
434
|
-
if (task.description) td.appendChild(el('p', 'detail-desc', task.description));
|
|
435
|
-
if (task.acs.length > 0) {
|
|
436
|
-
var list = el('ul', 'ac-list');
|
|
437
|
-
task.acs.forEach(function (ac) {
|
|
438
|
-
var li = el('li', ac.checked ? 'ac done' : 'ac');
|
|
439
|
-
var box = document.createElement('input');
|
|
440
|
-
box.type = 'checkbox';
|
|
441
|
-
box.checked = ac.checked;
|
|
442
|
-
box.disabled = true;
|
|
443
|
-
li.appendChild(box);
|
|
444
|
-
li.appendChild(document.createTextNode(ac.text));
|
|
445
|
-
list.appendChild(li);
|
|
446
|
-
});
|
|
447
|
-
td.appendChild(list);
|
|
448
|
-
} else {
|
|
449
|
-
td.appendChild(el('p', 'detail-empty', 'No acceptance criteria.'));
|
|
450
|
-
}
|
|
451
|
-
detail.style.display = 'none';
|
|
452
|
-
tr.addEventListener('click', function () {
|
|
453
|
-
var opening = detail.style.display === 'none';
|
|
454
|
-
detail.style.display = opening ? '' : 'none';
|
|
455
|
-
tr.classList.toggle('expanded', opening);
|
|
456
|
-
});
|
|
466
|
+
tr.addEventListener('click', function () { openDetail(task.id); });
|
|
457
467
|
tbody.appendChild(tr);
|
|
458
|
-
tbody.appendChild(detail);
|
|
459
468
|
});
|
|
460
469
|
}
|
|
461
470
|
document.querySelectorAll('#tasks-table th').forEach(function (th) {
|
|
@@ -499,6 +508,40 @@
|
|
|
499
508
|
});
|
|
500
509
|
renderTasks();
|
|
501
510
|
|
|
511
|
+
/* ---------- Quick action buttons ---------- */
|
|
512
|
+
function cmdFeedback(btn, text) {
|
|
513
|
+
var title = btn.querySelector('.cmd-title');
|
|
514
|
+
if (!title || btn.getAttribute('data-busy')) return;
|
|
515
|
+
btn.setAttribute('data-busy', '1');
|
|
516
|
+
var orig = title.textContent;
|
|
517
|
+
title.textContent = text;
|
|
518
|
+
setTimeout(function () { title.textContent = orig; btn.removeAttribute('data-busy'); }, 1200);
|
|
519
|
+
}
|
|
520
|
+
function copyCommand(btn, command) {
|
|
521
|
+
if (navigator.clipboard) navigator.clipboard.writeText(command).catch(function () {});
|
|
522
|
+
cmdFeedback(btn, 'copied \u2713');
|
|
523
|
+
}
|
|
524
|
+
document.querySelectorAll('#cmd-buttons .cmd-btn').forEach(function (b) {
|
|
525
|
+
b.addEventListener('click', function () {
|
|
526
|
+
var copy = b.getAttribute('data-copy');
|
|
527
|
+
if (copy) { copyCommand(b, copy); return; }
|
|
528
|
+
var cmd = b.getAttribute('data-cmd');
|
|
529
|
+
fetch('/api/run', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ command: cmd }) })
|
|
530
|
+
.then(function (res) { if (!res.ok) throw new Error('run failed'); cmdFeedback(b, 'started \u2713'); })
|
|
531
|
+
.catch(function () {});
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
/* ---------- Drafts ---------- */
|
|
536
|
+
function renderDrafts(drafts) {
|
|
537
|
+
var list = document.getElementById('drafts-list');
|
|
538
|
+
if (!list) return;
|
|
539
|
+
list.textContent = '';
|
|
540
|
+
if (!drafts || drafts.length === 0) { list.appendChild(el('li', '', 'No drafts.')); return; }
|
|
541
|
+
drafts.forEach(function (d) { list.appendChild(el('li', '', d.id + ' \u2014 ' + d.title)); });
|
|
542
|
+
}
|
|
543
|
+
renderDrafts(data.drafts);
|
|
544
|
+
|
|
502
545
|
/* ---------- Shared diagram helpers ---------- */
|
|
503
546
|
var SVGNS = 'http://www.w3.org/2000/svg';
|
|
504
547
|
var TONE_VAR = { ok: 'var(--ok)', accent: 'var(--accent)', warn: 'var(--warn)', danger: 'var(--danger)', dim: 'var(--dim)' };
|
|
@@ -750,9 +793,8 @@
|
|
|
750
793
|
document.addEventListener('focusout', tipHide);
|
|
751
794
|
}
|
|
752
795
|
|
|
753
|
-
/* ---------- Detail
|
|
754
|
-
var
|
|
755
|
-
var backdrop = document.getElementById('sbl-backdrop');
|
|
796
|
+
/* ---------- Detail dialog ---------- */
|
|
797
|
+
var dialog = document.getElementById('task-dialog');
|
|
756
798
|
var depsOut = {};
|
|
757
799
|
var depsIn = {};
|
|
758
800
|
data.deps.forEach(function (d) {
|
|
@@ -766,11 +808,13 @@
|
|
|
766
808
|
});
|
|
767
809
|
return found;
|
|
768
810
|
}
|
|
811
|
+
function isDoneStatus(status) {
|
|
812
|
+
var s = String(status).toLowerCase();
|
|
813
|
+
return s === 'done' || s === 'complete' || s === 'completed';
|
|
814
|
+
}
|
|
769
815
|
function closeDetail() {
|
|
770
|
-
if (
|
|
771
|
-
if (backdrop) backdrop.hidden = true;
|
|
816
|
+
if (dialog && dialog.open) dialog.close();
|
|
772
817
|
}
|
|
773
|
-
window.closeDetail = closeDetail;
|
|
774
818
|
function acList(task) {
|
|
775
819
|
var list = el('ul', 'ac-list');
|
|
776
820
|
task.acs.forEach(function (ac) {
|
|
@@ -792,7 +836,6 @@
|
|
|
792
836
|
ids.filter(function (id) { return findTask(id); }).forEach(function (id) {
|
|
793
837
|
var btn = el('button', 'dep-link', id);
|
|
794
838
|
btn.type = 'button';
|
|
795
|
-
btn.setAttribute('data-dep', id);
|
|
796
839
|
btn.addEventListener('click', function () { openDetail(id); });
|
|
797
840
|
row.appendChild(btn);
|
|
798
841
|
});
|
|
@@ -800,10 +843,24 @@
|
|
|
800
843
|
block.appendChild(row);
|
|
801
844
|
return block;
|
|
802
845
|
}
|
|
846
|
+
function metaRow(task) {
|
|
847
|
+
var row = el('div', 'meta-row');
|
|
848
|
+
function item(label, value) {
|
|
849
|
+
var s = el('span');
|
|
850
|
+
s.appendChild(el('span', 'meta-label', label + ': '));
|
|
851
|
+
s.appendChild(document.createTextNode(value || '—'));
|
|
852
|
+
return s;
|
|
853
|
+
}
|
|
854
|
+
row.appendChild(item('Milestone', task.milestone));
|
|
855
|
+
row.appendChild(item('Priority', task.priority));
|
|
856
|
+
row.appendChild(item('Assignee', task.assignee));
|
|
857
|
+
row.appendChild(item('Updated', task.updated));
|
|
858
|
+
return row;
|
|
859
|
+
}
|
|
803
860
|
function openDetail(id) {
|
|
804
861
|
var t = findTask(id);
|
|
805
|
-
if (!t || !
|
|
806
|
-
|
|
862
|
+
if (!t || !dialog) return;
|
|
863
|
+
var content = el('div', 'dialog-content');
|
|
807
864
|
var head = el('div', 'detail-head');
|
|
808
865
|
head.appendChild(el('span', 'detail-id', t.id));
|
|
809
866
|
var chip = el('span', 'status-chip', t.status);
|
|
@@ -814,21 +871,22 @@
|
|
|
814
871
|
closeBtn.setAttribute('aria-label', 'Close details');
|
|
815
872
|
closeBtn.addEventListener('click', closeDetail);
|
|
816
873
|
head.appendChild(closeBtn);
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
874
|
+
content.appendChild(head);
|
|
875
|
+
content.appendChild(el('h3', 'detail-title', t.title));
|
|
876
|
+
content.appendChild(el('p', 'detail-desc', t.description || 'No description.'));
|
|
877
|
+
content.appendChild(metaRow(t));
|
|
820
878
|
if (t.acs.length > 0) {
|
|
821
|
-
|
|
822
|
-
|
|
879
|
+
content.appendChild(el('h4', '', 'Acceptance criteria'));
|
|
880
|
+
content.appendChild(acList(t));
|
|
823
881
|
}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
882
|
+
content.appendChild(depSection('Depends on', depsOut[id] || []));
|
|
883
|
+
content.appendChild(depSection('Needed by', depsIn[id] || []));
|
|
884
|
+
dialog.textContent = '';
|
|
885
|
+
dialog.appendChild(content);
|
|
886
|
+
dialog.showModal();
|
|
829
887
|
}
|
|
830
888
|
window.__sblOpenDetail = openDetail;
|
|
831
|
-
if (
|
|
889
|
+
if (dialog) dialog.addEventListener('click', function (ev) { if (ev.target === dialog) closeDetail(); });
|
|
832
890
|
|
|
833
891
|
/* ---------- Keyboard shortcuts ---------- */
|
|
834
892
|
document.addEventListener('keydown', function (ev) {
|
|
@@ -847,154 +905,79 @@
|
|
|
847
905
|
}
|
|
848
906
|
});
|
|
849
907
|
|
|
850
|
-
/* ---------- Dependency
|
|
851
|
-
function
|
|
852
|
-
var nodeSet = {};
|
|
853
|
-
nodes.forEach(function (n) { nodeSet[n] = true; });
|
|
854
|
-
var prereqs = {};
|
|
855
|
-
nodes.forEach(function (n) { prereqs[n] = []; });
|
|
856
|
-
deps.forEach(function (d) {
|
|
857
|
-
if (!nodeSet[d.from] || !nodeSet[d.to]) return; // dangling ref
|
|
858
|
-
if (prereqs[d.from].indexOf(d.to) === -1) prereqs[d.from].push(d.to);
|
|
859
|
-
});
|
|
860
|
-
var layers = {};
|
|
861
|
-
var remaining = {};
|
|
862
|
-
nodes.forEach(function (n) { remaining[n] = true; });
|
|
863
|
-
var left = nodes.length;
|
|
864
|
-
var current = 0;
|
|
865
|
-
function take(n) {
|
|
866
|
-
layers[n] = current;
|
|
867
|
-
delete remaining[n];
|
|
868
|
-
left -= 1;
|
|
869
|
-
}
|
|
870
|
-
while (left > 0) {
|
|
871
|
-
var ready = nodes.filter(function (n) {
|
|
872
|
-
if (!remaining[n]) return false;
|
|
873
|
-
return prereqs[n].every(function (p) { return !remaining[p]; });
|
|
874
|
-
});
|
|
875
|
-
current += 1;
|
|
876
|
-
if (ready.length === 0) {
|
|
877
|
-
// stalled behind a cycle: snapshot self-reachable members first, then
|
|
878
|
-
// append them as one group (mirrors cycleMembers() in layering.ts)
|
|
879
|
-
var cycleSnap = [];
|
|
880
|
-
nodes.forEach(function (start) {
|
|
881
|
-
if (!remaining[start]) return;
|
|
882
|
-
var seen = {};
|
|
883
|
-
var stack = prereqs[start].filter(function (p) { return remaining[p]; });
|
|
884
|
-
var found = false;
|
|
885
|
-
while (stack.length > 0 && !found) {
|
|
886
|
-
var cur = stack.pop();
|
|
887
|
-
if (cur === start) { found = true; break; }
|
|
888
|
-
if (seen[cur]) continue;
|
|
889
|
-
seen[cur] = true;
|
|
890
|
-
stack = stack.concat(prereqs[cur].filter(function (p) { return remaining[p]; }));
|
|
891
|
-
}
|
|
892
|
-
if (found) cycleSnap.push(start);
|
|
893
|
-
});
|
|
894
|
-
cycleSnap.forEach(take);
|
|
895
|
-
if (cycleSnap.length === 0) break; // unreachable safety net
|
|
896
|
-
continue;
|
|
897
|
-
}
|
|
898
|
-
ready.forEach(take);
|
|
899
|
-
}
|
|
900
|
-
var out = {};
|
|
901
|
-
nodes.forEach(function (n) { out[n] = layers[n] || 1; });
|
|
902
|
-
return out;
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
function hotEdges(id, on) {
|
|
906
|
-
document.querySelectorAll('.depgraph .edge').forEach(function (edge) {
|
|
907
|
-
var hit = edge.getAttribute('data-from') === id || edge.getAttribute('data-to') === id;
|
|
908
|
-
edge.classList.toggle('hot', on && hit);
|
|
909
|
-
});
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
function renderDepGraph(mount, tasks, deps) {
|
|
908
|
+
/* ---------- Dependency flow ---------- */
|
|
909
|
+
function renderFlow(mount, tasks) {
|
|
913
910
|
mount.textContent = '';
|
|
914
|
-
if (tasks.length === 0) {
|
|
915
|
-
mount.appendChild(el('p', 'hint', 'No tasks to graph.'));
|
|
916
|
-
return;
|
|
917
|
-
}
|
|
918
|
-
var ids = tasks.map(function (t) { return t.id; });
|
|
919
|
-
var layers = assignLayers(ids, deps);
|
|
920
911
|
var byId = {};
|
|
921
912
|
tasks.forEach(function (t) { byId[t.id] = t; });
|
|
922
|
-
var
|
|
923
|
-
|
|
924
|
-
var
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
cols.forEach(function (c) { if (c.length > rows) rows = c.length; });
|
|
930
|
-
var width = PAD * 2 + cols.length * CHIP_W + (cols.length - 1) * GAP_X;
|
|
931
|
-
var height = PAD * 2 + Math.max(rows, 1) * CHIP_H + (Math.max(rows, 1) - 1) * GAP_Y;
|
|
932
|
-
var wrap = el('div', 'depgraph-wrap');
|
|
933
|
-
var svg = svgEl('svg', {
|
|
934
|
-
viewBox: '0 0 ' + width + ' ' + height, width: width, height: height,
|
|
935
|
-
'class': 'depgraph', role: 'img', 'aria-label': 'Task dependency graph'
|
|
936
|
-
});
|
|
937
|
-
var defs = svgEl('defs');
|
|
938
|
-
var marker = svgEl('marker', { id: 'sbl-arrow', markerWidth: '9', markerHeight: '7', refX: '8.5', refY: '3.5', orient: 'auto' });
|
|
939
|
-
marker.appendChild(svgEl('polygon', { points: '0 0, 9 3.5, 0 7', fill: 'var(--line-strong)' }));
|
|
940
|
-
defs.appendChild(marker);
|
|
941
|
-
svg.appendChild(defs);
|
|
942
|
-
var pos = {};
|
|
943
|
-
cols.forEach(function (col, ci) {
|
|
944
|
-
col.forEach(function (t, ri) {
|
|
945
|
-
pos[t.id] = {
|
|
946
|
-
x: PAD + ci * (CHIP_W + GAP_X),
|
|
947
|
-
y: PAD + ri * (CHIP_H + GAP_Y)
|
|
948
|
-
};
|
|
949
|
-
});
|
|
950
|
-
});
|
|
951
|
-
deps.forEach(function (d) {
|
|
952
|
-
if (!pos[d.from] || !pos[d.to] || d.from === d.to) return;
|
|
953
|
-
var a = pos[d.from];
|
|
954
|
-
var b = pos[d.to];
|
|
955
|
-
var x1 = a.x + CHIP_W, y1 = a.y + CHIP_H / 2, x2 = b.x, y2 = b.y + CHIP_H / 2;
|
|
956
|
-
var mx = (x1 + x2) / 2;
|
|
957
|
-
var path = svgEl('path', {
|
|
958
|
-
'class': 'edge', 'data-from': d.from, 'data-to': d.to,
|
|
959
|
-
d: 'M ' + x1 + ' ' + y1 + ' C ' + mx + ' ' + y1 + ', ' + mx + ' ' + y2 + ', ' + x2 + ' ' + y2,
|
|
960
|
-
'marker-end': 'url(#sbl-arrow)'
|
|
913
|
+
var pending = tasks.filter(function (t) { return !isDoneStatus(t.status); });
|
|
914
|
+
var upNext = [];
|
|
915
|
+
var blocked = [];
|
|
916
|
+
pending.forEach(function (t) {
|
|
917
|
+
var blockers = (depsOut[t.id] || []).filter(function (id) {
|
|
918
|
+
var d = byId[id];
|
|
919
|
+
return d && !isDoneStatus(d.status);
|
|
961
920
|
});
|
|
962
|
-
|
|
921
|
+
if (blockers.length === 0) upNext.push({ task: t, blockers: [] });
|
|
922
|
+
else blocked.push({ task: t, blockers: blockers });
|
|
963
923
|
});
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
924
|
+
function section(titleText, items, emptyText) {
|
|
925
|
+
var wrap = el('div', 'flow-block');
|
|
926
|
+
wrap.appendChild(el('h4', '', titleText));
|
|
927
|
+
if (items.length === 0) {
|
|
928
|
+
wrap.appendChild(el('p', 'flow-empty', emptyText));
|
|
929
|
+
} else {
|
|
930
|
+
var list = el('ul', 'flow-list');
|
|
931
|
+
items.forEach(function (item) {
|
|
932
|
+
var li = el('li', 'flow-card');
|
|
933
|
+
var head = el('div', 'flow-card-head');
|
|
934
|
+
head.appendChild(el('span', 'flow-card-id', item.task.id));
|
|
935
|
+
var chip = el('span', 'status-chip', item.task.status);
|
|
936
|
+
chip.setAttribute('data-tone', toneOf(item.task.status));
|
|
937
|
+
head.appendChild(chip);
|
|
938
|
+
li.appendChild(head);
|
|
939
|
+
li.appendChild(el('div', 'flow-card-title', item.task.title));
|
|
940
|
+
if (item.blockers.length > 0) {
|
|
941
|
+
var blockRow = el('div', 'flow-card-blockers');
|
|
942
|
+
blockRow.appendChild(document.createTextNode('Blocked by: '));
|
|
943
|
+
item.blockers.forEach(function (id, i) {
|
|
944
|
+
if (i > 0) blockRow.appendChild(document.createTextNode(', '));
|
|
945
|
+
var btn = el('button', 'dep-link', id);
|
|
946
|
+
btn.type = 'button';
|
|
947
|
+
btn.addEventListener('click', function () { openDetail(id); });
|
|
948
|
+
blockRow.appendChild(btn);
|
|
949
|
+
});
|
|
950
|
+
li.appendChild(blockRow);
|
|
988
951
|
}
|
|
952
|
+
li.addEventListener('click', function (ev) {
|
|
953
|
+
if (ev.target.closest('.dep-link')) return;
|
|
954
|
+
openDetail(item.task.id);
|
|
955
|
+
});
|
|
956
|
+
list.appendChild(li);
|
|
989
957
|
});
|
|
990
|
-
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
|
|
958
|
+
wrap.appendChild(list);
|
|
959
|
+
}
|
|
960
|
+
return wrap;
|
|
961
|
+
}
|
|
962
|
+
if (upNext.length === 0 && blocked.length === 0) {
|
|
963
|
+
mount.appendChild(el('p', 'hint', 'No pending tasks — everything is done.'));
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
var wrap = el('div', 'flow-wrap');
|
|
967
|
+
wrap.appendChild(section('Up Next', upNext, 'No tasks ready to start.'));
|
|
968
|
+
wrap.appendChild(section('Blocked', blocked, 'No blocked tasks.'));
|
|
994
969
|
mount.appendChild(wrap);
|
|
995
970
|
}
|
|
996
971
|
|
|
997
|
-
|
|
972
|
+
renderFlow($('#depgraph'), data.tasks);
|
|
973
|
+
})();
|
|
974
|
+
</script>
|
|
975
|
+
<script>
|
|
976
|
+
(function () {
|
|
977
|
+
if (!window.EventSource || (location.protocol !== 'http:' && location.protocol !== 'https:')) return;
|
|
978
|
+
var es = new EventSource('/api/events');
|
|
979
|
+
es.addEventListener('reload', function () { location.reload(); });
|
|
980
|
+
es.addEventListener('error', function () {});
|
|
998
981
|
})();
|
|
999
982
|
</script>
|
|
1000
983
|
</body>
|
package/package.json
CHANGED
package/dist/dashboard/regen.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
// src/dashboard/regen.ts
|
|
2
|
-
// Standalone regeneration entry invoked by the post-commit freshness hook:
|
|
3
|
-
// node "<repo>/node_modules/super-backlog/dist/dashboard/regen.js"
|
|
4
|
-
// Contract: never throw, never exit non-zero - a broken dashboard generation
|
|
5
|
-
// must never block or fail a user's commit. All errors go to console.error.
|
|
6
|
-
import { join, resolve } from 'node:path';
|
|
7
|
-
import { pathToFileURL } from 'node:url';
|
|
8
|
-
import process from 'node:process';
|
|
9
|
-
import { collectDashboardData } from './data.js';
|
|
10
|
-
import { renderDashboard } from './render.js';
|
|
11
|
-
import { atomicWrite } from '../lib/atomic.js';
|
|
12
|
-
import { KIT_VERSION } from '../lib/version.js';
|
|
13
|
-
/** Regenerate <cwd>/dashboard.html. Throws on failure; the direct-run wrapper swallows. */
|
|
14
|
-
export function regenerateDashboard(cwd) {
|
|
15
|
-
const html = renderDashboard(collectDashboardData(cwd, { kitVersion: KIT_VERSION }));
|
|
16
|
-
atomicWrite(join(cwd, 'dashboard.html'), html);
|
|
17
|
-
}
|
|
18
|
-
const invokedDirectly = process.argv[1] !== undefined &&
|
|
19
|
-
import.meta.url === pathToFileURL(resolve(process.argv[1])).href;
|
|
20
|
-
if (invokedDirectly) {
|
|
21
|
-
try {
|
|
22
|
-
regenerateDashboard(process.cwd());
|
|
23
|
-
}
|
|
24
|
-
catch (err) {
|
|
25
|
-
console.error(err instanceof Error ? (err.stack ?? err.message) : err);
|
|
26
|
-
}
|
|
27
|
-
process.exit(0); // always - success or failure
|
|
28
|
-
}
|