super-backlog 1.2.0 → 1.3.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/dist/dashboard/data.js +76 -4
- package/dist/models/dashboard-api.js +35 -6
- package/dist/models/install.js +9 -2
- package/dist/templates/dashboard.html +224 -33
- package/package.json +1 -1
package/dist/dashboard/data.js
CHANGED
|
@@ -52,17 +52,28 @@ function normalizeAcs(value) {
|
|
|
52
52
|
}
|
|
53
53
|
return out;
|
|
54
54
|
}
|
|
55
|
+
function firstAssignee(t) {
|
|
56
|
+
const list = t['assignees'];
|
|
57
|
+
if (Array.isArray(list)) {
|
|
58
|
+
for (const entry of list) {
|
|
59
|
+
const name = asString(entry);
|
|
60
|
+
if (name !== undefined)
|
|
61
|
+
return name;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return asString(t['assignee']);
|
|
65
|
+
}
|
|
55
66
|
export function normalizeTasks(rawTasks) {
|
|
56
67
|
return rawTasks.map((t) => ({
|
|
57
68
|
id: asString(t['id']) ?? '',
|
|
58
69
|
title: asString(t['title']) ?? '(untitled)',
|
|
59
70
|
status: asString(t['status']) ?? 'Unknown',
|
|
60
71
|
priority: asString(t['priority']),
|
|
61
|
-
assignee:
|
|
62
|
-
updated: asString(t['updated_at']) ?? asString(t['updated']),
|
|
72
|
+
assignee: firstAssignee(t),
|
|
73
|
+
updated: asString(t['updatedAt']) ?? asString(t['updated_at']) ?? asString(t['updated']),
|
|
63
74
|
milestone: asString(t['milestone']),
|
|
64
75
|
description: asString(t['description']),
|
|
65
|
-
acs: normalizeAcs(t['acceptance_criteria']),
|
|
76
|
+
acs: normalizeAcs(t['acceptanceCriteria'] ?? t['acceptance_criteria']),
|
|
66
77
|
}));
|
|
67
78
|
}
|
|
68
79
|
export function computeStatuses(tasks) {
|
|
@@ -265,6 +276,67 @@ function readProjectIdentity(cwd) {
|
|
|
265
276
|
const description = asString(cfg['description']) ?? asString(pkg?.['description']) ?? '';
|
|
266
277
|
return { name, description };
|
|
267
278
|
}
|
|
279
|
+
function sectionBetween(content, begin, end) {
|
|
280
|
+
const from = content.indexOf(begin);
|
|
281
|
+
if (from === -1)
|
|
282
|
+
return undefined;
|
|
283
|
+
const to = content.indexOf(end, from + begin.length);
|
|
284
|
+
if (to === -1)
|
|
285
|
+
return undefined;
|
|
286
|
+
const text = content.slice(from + begin.length, to).trim();
|
|
287
|
+
return text === '' ? undefined : text;
|
|
288
|
+
}
|
|
289
|
+
/** Parse one backlog task markdown file via its explicit section markers. */
|
|
290
|
+
export function parseTaskFile(content) {
|
|
291
|
+
const idMatch = /^id:\s*['"]?([^'"\r\n]+)['"]?\s*$/m.exec(content);
|
|
292
|
+
const description = sectionBetween(content, '<!-- SECTION:DESCRIPTION:BEGIN -->', '<!-- SECTION:DESCRIPTION:END -->');
|
|
293
|
+
const acs = [];
|
|
294
|
+
const acBlock = sectionBetween(content, '<!-- AC:BEGIN -->', '<!-- AC:END -->');
|
|
295
|
+
if (acBlock !== undefined) {
|
|
296
|
+
for (const line of acBlock.split(/\r?\n/)) {
|
|
297
|
+
const m = /^-\s*\[( |x|X)\]\s*(?:#\d+\s*)?(.+)$/.exec(line.trim());
|
|
298
|
+
if (m)
|
|
299
|
+
acs.push({ text: m[2].trim(), checked: m[1].toLowerCase() === 'x' });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return { id: idMatch?.[1]?.trim(), description, acs };
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Fill description/ACs from backlog/tasks/*.md where `task list --json`
|
|
306
|
+
* (schemaVersion 1) does not carry them. The CLI stays the source of truth
|
|
307
|
+
* for the list and statuses; files only supply missing detail fields.
|
|
308
|
+
*/
|
|
309
|
+
export function enrichTasksFromFiles(cwd, tasks) {
|
|
310
|
+
const dir = join(cwd, 'backlog', 'tasks');
|
|
311
|
+
let files;
|
|
312
|
+
try {
|
|
313
|
+
files = readdirSync(dir).filter((f) => f.endsWith('.md'));
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return tasks;
|
|
317
|
+
}
|
|
318
|
+
const byId = new Map();
|
|
319
|
+
for (const file of files) {
|
|
320
|
+
try {
|
|
321
|
+
const detail = parseTaskFile(readFileSync(join(dir, file), 'utf8'));
|
|
322
|
+
if (detail.id !== undefined)
|
|
323
|
+
byId.set(detail.id.toUpperCase(), detail);
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
// unreadable file -> no enrichment for that task
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return tasks.map((t) => {
|
|
330
|
+
const detail = byId.get(t.id.toUpperCase());
|
|
331
|
+
if (!detail)
|
|
332
|
+
return t;
|
|
333
|
+
return {
|
|
334
|
+
...t,
|
|
335
|
+
description: t.description ?? detail.description,
|
|
336
|
+
acs: t.acs.length > 0 ? t.acs : detail.acs,
|
|
337
|
+
};
|
|
338
|
+
});
|
|
339
|
+
}
|
|
268
340
|
function readLatestVersion(home, kitVersion) {
|
|
269
341
|
try {
|
|
270
342
|
const raw = readFileSync(join(home, '.super-backlog', 'version-check.json'), 'utf8');
|
|
@@ -303,7 +375,7 @@ export function collectDashboardData(cwd, opts) {
|
|
|
303
375
|
if (res.status !== 0)
|
|
304
376
|
return base;
|
|
305
377
|
const rawTasks = parseTasksJson(res.stdout);
|
|
306
|
-
const tasks = normalizeTasks(rawTasks);
|
|
378
|
+
const tasks = enrichTasksFromFiles(cwd, normalizeTasks(rawTasks));
|
|
307
379
|
return {
|
|
308
380
|
...base,
|
|
309
381
|
tasks,
|
|
@@ -1,18 +1,47 @@
|
|
|
1
|
+
// src/models/dashboard-api.ts
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
1
4
|
import { loadConfig } from './config.js';
|
|
2
5
|
import { discoverModels } from './discovery.js';
|
|
3
|
-
|
|
6
|
+
import { writeResolvedTiers, writeRouterConfig } from './install.js';
|
|
7
|
+
function sendJson(res, status, body) {
|
|
8
|
+
res.writeHead(status, { 'content-type': 'application/json' });
|
|
9
|
+
res.end(JSON.stringify(body));
|
|
10
|
+
}
|
|
11
|
+
function routerInstalled(cwd) {
|
|
12
|
+
return existsSync(join(cwd, '.super-backlog', 'models.json'));
|
|
13
|
+
}
|
|
14
|
+
export function createModelApiHandler(cwd, deps = {}) {
|
|
15
|
+
const discover = deps.discover ?? discoverModels;
|
|
4
16
|
return async (req, res) => {
|
|
5
17
|
const url = req.url ?? '/';
|
|
6
18
|
const method = req.method ?? 'GET';
|
|
7
19
|
if (method === 'GET' && url === '/api/models') {
|
|
8
|
-
res
|
|
9
|
-
|
|
20
|
+
sendJson(res, 200, { config: loadConfig(cwd), installed: routerInstalled(cwd), status: 'ok' });
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (method === 'POST' && (url === '/api/models/enable' || url === '/api/models/disable')) {
|
|
24
|
+
const enabled = url === '/api/models/enable';
|
|
25
|
+
try {
|
|
26
|
+
writeRouterConfig(cwd, enabled);
|
|
27
|
+
sendJson(res, 200, { ok: true, config: loadConfig(cwd), installed: routerInstalled(cwd) });
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
sendJson(res, 500, { ok: false, message: err instanceof Error ? err.message : String(err) });
|
|
31
|
+
}
|
|
10
32
|
return;
|
|
11
33
|
}
|
|
12
34
|
if (method === 'POST' && url === '/api/models/discover') {
|
|
13
|
-
const result = await
|
|
14
|
-
|
|
15
|
-
|
|
35
|
+
const result = await discover(cwd);
|
|
36
|
+
if (result) {
|
|
37
|
+
try {
|
|
38
|
+
writeResolvedTiers(cwd, result);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// discovery result still returned; the modal just won't remember it
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
sendJson(res, 200, result ?? { error: 'discovery failed' });
|
|
16
45
|
return;
|
|
17
46
|
}
|
|
18
47
|
res.writeHead(404, { 'content-type': 'text/plain' });
|
package/dist/models/install.js
CHANGED
|
@@ -6,13 +6,20 @@ import { loadConfig } from './config.js';
|
|
|
6
6
|
const CONFIG_DIR = '.super-backlog';
|
|
7
7
|
const CONFIG_FILE = 'models.json';
|
|
8
8
|
export function writeRouterConfig(cwd, enabled) {
|
|
9
|
+
writeConfigPatch(cwd, { enabled });
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
/** Persist discovered tiers so the dashboard shows them across sessions. */
|
|
13
|
+
export function writeResolvedTiers(cwd, resolved) {
|
|
14
|
+
writeConfigPatch(cwd, { resolved });
|
|
15
|
+
}
|
|
16
|
+
function writeConfigPatch(cwd, patch) {
|
|
9
17
|
const dir = join(cwd, CONFIG_DIR);
|
|
10
18
|
const path = join(dir, CONFIG_FILE);
|
|
11
19
|
if (!existsSync(dir)) {
|
|
12
20
|
mkdirSync(dir, { recursive: true });
|
|
13
21
|
}
|
|
14
22
|
const current = loadConfig(cwd);
|
|
15
|
-
const next = { ...current,
|
|
23
|
+
const next = { ...current, ...patch };
|
|
16
24
|
atomicWrite(path, `${JSON.stringify(next, null, 2)}\n`);
|
|
17
|
-
return true;
|
|
18
25
|
}
|
|
@@ -107,8 +107,9 @@
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
/* ---------- Detail panel ---------- */
|
|
110
|
+
dialog { margin: auto; } /* the global reset removes the UA margin that centers dialogs */
|
|
110
111
|
#task-dialog {
|
|
111
|
-
width: min(
|
|
112
|
+
width: min(720px, 92vw); max-height: 85vh; overflow-y: auto; overflow-x: hidden; padding: 0;
|
|
112
113
|
border: 1px solid var(--line-strong); border-radius: 14px;
|
|
113
114
|
background: var(--surface); color: var(--text);
|
|
114
115
|
box-shadow: 0 24px 80px rgba(0,0,0,.55);
|
|
@@ -154,8 +155,13 @@
|
|
|
154
155
|
#backlog-close:hover { color: var(--text); border-color: var(--line); }
|
|
155
156
|
#backlog-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
156
157
|
#backlog-frame { flex: 1; width: 100%; border: 0; background: var(--bg); }
|
|
157
|
-
.dialog-content { padding:
|
|
158
|
-
.detail-head {
|
|
158
|
+
.dialog-content { padding: 0 24px 26px; }
|
|
159
|
+
.detail-head {
|
|
160
|
+
position: sticky; top: 0; z-index: 1;
|
|
161
|
+
display: flex; align-items: center; gap: 10px;
|
|
162
|
+
margin: 0 -24px 12px; padding: 16px 24px 12px;
|
|
163
|
+
background: var(--surface); border-bottom: 1px solid var(--line);
|
|
164
|
+
}
|
|
159
165
|
.detail-id { font-family: var(--mono); color: var(--accent); font-weight: 700; }
|
|
160
166
|
.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; }
|
|
161
167
|
.status-chip[data-tone="ok"] { color: var(--ok); border-color: #2b5642; background: var(--ok-bg); }
|
|
@@ -164,13 +170,31 @@
|
|
|
164
170
|
.status-chip[data-tone="danger"] { color: var(--danger); border-color: #5c2c2c; background: var(--danger-bg); }
|
|
165
171
|
.detail-close { margin-left: auto; background: none; border: none; color: var(--dim); font-size: 1.3rem; cursor: pointer; line-height: 1; }
|
|
166
172
|
.detail-close:hover { color: var(--danger); }
|
|
167
|
-
.detail-title { font-size: 1.
|
|
168
|
-
#task-dialog .detail-desc { color: var(--muted); margin-bottom:
|
|
169
|
-
#task-dialog h4 { color: var(--dim); font-size: .7rem; letter-spacing: 1.3px; text-transform: uppercase; margin:
|
|
170
|
-
.meta
|
|
171
|
-
|
|
172
|
-
|
|
173
|
+
.detail-title { font-size: 1.3rem; font-weight: 700; line-height: 1.3; margin-bottom: 10px; overflow-wrap: anywhere; }
|
|
174
|
+
#task-dialog .detail-desc { color: var(--muted); line-height: 1.6; margin-bottom: 10px; white-space: normal; }
|
|
175
|
+
#task-dialog h4 { color: var(--dim); font-size: .7rem; letter-spacing: 1.3px; text-transform: uppercase; margin: 18px 0 8px; }
|
|
176
|
+
.detail-meta {
|
|
177
|
+
display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
|
|
178
|
+
gap: 12px 18px; margin: 16px 0 4px; padding: 14px 16px;
|
|
179
|
+
border: 1px solid var(--line); border-radius: 10px; background: var(--surface-2);
|
|
180
|
+
}
|
|
181
|
+
.meta-cell .meta-label {
|
|
182
|
+
display: block; color: var(--dim); font-size: .66rem;
|
|
183
|
+
letter-spacing: 1.2px; text-transform: uppercase; margin-bottom: 4px;
|
|
184
|
+
}
|
|
185
|
+
.meta-value { font-size: .88rem; color: var(--text); overflow-wrap: anywhere; }
|
|
186
|
+
.meta-value.empty { color: var(--dim); }
|
|
187
|
+
.meta-value[data-tone="danger"] { color: var(--danger); }
|
|
188
|
+
.meta-value[data-tone="warn"] { color: var(--warn); }
|
|
189
|
+
.meta-value[data-tone="dim"] { color: var(--muted); }
|
|
190
|
+
.ac-progress { display: flex; align-items: center; gap: 12px; margin: 0 0 10px; }
|
|
191
|
+
.ac-count { font-family: var(--mono); font-size: .78rem; color: var(--muted); white-space: nowrap; }
|
|
192
|
+
.ac-bar { flex: 1; height: 4px; border-radius: 999px; background: var(--surface-2); overflow: hidden; }
|
|
193
|
+
.ac-bar-fill { height: 100%; border-radius: 999px; background: var(--ok); }
|
|
173
194
|
.dep-row { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
195
|
+
.dep-dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; margin-right: 7px; background: var(--dim); }
|
|
196
|
+
.dep-dot.done { background: var(--ok); }
|
|
197
|
+
.detail-cmd { margin-top: 18px; }
|
|
174
198
|
.dep-link {
|
|
175
199
|
font-family: var(--mono); font-size: .74rem; padding: 2px 10px; border-radius: 999px;
|
|
176
200
|
border: 1px solid #274a63; background: #10202e; color: var(--accent); cursor: pointer;
|
|
@@ -346,6 +370,29 @@
|
|
|
346
370
|
.update-badge:hover { box-shadow: 0 0 10px rgba(255,180,84,.3); }
|
|
347
371
|
.update-badge:focus-visible { outline: 2px solid var(--warn); outline-offset: 2px; }
|
|
348
372
|
.update-badge .cmd-title { font-size: inherit; font-weight: 600; color: inherit; }
|
|
373
|
+
.side-models {
|
|
374
|
+
display: block; margin: 4px 0 0; padding: 2px 0; cursor: pointer; text-align: left;
|
|
375
|
+
font: inherit; font-family: var(--mono); font-size: .78rem; color: var(--muted);
|
|
376
|
+
background: none; border: none;
|
|
377
|
+
}
|
|
378
|
+
.side-models:hover { color: var(--accent); }
|
|
379
|
+
.side-models:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
380
|
+
|
|
381
|
+
/* ---------- Models modal ---------- */
|
|
382
|
+
#models-dialog {
|
|
383
|
+
width: min(560px, 92vw); max-height: 80vh; overflow-y: auto; overflow-x: hidden; padding: 0;
|
|
384
|
+
border: 1px solid var(--line-strong); border-radius: 14px;
|
|
385
|
+
background: var(--surface); color: var(--text);
|
|
386
|
+
box-shadow: 0 24px 80px rgba(0,0,0,.55);
|
|
387
|
+
}
|
|
388
|
+
#models-dialog::backdrop { background: rgba(4,7,12,.65); backdrop-filter: blur(2px); }
|
|
389
|
+
#models-dialog[open] { animation: sbl-dialog-in .18s ease-out; }
|
|
390
|
+
@media (prefers-reduced-motion: reduce) {
|
|
391
|
+
#models-dialog[open] { animation: none; }
|
|
392
|
+
}
|
|
393
|
+
.phase-cmd:disabled { opacity: .6; cursor: default; }
|
|
394
|
+
.models-head-title { font-weight: 700; }
|
|
395
|
+
.models-row { display: flex; align-items: center; gap: 10px; margin: 14px 0; flex-wrap: wrap; }
|
|
349
396
|
|
|
350
397
|
/* ---------- Dependency flow ---------- */
|
|
351
398
|
.sub-head {
|
|
@@ -387,6 +434,7 @@
|
|
|
387
434
|
<div class="brand"><span class="brand-glyph">●</span><b>__PROJECT_NAME__</b></div>
|
|
388
435
|
<div class="kicker">SUPERPOWERS × BACKLOG.MD</div>
|
|
389
436
|
<div class="side-version" id="side-version"><span>v__KIT_VERSION__</span></div>
|
|
437
|
+
<button type="button" id="models-btn" class="side-models">model router</button>
|
|
390
438
|
<nav aria-label="Dashboard sections">
|
|
391
439
|
<a href="#sec-01"><span class="n">01</span>Board & Quick Actions</a>
|
|
392
440
|
<a href="#sec-02"><span class="n">02</span>Status</a>
|
|
@@ -482,7 +530,7 @@
|
|
|
482
530
|
</main>
|
|
483
531
|
</div>
|
|
484
532
|
|
|
485
|
-
<dialog id="task-dialog"
|
|
533
|
+
<dialog id="task-dialog"></dialog>
|
|
486
534
|
|
|
487
535
|
<dialog id="backlog-dialog" aria-label="Backlog browser">
|
|
488
536
|
<div class="backlog-dialog-bar">
|
|
@@ -492,6 +540,17 @@
|
|
|
492
540
|
</div>
|
|
493
541
|
<iframe id="backlog-frame" title="Backlog.md browser"></iframe>
|
|
494
542
|
</dialog>
|
|
543
|
+
|
|
544
|
+
<dialog id="models-dialog" aria-label="Model router">
|
|
545
|
+
<div class="dialog-content">
|
|
546
|
+
<div class="detail-head">
|
|
547
|
+
<span class="detail-id">MODELS</span>
|
|
548
|
+
<span class="models-head-title">Model router</span>
|
|
549
|
+
<button type="button" class="detail-close" id="models-close" aria-label="Close">×</button>
|
|
550
|
+
</div>
|
|
551
|
+
<div id="models-body"></div>
|
|
552
|
+
</div>
|
|
553
|
+
</dialog>
|
|
495
554
|
<div id="sbl-tip" role="tooltip" hidden></div>
|
|
496
555
|
|
|
497
556
|
<script type="application/json" id="sbl-data">__SBL_DATA_JSON__</script>
|
|
@@ -660,6 +719,89 @@
|
|
|
660
719
|
});
|
|
661
720
|
}
|
|
662
721
|
|
|
722
|
+
/* ---------- Models modal ---------- */
|
|
723
|
+
var modelsBtn = document.getElementById('models-btn');
|
|
724
|
+
var modelsDialog = document.getElementById('models-dialog');
|
|
725
|
+
var modelsBody = document.getElementById('models-body');
|
|
726
|
+
function tierCell(label, value) {
|
|
727
|
+
var c = el('div', 'meta-cell');
|
|
728
|
+
c.appendChild(el('span', 'meta-label', label));
|
|
729
|
+
c.appendChild(el('span', 'meta-value' + (value ? '' : ' empty'), value || '—'));
|
|
730
|
+
return c;
|
|
731
|
+
}
|
|
732
|
+
function actionButton(cmdLine, label, onClick) {
|
|
733
|
+
var b = el('button', 'phase-cmd');
|
|
734
|
+
b.type = 'button';
|
|
735
|
+
b.appendChild(el('span', 'cmd-line', cmdLine));
|
|
736
|
+
b.appendChild(el('span', 'cmd-title', label));
|
|
737
|
+
b.addEventListener('click', onClick);
|
|
738
|
+
return b;
|
|
739
|
+
}
|
|
740
|
+
function renderTiers(mount, tiers) {
|
|
741
|
+
mount.textContent = '';
|
|
742
|
+
if (!tiers || tiers.error || (!tiers.workhorse && !tiers.budget)) {
|
|
743
|
+
mount.appendChild(el('p', 'hint', 'Discovery failed — is the OpenCode CLI available?'));
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
mount.appendChild(tierCell('Workhorse', tiers.workhorse));
|
|
747
|
+
mount.appendChild(tierCell('Budget', tiers.budget));
|
|
748
|
+
}
|
|
749
|
+
function renderModelsDialog(info) {
|
|
750
|
+
if (!modelsBody) return;
|
|
751
|
+
modelsBody.textContent = '';
|
|
752
|
+
var enabled = !!(info.config && info.config.enabled);
|
|
753
|
+
var row = el('div', 'models-row');
|
|
754
|
+
var chip = el('span', 'status-chip', info.installed ? (enabled ? 'enabled' : 'disabled') : 'not installed');
|
|
755
|
+
if (info.installed) chip.setAttribute('data-tone', enabled ? 'ok' : 'warn');
|
|
756
|
+
row.appendChild(chip);
|
|
757
|
+
var toggle = actionButton(
|
|
758
|
+
enabled ? 'sbl models disable' : 'sbl models enable',
|
|
759
|
+
enabled ? 'disable' : 'enable',
|
|
760
|
+
function () {
|
|
761
|
+
fetch('api/models/' + (enabled ? 'disable' : 'enable'), { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' })
|
|
762
|
+
.then(function (res) { if (!res.ok) throw new Error('toggle failed'); return res.json(); })
|
|
763
|
+
.then(function (next) { renderModelsDialog(next); })
|
|
764
|
+
.catch(function () { cmdFeedback(toggle, 'failed ✗'); });
|
|
765
|
+
},
|
|
766
|
+
);
|
|
767
|
+
row.appendChild(toggle);
|
|
768
|
+
var discover = actionButton('sbl models discover', 'run', function () {
|
|
769
|
+
discover.disabled = true;
|
|
770
|
+
cmdFeedback(discover, 'running…');
|
|
771
|
+
fetch('api/models/discover', { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' })
|
|
772
|
+
.then(function (res) { return res.json(); })
|
|
773
|
+
.then(function (r) { renderTiers(document.getElementById('models-tiers'), r); })
|
|
774
|
+
.catch(function () { cmdFeedback(discover, 'failed ✗'); })
|
|
775
|
+
.then(function () { discover.disabled = false; });
|
|
776
|
+
});
|
|
777
|
+
row.appendChild(discover);
|
|
778
|
+
modelsBody.appendChild(row);
|
|
779
|
+
var tiers = el('div', 'detail-meta');
|
|
780
|
+
tiers.id = 'models-tiers';
|
|
781
|
+
var resolved = (info.config && info.config.resolved) || {};
|
|
782
|
+
tiers.appendChild(tierCell('Workhorse', resolved.workhorse));
|
|
783
|
+
tiers.appendChild(tierCell('Budget', resolved.budget));
|
|
784
|
+
modelsBody.appendChild(tiers);
|
|
785
|
+
if (!info.installed) {
|
|
786
|
+
modelsBody.appendChild(el('p', 'hint', 'Install the router first, then enable it:'));
|
|
787
|
+
var hint = actionButton('sbl init --models', 'copy', function () { copyCommand(hint, 'sbl init --models'); });
|
|
788
|
+
modelsBody.appendChild(hint);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
if (modelsBtn && modelsDialog) {
|
|
792
|
+
modelsBtn.addEventListener('click', function () {
|
|
793
|
+
fetch('api/models')
|
|
794
|
+
.then(function (res) { if (!res.ok) throw new Error('load failed'); return res.json(); })
|
|
795
|
+
.then(function (info) { renderModelsDialog(info); modelsDialog.showModal(); })
|
|
796
|
+
.catch(function () {});
|
|
797
|
+
});
|
|
798
|
+
var modelsClose = document.getElementById('models-close');
|
|
799
|
+
if (modelsClose) modelsClose.addEventListener('click', function () { modelsDialog.close(); });
|
|
800
|
+
modelsDialog.addEventListener('click', function (e) {
|
|
801
|
+
if (e.target === modelsDialog) modelsDialog.close();
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
|
|
663
805
|
/* ---------- Drafts ---------- */
|
|
664
806
|
function renderDrafts(drafts) {
|
|
665
807
|
var list = document.getElementById('drafts-list');
|
|
@@ -995,32 +1137,67 @@
|
|
|
995
1137
|
return list;
|
|
996
1138
|
}
|
|
997
1139
|
function depSection(titleText, ids) {
|
|
1140
|
+
var valid = ids.filter(function (id) { return findTask(id); });
|
|
1141
|
+
if (valid.length === 0) return null;
|
|
998
1142
|
var block = el('div');
|
|
999
1143
|
block.appendChild(el('h4', '', titleText));
|
|
1000
1144
|
var row = el('div', 'dep-row');
|
|
1001
|
-
|
|
1002
|
-
var
|
|
1145
|
+
valid.forEach(function (id) {
|
|
1146
|
+
var dep = findTask(id);
|
|
1147
|
+
var btn = el('button', 'dep-link');
|
|
1003
1148
|
btn.type = 'button';
|
|
1149
|
+
var dot = el('span', 'dep-dot' + (dep && isDoneStatus(dep.status) ? ' done' : ''));
|
|
1150
|
+
dot.setAttribute('aria-hidden', 'true');
|
|
1151
|
+
btn.appendChild(dot);
|
|
1152
|
+
btn.appendChild(document.createTextNode(id));
|
|
1004
1153
|
btn.addEventListener('click', function () { openDetail(id); });
|
|
1005
1154
|
row.appendChild(btn);
|
|
1006
1155
|
});
|
|
1007
|
-
if (row.children.length === 0) row.appendChild(el('p', 'detail-empty', 'None'));
|
|
1008
1156
|
block.appendChild(row);
|
|
1009
1157
|
return block;
|
|
1010
1158
|
}
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1159
|
+
var PRIORITY_TONE = { high: 'danger', medium: 'warn', low: 'dim' };
|
|
1160
|
+
function priorityTone(priority) {
|
|
1161
|
+
return PRIORITY_TONE[String(priority || '').toLowerCase()] || '';
|
|
1162
|
+
}
|
|
1163
|
+
function metaGrid(task) {
|
|
1164
|
+
var grid = el('div', 'detail-meta');
|
|
1165
|
+
function cell(label, value, tone) {
|
|
1166
|
+
var c = el('div', 'meta-cell');
|
|
1167
|
+
c.appendChild(el('span', 'meta-label', label));
|
|
1168
|
+
var v = el('span', 'meta-value' + (value ? '' : ' empty'), value || '—');
|
|
1169
|
+
if (value && tone) v.setAttribute('data-tone', tone);
|
|
1170
|
+
c.appendChild(v);
|
|
1171
|
+
return c;
|
|
1018
1172
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
return
|
|
1173
|
+
grid.appendChild(cell('Milestone', task.milestone));
|
|
1174
|
+
grid.appendChild(cell('Priority', task.priority, priorityTone(task.priority)));
|
|
1175
|
+
grid.appendChild(cell('Assignee', task.assignee));
|
|
1176
|
+
grid.appendChild(cell('Updated', task.updated));
|
|
1177
|
+
return grid;
|
|
1178
|
+
}
|
|
1179
|
+
function descParagraphs(text) {
|
|
1180
|
+
var frag = document.createDocumentFragment();
|
|
1181
|
+
String(text).split(/\n\s*\n/).forEach(function (para) {
|
|
1182
|
+
var trimmed = para.trim();
|
|
1183
|
+
if (trimmed !== '') frag.appendChild(el('p', 'detail-desc', trimmed));
|
|
1184
|
+
});
|
|
1185
|
+
return frag;
|
|
1186
|
+
}
|
|
1187
|
+
function acSection(task) {
|
|
1188
|
+
var frag = document.createDocumentFragment();
|
|
1189
|
+
var done = task.acs.filter(function (ac) { return ac.checked; }).length;
|
|
1190
|
+
frag.appendChild(el('h4', '', 'Acceptance criteria'));
|
|
1191
|
+
var progress = el('div', 'ac-progress');
|
|
1192
|
+
progress.appendChild(el('span', 'ac-count', done + ' / ' + task.acs.length + ' done'));
|
|
1193
|
+
var bar = el('div', 'ac-bar');
|
|
1194
|
+
var fill = el('div', 'ac-bar-fill');
|
|
1195
|
+
fill.style.width = task.acs.length > 0 ? Math.round((done / task.acs.length) * 100) + '%' : '0%';
|
|
1196
|
+
bar.appendChild(fill);
|
|
1197
|
+
progress.appendChild(bar);
|
|
1198
|
+
frag.appendChild(progress);
|
|
1199
|
+
frag.appendChild(acList(task));
|
|
1200
|
+
return frag;
|
|
1024
1201
|
}
|
|
1025
1202
|
function openDetail(id) {
|
|
1026
1203
|
var t = findTask(id);
|
|
@@ -1037,15 +1214,29 @@
|
|
|
1037
1214
|
closeBtn.addEventListener('click', closeDetail);
|
|
1038
1215
|
head.appendChild(closeBtn);
|
|
1039
1216
|
content.appendChild(head);
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
content.appendChild(
|
|
1217
|
+
var title = el('h3', 'detail-title', t.title);
|
|
1218
|
+
title.id = 'detail-title-h';
|
|
1219
|
+
dialog.setAttribute('aria-labelledby', 'detail-title-h');
|
|
1220
|
+
content.appendChild(title);
|
|
1221
|
+
if (t.description) {
|
|
1222
|
+
content.appendChild(descParagraphs(t.description));
|
|
1223
|
+
} else {
|
|
1224
|
+
content.appendChild(el('p', 'detail-desc', 'No description.'));
|
|
1046
1225
|
}
|
|
1047
|
-
content.appendChild(
|
|
1048
|
-
content.appendChild(
|
|
1226
|
+
content.appendChild(metaGrid(t));
|
|
1227
|
+
if (t.acs.length > 0) content.appendChild(acSection(t));
|
|
1228
|
+
var out = depSection('Depends on', depsOut[id] || []);
|
|
1229
|
+
if (out) content.appendChild(out);
|
|
1230
|
+
var inn = depSection('Needed by', depsIn[id] || []);
|
|
1231
|
+
if (inn) content.appendChild(inn);
|
|
1232
|
+
var cmd = el('button', 'phase-cmd detail-cmd');
|
|
1233
|
+
cmd.type = 'button';
|
|
1234
|
+
/* backlog task ids are TASK-<n>; other prefixes fall back to the raw id */
|
|
1235
|
+
var cmdLine = 'backlog task edit ' + t.id.replace(/^task-/i, '');
|
|
1236
|
+
cmd.appendChild(el('span', 'cmd-line', cmdLine));
|
|
1237
|
+
cmd.appendChild(el('span', 'cmd-title', 'copy'));
|
|
1238
|
+
cmd.addEventListener('click', function () { copyCommand(cmd, cmdLine); });
|
|
1239
|
+
content.appendChild(cmd);
|
|
1049
1240
|
dialog.textContent = '';
|
|
1050
1241
|
dialog.appendChild(content);
|
|
1051
1242
|
dialog.showModal();
|