@yemi33/minions 0.1.2144 → 0.1.2145
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/bin/minions.js +85 -0
- package/bin/minions.js.rej +16 -0
- package/dashboard/js/refresh.js +14 -0
- package/dashboard/js/render-pinned.js +119 -3
- package/dashboard/js/utils.js +20 -0
- package/dashboard/layout.html +1 -0
- package/dashboard/slim/body.html +113 -1
- package/dashboard/slim/body.html.rej +11 -0
- package/dashboard/slim/js/command-send.js.rej +12 -0
- package/dashboard/slim/js/helpers.js +9 -0
- package/dashboard/slim/js/history.js +153 -88
- package/dashboard/slim/js/history.js.rej +26 -0
- package/dashboard/slim/js/modals-tiles.js +8 -2
- package/dashboard/slim/js/pinned.js +182 -0
- package/dashboard/slim/js/settings.js +126 -6
- package/dashboard/slim/js/status.js +9 -6
- package/dashboard/slim/layout.html +1 -0
- package/dashboard/slim/styles.css +77 -2
- package/dashboard/slim/styles.css.rej +124 -0
- package/dashboard/styles.css +19 -0
- package/dashboard-build.js +9 -2
- package/dashboard.js +44 -1
- package/docs/README.md.rej +9 -0
- package/docs/auto-discovery.md +2 -2
- package/docs/constellation-style-telemetry.md +161 -0
- package/docs/engine-restart.md +1 -1
- package/docs/kb-sweep.md +2 -2
- package/docs/managed-spawn.md +1 -1
- package/docs/watches.md +11 -11
- package/engine/cli.js +57 -12
- package/engine/features.js +11 -0
- package/engine/shared.js +173 -36
- package/engine/watchdog.js +458 -0
- package/package.json +1 -1
|
@@ -1,31 +1,18 @@
|
|
|
1
1
|
// Completions Card helpers (W-betterHistory) — see the CSS block of the
|
|
2
2
|
// same name in dashboard/slim.html for the component layout reference.
|
|
3
3
|
// ─────────────────────────────────────────────────
|
|
4
|
-
// The type chip
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
'
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'plan': '📋',
|
|
16
|
-
'plan-to-prd': '📐',
|
|
17
|
-
'decompose': '🪓',
|
|
18
|
-
'meeting': '💬',
|
|
19
|
-
'explore': '🔍',
|
|
20
|
-
'ask': '❓',
|
|
21
|
-
'docs': '📝',
|
|
22
|
-
'setup': '⚙️',
|
|
23
|
-
'qa-validate': '🎯',
|
|
4
|
+
// The type chip renders a monochrome Fluent System Icon via CSS mask (see the
|
|
5
|
+
// .completions-card-type-chip[data-type="…"] rules in styles.css), keyed on
|
|
6
|
+
// the WORK_TYPE constant. This set lists every type that HAS an icon (14
|
|
7
|
+
// WORK_TYPE values + the synthetic 'qa-validate' routed via routing.md);
|
|
8
|
+
// 'review' is intentionally absent — it renders a "REVIEW" word chip instead.
|
|
9
|
+
// Unknown/absent types fall back to a generic "•" text glyph and still get the
|
|
10
|
+
// raw type name in the hover tooltip.
|
|
11
|
+
var TYPE_ICON_SET = {
|
|
12
|
+
'implement': 1, 'implement:large': 1, 'fix': 1, 'verify': 1, 'test': 1,
|
|
13
|
+
'plan': 1, 'plan-to-prd': 1, 'decompose': 1, 'meeting': 1, 'explore': 1,
|
|
14
|
+
'ask': 1, 'docs': 1, 'setup': 1, 'qa-validate': 1,
|
|
24
15
|
};
|
|
25
|
-
function typeEmoji(t) {
|
|
26
|
-
if (!t) return '•';
|
|
27
|
-
return COMPLETION_TYPE_EMOJI[t] || '•';
|
|
28
|
-
}
|
|
29
16
|
|
|
30
17
|
// Pick the title for a completion card. Uses the dispatch description
|
|
31
18
|
// (c.task) — that's what the agent was actually told to do — falling back
|
|
@@ -59,17 +46,23 @@
|
|
|
59
46
|
return chip;
|
|
60
47
|
}
|
|
61
48
|
|
|
62
|
-
// Render a single
|
|
63
|
-
// element with attached click handlers. Used by renderHistoryFeed
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
49
|
+
// Render a single history card (DOM node). Returns the .history-item
|
|
50
|
+
// element with attached click handlers. Used by renderHistoryFeed for both
|
|
51
|
+
// finished completions AND in-flight dispatches — a running dispatch is just
|
|
52
|
+
// a dispatch without a result yet, so `isActive` swaps the terminal ✓/✕ for a
|
|
53
|
+
// live ● and anchors the time on `started_at` instead of `completed_at`.
|
|
54
|
+
function renderCompletionCard(c, isActive) {
|
|
55
|
+
var status = isActive ? 'active' : completionStatus(c);
|
|
56
|
+
var ts = isActive
|
|
57
|
+
? (c.started_at || c.created_at || c._ts)
|
|
58
|
+
: (c.completed_at || c.completedAt || c.endedAt || c.started_at || c.startedAt || c._ts);
|
|
67
59
|
var title = completionTitle(c);
|
|
68
60
|
var item = (c.meta && c.meta.item) || {};
|
|
69
61
|
var pr = (c.meta && c.meta.pr) || null;
|
|
62
|
+
var kindClass = isActive ? 'dispatch' : (status === 'fail' ? 'failure' : 'completion');
|
|
70
63
|
|
|
71
64
|
var card = document.createElement('div');
|
|
72
|
-
card.className = 'history-item completions-card kind-' +
|
|
65
|
+
card.className = 'history-item completions-card kind-' + kindClass;
|
|
73
66
|
card.setAttribute('role', 'button');
|
|
74
67
|
card.setAttribute('tabindex', '0');
|
|
75
68
|
card.title = title;
|
|
@@ -80,25 +73,38 @@
|
|
|
80
73
|
rail.className = 'completions-card-rail';
|
|
81
74
|
var railTop = document.createElement('div');
|
|
82
75
|
railTop.className = 'completions-card-rail-top';
|
|
76
|
+
// Review cards render the word "REVIEW" as a chip (same box treatment as
|
|
77
|
+
// the WI/Plan/PR lineage chips, larger font) instead of the type emoji.
|
|
78
|
+
// Every other type keeps its single-glyph emoji.
|
|
83
79
|
var chip = document.createElement('div');
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
80
|
+
if (c.type === 'review') {
|
|
81
|
+
chip.className = 'completions-card-type-word';
|
|
82
|
+
chip.textContent = 'REVIEW';
|
|
83
|
+
chip.title = 'review';
|
|
84
|
+
} else {
|
|
85
|
+
chip.className = 'completions-card-type-chip';
|
|
86
|
+
chip.textContent = typeEmoji(c.type);
|
|
87
|
+
if (c.type) chip.title = c.type;
|
|
88
|
+
}
|
|
87
89
|
railTop.appendChild(chip);
|
|
88
90
|
|
|
89
91
|
var railBottom = document.createElement('div');
|
|
90
92
|
railBottom.className = 'completions-card-rail-bottom';
|
|
91
93
|
var icon = document.createElement('span');
|
|
92
94
|
icon.className = 'completions-card-status-icon ' + status;
|
|
93
|
-
icon.textContent = status === 'ok' ? '✓' : (status === 'warn' ? '⚠' : '✕');
|
|
94
|
-
icon.title = status === 'ok' ? 'Success' : (status === 'warn' ? 'Partial' : 'Failure');
|
|
95
|
+
icon.textContent = status === 'active' ? '●' : (status === 'ok' ? '✓' : (status === 'warn' ? '⚠' : '✕'));
|
|
96
|
+
icon.title = status === 'active' ? 'Running' : (status === 'ok' ? 'Success' : (status === 'warn' ? 'Partial' : 'Failure'));
|
|
95
97
|
var sep = document.createElement('span');
|
|
96
98
|
sep.className = 'completions-card-rail-sep';
|
|
97
99
|
sep.textContent = '·';
|
|
98
100
|
sep.setAttribute('aria-hidden', 'true');
|
|
99
101
|
var when = document.createElement('span');
|
|
100
102
|
when.className = 'completions-card-time';
|
|
101
|
-
|
|
103
|
+
// Active dispatches read as "running 5m" (elapsed since start); finished
|
|
104
|
+
// rows read as "5m ago" (relative to completion). Both reuse relTime.
|
|
105
|
+
when.textContent = ts
|
|
106
|
+
? (status === 'active' ? 'running ' + relTime(ts).replace(' ago', '') : relTime(ts))
|
|
107
|
+
: '';
|
|
102
108
|
if (ts) when.title = new Date(ts).toLocaleString();
|
|
103
109
|
railBottom.appendChild(icon);
|
|
104
110
|
railBottom.appendChild(sep);
|
|
@@ -115,6 +121,16 @@
|
|
|
115
121
|
titleEl.textContent = title;
|
|
116
122
|
bodyTop.appendChild(titleEl);
|
|
117
123
|
|
|
124
|
+
// Byline — which team member ran it (name · role). Present on every row so
|
|
125
|
+
// the feed answers "who did this" at a glance, not just "what / result".
|
|
126
|
+
var who = c.agentName || c.agent;
|
|
127
|
+
if (who) {
|
|
128
|
+
var byline = document.createElement('div');
|
|
129
|
+
byline.className = 'completions-card-byline';
|
|
130
|
+
byline.textContent = c.agentRole ? who + ' · ' + c.agentRole : who;
|
|
131
|
+
bodyTop.appendChild(byline);
|
|
132
|
+
}
|
|
133
|
+
|
|
118
134
|
if (status === 'fail' && (c.failureClass || c.reason)) {
|
|
119
135
|
var failLine = document.createElement('div');
|
|
120
136
|
failLine.className = 'completions-card-fail-line';
|
|
@@ -310,20 +326,97 @@
|
|
|
310
326
|
// Bind completions-modal close handlers once (shared helper above).
|
|
311
327
|
bindModalClose('slim-completions-modal', 'slim-completions-close');
|
|
312
328
|
|
|
329
|
+
// Render a PR as a meta-rail card matching the dispatch/completion shape:
|
|
330
|
+
// a "PR" word chip in the rail (left), the title in the body, the activity
|
|
331
|
+
// time in the rail-bottom, and the PR status as a tile-chip — the SAME
|
|
332
|
+
// component the /pull-requests tile list uses — in the bottom row. The whole
|
|
333
|
+
// card is the click target (opens the PR URL); there's no separate "open"
|
|
334
|
+
// link anymore.
|
|
335
|
+
function renderPrCard(p) {
|
|
336
|
+
var num = p.prNumber || p.number || p.id;
|
|
337
|
+
var title = '#' + (num || '?') + ' — ' + (p.title || '(untitled PR)');
|
|
338
|
+
var ts = p.updatedAt || p.lastReviewedAt || p.lastBuildCheck || p._attachedAt || p.created || p.createdAt;
|
|
339
|
+
var build = p.buildStatus || '';
|
|
340
|
+
// Mirror renderPrTileBody's color logic so the chip matches the PR list:
|
|
341
|
+
// merged → green, failing build → red, active/linked → blue, else neutral.
|
|
342
|
+
var cls = p.status === 'merged' ? 'green'
|
|
343
|
+
: (build === 'failing' ? 'red'
|
|
344
|
+
: ((p.status === 'active' || p.status === 'linked') ? 'blue' : ''));
|
|
345
|
+
|
|
346
|
+
var card = document.createElement('div');
|
|
347
|
+
card.className = 'history-item completions-card kind-pr';
|
|
348
|
+
card.setAttribute('role', 'button');
|
|
349
|
+
card.setAttribute('tabindex', '0');
|
|
350
|
+
card.title = title;
|
|
351
|
+
|
|
352
|
+
var rail = document.createElement('div');
|
|
353
|
+
rail.className = 'completions-card-rail';
|
|
354
|
+
var railTop = document.createElement('div');
|
|
355
|
+
railTop.className = 'completions-card-rail-top';
|
|
356
|
+
var word = document.createElement('div');
|
|
357
|
+
word.className = 'completions-card-type-word';
|
|
358
|
+
word.textContent = 'PR';
|
|
359
|
+
word.title = 'pull request';
|
|
360
|
+
railTop.appendChild(word);
|
|
361
|
+
var railBottom = document.createElement('div');
|
|
362
|
+
railBottom.className = 'completions-card-rail-bottom';
|
|
363
|
+
var when = document.createElement('span');
|
|
364
|
+
when.className = 'completions-card-time';
|
|
365
|
+
when.textContent = ts ? relTime(ts) : '';
|
|
366
|
+
if (ts) when.title = new Date(ts).toLocaleString();
|
|
367
|
+
railBottom.appendChild(when);
|
|
368
|
+
rail.appendChild(railTop);
|
|
369
|
+
rail.appendChild(railBottom);
|
|
370
|
+
|
|
371
|
+
var bodyTop = document.createElement('div');
|
|
372
|
+
bodyTop.className = 'completions-card-body-top';
|
|
373
|
+
var titleEl = document.createElement('div');
|
|
374
|
+
titleEl.className = 'completions-card-title';
|
|
375
|
+
titleEl.textContent = title;
|
|
376
|
+
bodyTop.appendChild(titleEl);
|
|
377
|
+
|
|
378
|
+
// Status row — grid col 2, row 2 (aligns with rail-bottom). A single
|
|
379
|
+
// tile-chip, the same pill component (and color classes) as the PR list.
|
|
380
|
+
var statusRow = document.createElement('div');
|
|
381
|
+
statusRow.className = 'completions-card-pr-status';
|
|
382
|
+
var chip = document.createElement('span');
|
|
383
|
+
chip.className = 'tile-chip ' + cls;
|
|
384
|
+
chip.textContent = p.status || '—';
|
|
385
|
+
statusRow.appendChild(chip);
|
|
386
|
+
|
|
387
|
+
card.appendChild(rail);
|
|
388
|
+
card.appendChild(bodyTop);
|
|
389
|
+
card.appendChild(statusRow);
|
|
390
|
+
|
|
391
|
+
// Whole-card click (and keyboard activation) opens the PR — replaces the
|
|
392
|
+
// old inline "open" hyperlink.
|
|
393
|
+
if (p.url) {
|
|
394
|
+
var openPr = function() { window.open(p.url, '_blank', 'noopener'); };
|
|
395
|
+
card.addEventListener('click', openPr);
|
|
396
|
+
card.addEventListener('keydown', function(ev) {
|
|
397
|
+
if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); openPr(); }
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
return card;
|
|
401
|
+
}
|
|
402
|
+
|
|
313
403
|
function renderHistoryFeed(input) {
|
|
314
404
|
var listEl = document.getElementById('history-list');
|
|
315
405
|
if (!listEl) return;
|
|
316
406
|
var events = [];
|
|
317
407
|
|
|
318
|
-
// Active dispatches (blue) —
|
|
408
|
+
// Active dispatches (blue) — rendered with the same meta-rail card as
|
|
409
|
+
// completions (a running dispatch is just a dispatch without a result yet).
|
|
410
|
+
// Carry the raw record so the card renderer reads task/agent/lineage from
|
|
411
|
+
// the canonical field paths. NOTE: the engine stamps `started_at`
|
|
412
|
+
// (snake_case) — reading `startedAt` here is the old bug that made every
|
|
413
|
+
// dispatch show "0s ago". Sorted purely chronologically by start time.
|
|
319
414
|
(input.active || []).forEach(function(d) {
|
|
320
|
-
var ts = d.
|
|
415
|
+
var ts = d.started_at || d.created_at || d._ts;
|
|
321
416
|
events.push({
|
|
322
417
|
kind: 'dispatch',
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
meta: 'started ' + relTime(ts) + (d.workItemId ? ' · ' + d.workItemId : ''),
|
|
326
|
-
ts: ts ? Date.parse(ts) || Date.now() : Date.now(),
|
|
418
|
+
raw: d,
|
|
419
|
+
ts: ts ? Date.parse(ts) || 0 : 0,
|
|
327
420
|
});
|
|
328
421
|
});
|
|
329
422
|
|
|
@@ -340,17 +433,18 @@
|
|
|
340
433
|
});
|
|
341
434
|
});
|
|
342
435
|
|
|
343
|
-
// PR events (amber) — shows PRs we're tracking, anchored on
|
|
344
|
-
//
|
|
345
|
-
//
|
|
436
|
+
// PR events (amber) — shows PRs we're tracking, anchored on the most
|
|
437
|
+
// recent activity. The slim PR shape does NOT carry updatedAt/createdAt —
|
|
438
|
+
// reading those (the old code) yielded ts=0 and pinned every PR to the
|
|
439
|
+
// bottom of the feed regardless of recency. Fall through the timestamps the
|
|
440
|
+
// payload actually provides, newest-meaning first.
|
|
441
|
+
// Carry the raw PR record so renderPrCard reads its own fields (the card
|
|
442
|
+
// opens the PR on click — no inline link).
|
|
346
443
|
(input.prs || []).slice(0, 6).forEach(function(p) {
|
|
347
|
-
var ts = p.updatedAt || p.createdAt;
|
|
444
|
+
var ts = p.updatedAt || p.lastReviewedAt || p.lastBuildCheck || p._attachedAt || p.created || p.createdAt;
|
|
348
445
|
events.push({
|
|
349
446
|
kind: 'pr',
|
|
350
|
-
|
|
351
|
-
title: '#' + (p.prNumber || p.id || '?') + ' — ' + (p.title || '(untitled PR)'),
|
|
352
|
-
meta: (p.buildStatus || 'no-build') + ' · ' + (p.reviewStatus || 'no-review'),
|
|
353
|
-
metaUrl: p.url || null,
|
|
447
|
+
raw: p,
|
|
354
448
|
ts: ts ? Date.parse(ts) || 0 : 0,
|
|
355
449
|
});
|
|
356
450
|
});
|
|
@@ -366,48 +460,19 @@
|
|
|
366
460
|
return;
|
|
367
461
|
}
|
|
368
462
|
// Build the feed as DOM nodes so SEC-03's .innerHTML ratchet stays happy
|
|
369
|
-
// (test/unit.test.js DYNAMIC_INNERHTML_BASELINE).
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
// shape since the user-facing redesign is scoped to completions for now.
|
|
463
|
+
// (test/unit.test.js DYNAMIC_INNERHTML_BASELINE). Every event type now
|
|
464
|
+
// renders as a meta-rail card: dispatch / completion / failure via
|
|
465
|
+
// renderCompletionCard, PRs via renderPrCard.
|
|
373
466
|
var frag = document.createDocumentFragment();
|
|
374
467
|
for (var i = 0; i < events.length; i++) {
|
|
375
468
|
var e = events[i];
|
|
376
|
-
if (
|
|
377
|
-
frag.appendChild(renderCompletionCard(e.raw));
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
var head = document.createElement('div');
|
|
383
|
-
head.className = 'history-head';
|
|
384
|
-
var kind = document.createElement('span');
|
|
385
|
-
kind.className = 'history-kind';
|
|
386
|
-
kind.textContent = e.kindLabel;
|
|
387
|
-
var when = document.createElement('span');
|
|
388
|
-
when.className = 'history-time';
|
|
389
|
-
when.textContent = e.ts ? relTime(e.ts) : '';
|
|
390
|
-
head.appendChild(kind);
|
|
391
|
-
head.appendChild(when);
|
|
392
|
-
var titleEl = document.createElement('div');
|
|
393
|
-
titleEl.className = 'history-title';
|
|
394
|
-
titleEl.textContent = e.title;
|
|
395
|
-
var metaEl = document.createElement('div');
|
|
396
|
-
metaEl.className = 'history-meta';
|
|
397
|
-
metaEl.appendChild(document.createTextNode(e.meta || ''));
|
|
398
|
-
if (e.metaUrl) {
|
|
399
|
-
metaEl.appendChild(document.createTextNode(' · '));
|
|
400
|
-
var a = document.createElement('a');
|
|
401
|
-
a.href = e.metaUrl;
|
|
402
|
-
a.target = '_blank';
|
|
403
|
-
a.rel = 'noopener';
|
|
404
|
-
a.textContent = 'open';
|
|
405
|
-
metaEl.appendChild(a);
|
|
469
|
+
if (e.kind === 'dispatch' && e.raw) {
|
|
470
|
+
frag.appendChild(renderCompletionCard(e.raw, true));
|
|
471
|
+
} else if ((e.kind === 'completion' || e.kind === 'failure') && e.raw) {
|
|
472
|
+
frag.appendChild(renderCompletionCard(e.raw, false));
|
|
473
|
+
} else if (e.kind === 'pr' && e.raw) {
|
|
474
|
+
frag.appendChild(renderPrCard(e.raw));
|
|
406
475
|
}
|
|
407
|
-
item.appendChild(head);
|
|
408
|
-
item.appendChild(titleEl);
|
|
409
|
-
item.appendChild(metaEl);
|
|
410
|
-
frag.appendChild(item);
|
|
411
476
|
}
|
|
412
477
|
listEl.replaceChildren(frag);
|
|
413
478
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
diff a/dashboard/slim/js/history.js b/dashboard/slim/js/history.js (rejected hunks)
|
|
2
|
+
@@ -96,7 +83,12 @@
|
|
3
|
+
chip.title = 'review';
|
|
4
|
+
} else {
|
|
5
|
+
chip.className = 'completions-card-type-chip';
|
|
6
|
+
- chip.textContent = typeEmoji(c.type);
|
|
7
|
+
+ // Known types → Fluent icon via CSS mask (data-type); unknown → "•" glyph.
|
|
8
|
+
+ if (c.type && TYPE_ICON_SET[c.type]) {
|
|
9
|
+
+ chip.setAttribute('data-type', c.type);
|
|
10
|
+
+ } else {
|
|
11
|
+
+ chip.textContent = '•';
|
|
12
|
+
+ }
|
|
13
|
+
if (c.type) chip.title = c.type;
|
|
14
|
+
}
|
|
15
|
+
railTop.appendChild(chip);
|
|
16
|
+
@@ -105,7 +97,9 @@
|
|
17
|
+
railBottom.className = 'completions-card-rail-bottom';
|
|
18
|
+
var icon = document.createElement('span');
|
|
19
|
+
icon.className = 'completions-card-status-icon ' + status;
|
|
20
|
+
- icon.textContent = status === 'active' ? '●' : (status === 'ok' ? '✓' : (status === 'warn' ? '⚠' : '✕'));
|
|
21
|
+
+ // ok/warn/fail render as Fluent icons via the status class (CSS mask in
|
|
22
|
+
+ // styles.css); the live 'active' state keeps its pulsing dot.
|
|
23
|
+
+ if (status === 'active') icon.textContent = '●';
|
|
24
|
+
icon.title = status === 'active' ? 'Running' : (status === 'ok' ? 'Success' : (status === 'warn' ? 'Partial' : 'Failure'));
|
|
25
|
+
var sep = document.createElement('span');
|
|
26
|
+
sep.className = 'completions-card-rail-sep';
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
// onClose (optional) runs on every dismiss path — close button, backdrop
|
|
2
|
+
// click, and Esc — so callers can attach a side effect (e.g. persisting a
|
|
3
|
+
// "seen" flag for a first-visit popup) without re-wiring the listeners.
|
|
4
|
+
function bindModalClose(modalId, closeBtnId, onClose) {
|
|
2
5
|
var modal = document.getElementById(modalId);
|
|
3
6
|
if (!modal) return;
|
|
4
|
-
function close() { modal.classList.remove('open'); }
|
|
7
|
+
function close() { modal.classList.remove('open'); if (onClose) onClose(); }
|
|
5
8
|
var closeBtn = document.getElementById(closeBtnId);
|
|
6
9
|
if (closeBtn) closeBtn.addEventListener('click', close);
|
|
7
10
|
modal.addEventListener('click', function(ev) { if (ev.target === modal) close(); });
|
|
@@ -123,6 +126,9 @@
|
|
|
123
126
|
// Populate + open the cockpit-tile detail modal from the latest status
|
|
124
127
|
// snapshot. Mirrors the corresponding old-dashboard tab for each tile.
|
|
125
128
|
function openTileModal(key) {
|
|
129
|
+
// The Pinned-context tile has its own list/editor modal (view + edit + unpin)
|
|
130
|
+
// rather than the read-only tile detail view.
|
|
131
|
+
if (key === 'pinned') { openSlimPinnedList(); return; }
|
|
126
132
|
var view = TILE_VIEWS[key];
|
|
127
133
|
if (!view) return;
|
|
128
134
|
var modal = document.getElementById('slim-tile-modal');
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// ── Pinned context (create / view / edit / unpin) ──────────────
|
|
2
|
+
// Slim front-end over the same endpoints the classic dashboard uses:
|
|
3
|
+
// GET (via /api/status `pinned`), POST /api/pinned (create),
|
|
4
|
+
// POST /api/pinned/update (edit), POST /api/pinned/remove (unpin).
|
|
5
|
+
// The "Pin Content" action button and the "Pinned context" status tile
|
|
6
|
+
// both surface this. Editing reuses the atomic update endpoint.
|
|
7
|
+
|
|
8
|
+
// When non-null, the editor is editing the entry with this (original) title;
|
|
9
|
+
// null means it is creating a new pin.
|
|
10
|
+
var _pinEditOriginal = null;
|
|
11
|
+
|
|
12
|
+
function slimPinnedEntries() {
|
|
13
|
+
return (lastStatusData && Array.isArray(lastStatusData.pinned)) ? lastStatusData.pinned : [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function pinLevelChipClass(level) {
|
|
17
|
+
return level === 'critical' ? 'red' : level === 'warning' ? 'amber' : 'blue';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ── Editor (create + edit) ───────────────────────────────────────
|
|
21
|
+
function openSlimPinEditor(entry) {
|
|
22
|
+
_pinEditOriginal = entry ? entry.title : null;
|
|
23
|
+
var modal = document.getElementById('slim-pin-edit-modal');
|
|
24
|
+
if (!modal) return;
|
|
25
|
+
document.getElementById('slim-pin-edit-heading').textContent = entry ? 'Edit pinned content' : 'Pin content';
|
|
26
|
+
document.getElementById('slim-pin-title').value = entry ? (entry.title || '') : '';
|
|
27
|
+
document.getElementById('slim-pin-content').value = entry ? (entry.content || '') : '';
|
|
28
|
+
document.getElementById('slim-pin-level').value = (entry && entry.level) ? entry.level : 'info';
|
|
29
|
+
var msg = document.getElementById('slim-pin-msg');
|
|
30
|
+
if (msg) { msg.textContent = ''; msg.style.color = 'var(--muted)'; }
|
|
31
|
+
var submit = document.getElementById('slim-pin-submit');
|
|
32
|
+
if (submit) { submit.disabled = false; submit.textContent = entry ? 'Save' : 'Pin'; }
|
|
33
|
+
modal.classList.add('open');
|
|
34
|
+
setTimeout(function() { var t = document.getElementById('slim-pin-title'); if (t) t.focus(); }, 40);
|
|
35
|
+
}
|
|
36
|
+
function closeSlimPinEditor() {
|
|
37
|
+
var modal = document.getElementById('slim-pin-edit-modal');
|
|
38
|
+
if (modal) modal.classList.remove('open');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function submitSlimPin() {
|
|
42
|
+
var title = (document.getElementById('slim-pin-title').value || '').trim();
|
|
43
|
+
var content = document.getElementById('slim-pin-content').value;
|
|
44
|
+
var level = document.getElementById('slim-pin-level').value;
|
|
45
|
+
var msg = document.getElementById('slim-pin-msg');
|
|
46
|
+
var submit = document.getElementById('slim-pin-submit');
|
|
47
|
+
if (!title || !content.trim()) {
|
|
48
|
+
if (msg) { msg.style.color = 'var(--red)'; msg.textContent = 'Title and content are required.'; }
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
var editing = _pinEditOriginal != null;
|
|
52
|
+
if (submit) { submit.disabled = true; submit.textContent = editing ? 'Saving…' : 'Pinning…'; }
|
|
53
|
+
try {
|
|
54
|
+
var res;
|
|
55
|
+
if (editing) {
|
|
56
|
+
res = await fetch('/api/pinned/update', {
|
|
57
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
58
|
+
body: JSON.stringify({ originalTitle: _pinEditOriginal, title: title, content: content, level: level }),
|
|
59
|
+
});
|
|
60
|
+
} else {
|
|
61
|
+
res = await fetch('/api/pinned', {
|
|
62
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
63
|
+
body: JSON.stringify({ title: title, content: content, level: level }),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
var d = await res.json().catch(function() { return {}; });
|
|
67
|
+
if (!res.ok) throw new Error(d.error || ('HTTP ' + res.status));
|
|
68
|
+
closeSlimPinEditor();
|
|
69
|
+
scheduleStatusRefresh(400); // refresh the Pinned-context tile
|
|
70
|
+
// If the list modal is open, re-render it once the snapshot catches up.
|
|
71
|
+
setTimeout(function() {
|
|
72
|
+
var listModal = document.getElementById('slim-pinned-modal');
|
|
73
|
+
if (listModal && listModal.classList.contains('open')) renderSlimPinnedList();
|
|
74
|
+
}, 700);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
if (msg) { msg.style.color = 'var(--red)'; msg.textContent = 'Error: ' + (e && e.message ? e.message : 'failed'); }
|
|
77
|
+
if (submit) { submit.disabled = false; submit.textContent = editing ? 'Save' : 'Pin'; }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── List / view / unpin ──────────────────────────────────────────
|
|
82
|
+
function openSlimPinnedList() {
|
|
83
|
+
var modal = document.getElementById('slim-pinned-modal');
|
|
84
|
+
if (!modal) return;
|
|
85
|
+
renderSlimPinnedList();
|
|
86
|
+
modal.classList.add('open');
|
|
87
|
+
}
|
|
88
|
+
function closeSlimPinnedList() {
|
|
89
|
+
var modal = document.getElementById('slim-pinned-modal');
|
|
90
|
+
if (modal) modal.classList.remove('open');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function renderSlimPinnedList() {
|
|
94
|
+
var body = document.getElementById('slim-pinned-body');
|
|
95
|
+
if (!body) return;
|
|
96
|
+
var entries = slimPinnedEntries();
|
|
97
|
+
body.textContent = '';
|
|
98
|
+
if (!entries.length) {
|
|
99
|
+
var empty = document.createElement('div');
|
|
100
|
+
empty.className = 'tile-empty';
|
|
101
|
+
empty.textContent = 'Nothing pinned. Pin context all agents should read first.';
|
|
102
|
+
body.appendChild(empty);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
entries.forEach(function(entry) {
|
|
106
|
+
var card = document.createElement('div');
|
|
107
|
+
card.className = 'pinned-row';
|
|
108
|
+
|
|
109
|
+
var top = document.createElement('div');
|
|
110
|
+
top.className = 'pinned-row-top';
|
|
111
|
+
var titleEl = document.createElement('span');
|
|
112
|
+
titleEl.className = 'pinned-row-title';
|
|
113
|
+
titleEl.textContent = entry.title || '(untitled)';
|
|
114
|
+
titleEl.title = entry.title || '';
|
|
115
|
+
top.appendChild(titleEl);
|
|
116
|
+
var chip = document.createElement('span');
|
|
117
|
+
chip.className = 'tile-chip ' + pinLevelChipClass(entry.level);
|
|
118
|
+
chip.textContent = entry.level || 'info';
|
|
119
|
+
top.appendChild(chip);
|
|
120
|
+
card.appendChild(top);
|
|
121
|
+
|
|
122
|
+
var preview = document.createElement('div');
|
|
123
|
+
preview.className = 'pinned-row-preview';
|
|
124
|
+
var text = String(entry.content || '');
|
|
125
|
+
preview.textContent = text.length > 220 ? text.slice(0, 220) + '…' : text;
|
|
126
|
+
card.appendChild(preview);
|
|
127
|
+
|
|
128
|
+
var actions = document.createElement('div');
|
|
129
|
+
actions.className = 'pinned-row-actions';
|
|
130
|
+
var editBtn = document.createElement('button');
|
|
131
|
+
editBtn.className = 'btn-secondary';
|
|
132
|
+
editBtn.type = 'button';
|
|
133
|
+
editBtn.textContent = 'Edit';
|
|
134
|
+
editBtn.addEventListener('click', function() { openSlimPinEditor(entry); });
|
|
135
|
+
var unpinBtn = document.createElement('button');
|
|
136
|
+
unpinBtn.className = 'btn-secondary pinned-row-unpin';
|
|
137
|
+
unpinBtn.type = 'button';
|
|
138
|
+
unpinBtn.textContent = 'Unpin';
|
|
139
|
+
unpinBtn.addEventListener('click', function() { removeSlimPin(entry.title); });
|
|
140
|
+
actions.appendChild(editBtn);
|
|
141
|
+
actions.appendChild(unpinBtn);
|
|
142
|
+
card.appendChild(actions);
|
|
143
|
+
|
|
144
|
+
body.appendChild(card);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function removeSlimPin(title) {
|
|
149
|
+
if (!title) return;
|
|
150
|
+
if (!confirm('Unpin "' + title + '"?')) return;
|
|
151
|
+
try {
|
|
152
|
+
var res = await fetch('/api/pinned/remove', {
|
|
153
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
154
|
+
body: JSON.stringify({ title: title }),
|
|
155
|
+
});
|
|
156
|
+
if (!res.ok) { var d = await res.json().catch(function() { return {}; }); throw new Error(d.error || ('HTTP ' + res.status)); }
|
|
157
|
+
// Optimistically drop it from the cached snapshot so the list + tile update
|
|
158
|
+
// before the next poll, then reconcile.
|
|
159
|
+
if (lastStatusData && Array.isArray(lastStatusData.pinned)) {
|
|
160
|
+
lastStatusData.pinned = lastStatusData.pinned.filter(function(e) { return e.title !== title; });
|
|
161
|
+
}
|
|
162
|
+
renderSlimPinnedList();
|
|
163
|
+
scheduleStatusRefresh(400);
|
|
164
|
+
} catch (e) {
|
|
165
|
+
alert('Unpin failed: ' + (e && e.message ? e.message : e));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
bindModalClose('slim-pinned-modal', 'slim-pinned-close');
|
|
170
|
+
bindModalClose('slim-pin-edit-modal', 'slim-pin-edit-close');
|
|
171
|
+
(function bindPinnedUi() {
|
|
172
|
+
var pinBtn = document.getElementById('slim-pin-btn');
|
|
173
|
+
if (pinBtn) pinBtn.addEventListener('click', function() { openSlimPinEditor(null); });
|
|
174
|
+
var addBtn = document.getElementById('slim-pinned-add');
|
|
175
|
+
if (addBtn) addBtn.addEventListener('click', function() { openSlimPinEditor(null); });
|
|
176
|
+
var tileChip = document.getElementById('slim-tile-pin-chip');
|
|
177
|
+
if (tileChip) tileChip.addEventListener('click', function(ev) { ev.stopPropagation(); openSlimPinEditor(null); });
|
|
178
|
+
var cancel = document.getElementById('slim-pin-cancel');
|
|
179
|
+
if (cancel) cancel.addEventListener('click', closeSlimPinEditor);
|
|
180
|
+
var submit = document.getElementById('slim-pin-submit');
|
|
181
|
+
if (submit) submit.addEventListener('click', submitSlimPin);
|
|
182
|
+
})();
|