@yemi33/minions 0.1.11 → 0.1.13
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/CHANGELOG.md +60 -0
- package/dashboard/js/command-center.js +377 -0
- package/dashboard/js/command-history.js +70 -0
- package/dashboard/js/command-input.js +268 -0
- package/dashboard/js/command-parser.js +129 -0
- package/dashboard/js/detail-panel.js +98 -0
- package/dashboard/js/live-stream.js +69 -0
- package/dashboard/js/modal-qa.js +268 -0
- package/dashboard/js/modal.js +131 -0
- package/dashboard/js/refresh.js +59 -0
- package/dashboard/js/render-agents.js +17 -0
- package/dashboard/js/render-dispatch.js +148 -0
- package/dashboard/js/render-inbox.js +126 -0
- package/dashboard/js/render-kb.js +107 -0
- package/dashboard/js/render-other.js +181 -0
- package/dashboard/js/render-plans.js +304 -0
- package/dashboard/js/render-prd.js +469 -0
- package/dashboard/js/render-prs.js +94 -0
- package/dashboard/js/render-schedules.js +158 -0
- package/dashboard/js/render-skills.js +89 -0
- package/dashboard/js/render-work-items.js +219 -0
- package/dashboard/js/settings.js +135 -0
- package/dashboard/js/state.js +84 -0
- package/dashboard/js/utils.js +39 -0
- package/dashboard/layout.html +123 -0
- package/dashboard/pages/engine.html +12 -0
- package/dashboard/pages/home.html +31 -0
- package/dashboard/pages/inbox.html +17 -0
- package/dashboard/pages/plans.html +4 -0
- package/dashboard/pages/prd.html +5 -0
- package/dashboard/pages/prs.html +4 -0
- package/dashboard/pages/schedule.html +10 -0
- package/dashboard/pages/work.html +5 -0
- package/dashboard/styles.css +598 -0
- package/dashboard-build.js +51 -0
- package/dashboard.html +179 -107
- package/dashboard.js +51 -1
- package/engine/ado.js +14 -0
- package/engine/cli.js +11 -0
- package/engine/github.js +14 -0
- package/engine/lifecycle.js +25 -29
- package/engine.js +106 -19
- package/package.json +1 -1
- package/routing.md +1 -1
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
// render-prd.js — PRD-related rendering functions extracted from dashboard.html
|
|
2
|
+
|
|
3
|
+
function renderPrd(prd, prog) {
|
|
4
|
+
const section = document.getElementById('prd-content');
|
|
5
|
+
const badge = document.getElementById('prd-badge');
|
|
6
|
+
if (!prd) {
|
|
7
|
+
section.innerHTML = '<p class="prd-pending" style="margin-bottom:0">No PRD found.</p>';
|
|
8
|
+
badge.innerHTML = '';
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
if (prd.error) {
|
|
12
|
+
section.innerHTML = '<p style="color:var(--orange);margin-bottom:0">PRD file found but has parse errors.</p>';
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
badge.innerHTML = '<span style="color:var(--green);font-size:11px">' + prd.age + '</span>';
|
|
16
|
+
// Stats are now rendered per-group inside renderPrdProgress
|
|
17
|
+
section.innerHTML = '';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function renderPrdProgress(prog) {
|
|
21
|
+
const el = document.getElementById('prd-progress-content');
|
|
22
|
+
const countEl = document.getElementById('prd-progress-count');
|
|
23
|
+
if (!prog) { el.innerHTML = ''; countEl.textContent = '—'; return; }
|
|
24
|
+
|
|
25
|
+
// Compute progress from active (non-archived) items only
|
|
26
|
+
const activeItems = (prog.items || []).filter(i => !i._archived);
|
|
27
|
+
if (activeItems.length > 0) {
|
|
28
|
+
const activeDone = activeItems.filter(i => i.status === 'done' || i.status === 'implemented' || i.status === 'in-pr').length; // in-pr counted as done for backward compat
|
|
29
|
+
countEl.textContent = Math.round((activeDone / activeItems.length) * 100) + '%';
|
|
30
|
+
} else {
|
|
31
|
+
countEl.textContent = '—';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function renderGroupStats(items) {
|
|
35
|
+
const total = items.length;
|
|
36
|
+
if (total === 0) return '';
|
|
37
|
+
const done = items.filter(i => i.status === 'done' || i.status === 'implemented' || i.status === 'in-pr').length; // in-pr counted as done for backward compat
|
|
38
|
+
const inProgress = items.filter(i => i.status === 'in-progress').length;
|
|
39
|
+
const failed = items.filter(i => i.status === 'failed').length;
|
|
40
|
+
const paused = items.filter(i => i.status === 'paused').length;
|
|
41
|
+
const missing = items.filter(i => i.status === 'missing' || !i.status).length;
|
|
42
|
+
const pct = (n) => total > 0 ? ((n / total) * 100).toFixed(1) : 0;
|
|
43
|
+
|
|
44
|
+
const stats = '<div class="prd-stats" style="margin:0">' +
|
|
45
|
+
'<div class="prd-stat"><div class="prd-stat-num green">' + done + '</div><div class="prd-stat-label">Done</div></div>' +
|
|
46
|
+
'<div class="prd-stat"><div class="prd-stat-num" style="color:var(--yellow)">' + inProgress + '</div><div class="prd-stat-label">Active</div></div>' +
|
|
47
|
+
(failed ? '<div class="prd-stat"><div class="prd-stat-num" style="color:var(--red)">' + failed + '</div><div class="prd-stat-label">Failed</div></div>' : '') +
|
|
48
|
+
(paused ? '<div class="prd-stat"><div class="prd-stat-num" style="color:var(--muted)">' + paused + '</div><div class="prd-stat-label">Paused</div></div>' : '') +
|
|
49
|
+
'<div class="prd-stat"><div class="prd-stat-num" style="color:var(--muted)">' + missing + '</div><div class="prd-stat-label">To Do</div></div>' +
|
|
50
|
+
'<div class="prd-stat"><div class="prd-stat-num" style="color:var(--text)">' + total + '</div><div class="prd-stat-label">Total</div></div>' +
|
|
51
|
+
'</div>';
|
|
52
|
+
|
|
53
|
+
const bar = '<div class="prd-progress-bar">' +
|
|
54
|
+
'<div class="seg complete" style="width:' + pct(done) + '%"></div>' +
|
|
55
|
+
'<div class="seg in-progress" style="width:' + pct(inProgress) + '%"></div>' +
|
|
56
|
+
'<div class="seg paused" style="width:' + pct(paused) + '%"></div>' +
|
|
57
|
+
'<div class="seg missing" style="width:' + pct(missing) + '%"></div>' +
|
|
58
|
+
'</div>';
|
|
59
|
+
|
|
60
|
+
return '<div style="margin:6px 0 8px 0;padding:0 8px">' + stats + '<div style="margin-top:8px">' + bar + '</div></div>';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// PRD item statuses: missing → in-progress → done
|
|
64
|
+
const statusBadge = (s) => {
|
|
65
|
+
const styles = {
|
|
66
|
+
'done': 'background:rgba(63,185,80,0.15);color:var(--green)',
|
|
67
|
+
'implemented': 'background:rgba(63,185,80,0.15);color:var(--green)', /* legacy alias */
|
|
68
|
+
'in-pr': 'background:rgba(63,185,80,0.15);color:var(--green)', /* backward compat: displayed as done */
|
|
69
|
+
'in-progress': 'background:rgba(210,153,34,0.15);color:var(--yellow);animation:wipPulse 1.5s infinite',
|
|
70
|
+
'failed': 'background:rgba(248,81,73,0.15);color:var(--red)',
|
|
71
|
+
'paused': 'background:rgba(139,148,158,0.15);color:var(--muted)',
|
|
72
|
+
};
|
|
73
|
+
const labels = { 'done': 'DONE', 'implemented': 'DONE', 'in-pr': 'DONE', 'in-progress': 'WIP', 'failed': 'FAIL', 'paused': 'PAUSED', 'missing': '—' };
|
|
74
|
+
const style = styles[s] || 'background:var(--surface);color:var(--muted)';
|
|
75
|
+
const label = labels[s] || '—';
|
|
76
|
+
return '<span style="font-size:9px;font-weight:700;padding:2px 6px;border-radius:3px;letter-spacing:0.5px;white-space:nowrap;' + style + '">' + label + '</span>';
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Build work item lookup: PRD item ID → work item info
|
|
80
|
+
const wiById = {};
|
|
81
|
+
for (const w of (window._lastWorkItems || [])) {
|
|
82
|
+
if (w.id) wiById[w.id] = w;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Group items by source plan
|
|
86
|
+
const grouped = {};
|
|
87
|
+
for (const i of (prog.items || [])) {
|
|
88
|
+
const key = i.source || '_ungrouped';
|
|
89
|
+
if (!grouped[key]) grouped[key] = { summary: i.planSummary || i.source || 'Items', project: i.planProject || '', file: i.source || '', items: [], archived: !!i._archived, planStatus: i.planStatus || 'active', sourcePlan: i.sourcePlan || '', planStale: i.planStale || false, lastSyncedFromPlan: i.lastSyncedFromPlan || null, prdUpdatedAt: i.prdUpdatedAt || null };
|
|
90
|
+
grouped[key].items.push(i);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const renderItem = (i) => {
|
|
94
|
+
const prLinks = (i.prs || []).map(pr =>
|
|
95
|
+
'<a class="pr-title" href="' + escHtml(pr.url || '#') + '" target="_blank" style="font-size:10px;margin-left:4px" title="' + escHtml(pr.title || '') + '">' + escHtml(pr.id) + '</a>'
|
|
96
|
+
).join(' ');
|
|
97
|
+
const projBadges = (i.projects || []).map(p =>
|
|
98
|
+
'<span class="prd-project-badge">' + escHtml(p) + '</span>'
|
|
99
|
+
).join(' ');
|
|
100
|
+
const src = escHtml(i.source || '');
|
|
101
|
+
const iid = escHtml(i.id || '');
|
|
102
|
+
|
|
103
|
+
// Linked work item info
|
|
104
|
+
const wi = wiById[i.id];
|
|
105
|
+
const wiLabel = wi ? '<span style="font-size:9px;color:var(--muted);background:var(--surface);padding:1px 5px;border-radius:3px;border:1px solid var(--border)" title="Work item: ' + escHtml(wi.id) + '">' +
|
|
106
|
+
escHtml(wi.id) + (wi.dispatched_to ? ' → ' + escHtml(wi.dispatched_to) : '') + '</span>' : '';
|
|
107
|
+
|
|
108
|
+
// Requeue button for failed items
|
|
109
|
+
const canRequeue = wi && (wi.status === 'failed' || i.status === 'failed');
|
|
110
|
+
const requeueState = wi ? getPrdRequeueState(wi.id) : null;
|
|
111
|
+
let requeueBtn = '';
|
|
112
|
+
if (requeueState && requeueState.status === 'pending') {
|
|
113
|
+
requeueBtn = '<span style="color:var(--yellow);cursor:wait;font-size:9px;padding:1px 5px;background:rgba(210,153,34,0.1);border:1px solid rgba(210,153,34,0.35);border-radius:3px" title="Retry request in progress">requeuing…</span>';
|
|
114
|
+
} else if (requeueState && requeueState.status === 'queued') {
|
|
115
|
+
requeueBtn = '<span style="color:var(--green);cursor:default;font-size:9px;padding:1px 5px;background:rgba(63,185,80,0.1);border:1px solid rgba(63,185,80,0.35);border-radius:3px" title="Successfully requeued">requeued</span>';
|
|
116
|
+
} else if (requeueState && requeueState.status === 'error') {
|
|
117
|
+
requeueBtn = '<span style="color:var(--red);cursor:default;font-size:9px;padding:1px 5px;background:rgba(248,81,73,0.1);border:1px solid rgba(248,81,73,0.35);border-radius:3px" title="' + escHtml(requeueState.message || 'Retry failed') + '">retry failed</span>';
|
|
118
|
+
} else if (canRequeue) {
|
|
119
|
+
requeueBtn = '<span onclick="event.stopPropagation();prdItemRequeue(\'' + escHtml(wi.id) + '\',\'' + escHtml(wi._source || '') + '\')" style="color:var(--green);cursor:pointer;font-size:9px;padding:1px 5px;background:rgba(63,185,80,0.1);border:1px solid rgba(63,185,80,0.3);border-radius:3px" title="Requeue this work item">retry</span>';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return '<div class="prd-item-row st-' + (i.status || 'missing') + '" style="flex-wrap:wrap;cursor:pointer" onclick="prdItemEdit(\'' + src + '\',\'' + iid + '\')">' +
|
|
123
|
+
statusBadge(i.status) +
|
|
124
|
+
'<span class="prd-item-id">' + escHtml(i.id) + '</span>' +
|
|
125
|
+
'<span class="prd-item-name" title="' + escHtml(i.name) + '">' + escHtml(i.name) + '</span>' +
|
|
126
|
+
wiLabel +
|
|
127
|
+
requeueBtn +
|
|
128
|
+
(projBadges ? '<span>' + projBadges + '</span>' : '') +
|
|
129
|
+
(prLinks ? '<span>' + prLinks + '</span>' : '') +
|
|
130
|
+
'<span class="prd-item-priority ' + (i.priority || '') + '">' + escHtml(i.priority || '') + '</span>' +
|
|
131
|
+
'<span onclick="event.stopPropagation();prdItemRemove(\'' + src + '\',\'' + iid + '\')" style="color:var(--red);cursor:pointer;font-size:10px;padding:0 4px" title="Remove item">x</span>' +
|
|
132
|
+
(i.description ? '<div style="width:100%;font-size:11px;color:var(--muted);padding:2px 0 2px 42px;line-height:1.4">' + escHtml(i.description) + '</div>' : '') +
|
|
133
|
+
'</div>';
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const keys = Object.keys(grouped);
|
|
137
|
+
const formatDuration = (ms) => {
|
|
138
|
+
if (!ms || ms < 0) return '';
|
|
139
|
+
const s = Math.floor(ms / 1000);
|
|
140
|
+
if (s < 60) return s + 's';
|
|
141
|
+
const m = Math.floor(s / 60);
|
|
142
|
+
if (m < 60) return m + 'm ' + (s % 60) + 's';
|
|
143
|
+
const h = Math.floor(m / 60);
|
|
144
|
+
if (h < 24) return h + 'h ' + (m % 60) + 'm';
|
|
145
|
+
const d = Math.floor(h / 24);
|
|
146
|
+
return d + 'd ' + (h % 24) + 'h';
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const timings = prog.planTimings || {};
|
|
150
|
+
|
|
151
|
+
const renderGroupHeader = (g) => {
|
|
152
|
+
const done = g.items.filter(i => i.status === 'done' || i.status === 'complete' || i.status === 'implemented').length;
|
|
153
|
+
const wip = g.items.filter(i => i.status === 'in-progress' || i.status === 'dispatched').length;
|
|
154
|
+
const summary = (g.summary || '').replace(/^Convert plan to PRD:\s*/i, '').slice(0, 80);
|
|
155
|
+
const isAwaitingApproval = g.planStatus === 'awaiting-approval';
|
|
156
|
+
const isPaused = g.planStatus === 'paused';
|
|
157
|
+
const isBlocked = isAwaitingApproval || isPaused;
|
|
158
|
+
|
|
159
|
+
// Runtime: from first dispatch to last completion (or now if still running, frozen if paused)
|
|
160
|
+
const t = timings[g.file];
|
|
161
|
+
let runtimeLabel = '';
|
|
162
|
+
if (t && t.firstDispatched) {
|
|
163
|
+
const end = t.allDone && t.lastCompleted ? t.lastCompleted : isBlocked ? (t.lastCompleted || t.firstDispatched) : Date.now();
|
|
164
|
+
const elapsed = end - t.firstDispatched;
|
|
165
|
+
const icon = t.allDone ? '✓' : isBlocked ? '⏸' : '⏱';
|
|
166
|
+
runtimeLabel = '<span style="color:' + (t.allDone ? 'var(--green)' : isBlocked ? 'var(--muted)' : 'var(--yellow)') + ';font-weight:400;font-size:10px">' +
|
|
167
|
+
icon + ' ' + formatDuration(elapsed) + (t.allDone ? '' : isAwaitingApproval ? ' (awaiting approval)' : isPaused ? ' (paused)' : ' elapsed') + '</span>';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const pausedLabel = isAwaitingApproval
|
|
171
|
+
? '<span style="color:var(--yellow);font-weight:600;font-size:10px;padding:1px 6px;border:1px solid var(--yellow);border-radius:3px">AWAITING APPROVAL</span>'
|
|
172
|
+
: isPaused
|
|
173
|
+
? '<span style="color:var(--muted);font-weight:600;font-size:10px;padding:1px 6px;border:1px solid var(--muted);border-radius:3px">PAUSED</span>'
|
|
174
|
+
: '';
|
|
175
|
+
const staleLabel = (!isBlocked && g.planStale)
|
|
176
|
+
? '<span style="color:var(--orange);font-weight:700;font-size:10px;padding:1px 6px;border:1px solid var(--orange);border-radius:3px;background:rgba(210,153,34,0.12)" title="Source plan changed after this PRD was generated">STALE</span>'
|
|
177
|
+
: '';
|
|
178
|
+
const staleRecovery = (!isBlocked && g.planStale)
|
|
179
|
+
? '<div style="width:100%;margin-top:4px;padding:6px 8px;border:1px solid rgba(210,153,34,0.35);border-radius:4px;background:rgba(210,153,34,0.08);display:flex;align-items:center;gap:8px;flex-wrap:wrap">' +
|
|
180
|
+
'<span style="color:var(--orange);font-size:10px;font-weight:600">⚠️ Source plan was revised. This PRD may be outdated.</span>' +
|
|
181
|
+
'<span onclick="event.stopPropagation();prdRegenerate(\'' + escHtml(g.file) + '\')" style="color:var(--green);cursor:pointer;font-size:10px;font-weight:700;padding:2px 8px;background:rgba(63,185,80,0.12);border:1px solid rgba(63,185,80,0.35);border-radius:4px" title="Regenerate PRD from latest plan">Regenerate now</span>' +
|
|
182
|
+
'<span onclick="event.stopPropagation();planView(\'' + escHtml(g.sourcePlan || g.file) + '\')" style="color:var(--blue);cursor:pointer;font-size:10px;padding:2px 8px;background:rgba(56,139,253,0.1);border:1px solid rgba(56,139,253,0.3);border-radius:4px" title="Review latest plan changes">Review plan</span>' +
|
|
183
|
+
'</div>'
|
|
184
|
+
: '';
|
|
185
|
+
const pauseResumeBtn = isPaused
|
|
186
|
+
? '<span onclick="event.stopPropagation();planApprove(\'' + escHtml(g.file) + '\')" style="color:var(--green);cursor:pointer;font-size:9px;padding:1px 6px;background:rgba(63,185,80,0.1);border:1px solid rgba(63,185,80,0.3);border-radius:3px">Resume</span>'
|
|
187
|
+
: '<span onclick="event.stopPropagation();planPause(\'' + escHtml(g.file) + '\')" style="color:var(--yellow);cursor:pointer;font-size:9px;padding:1px 6px;background:rgba(210,153,34,0.1);border:1px solid rgba(210,153,34,0.3);border-radius:3px">Pause</span>';
|
|
188
|
+
const deleteBtn = '<span onclick="event.stopPropagation();planDelete(\'' + escHtml(g.file) + '\')" style="color:var(--red);cursor:pointer;font-size:9px;padding:1px 6px;background:rgba(248,81,73,0.1);border:1px solid rgba(248,81,73,0.3);border-radius:3px">Delete</span>';
|
|
189
|
+
const sourcePlanLink = g.sourcePlan
|
|
190
|
+
? '<span onclick="event.stopPropagation();planView(\'' + escHtml(g.sourcePlan) + '\')" style="color:var(--blue);cursor:pointer;font-size:9px;padding:1px 6px;background:rgba(56,139,253,0.1);border:1px solid rgba(56,139,253,0.3);border-radius:3px" title="View source plan">📄 Plan</span>'
|
|
191
|
+
: '';
|
|
192
|
+
|
|
193
|
+
return '<div style="font-size:11px;font-weight:600;color:var(--blue);margin-bottom:4px;padding:6px 8px;background:var(--surface2);border-radius:4px;display:flex;align-items:center;gap:8px;flex-wrap:wrap">' +
|
|
194
|
+
(g.project ? '<span class="prd-project-badge">' + escHtml(g.project) + '</span>' : '') +
|
|
195
|
+
'<span style="color:var(--text)">' + escHtml(summary || g.file) + '</span>' +
|
|
196
|
+
pausedLabel +
|
|
197
|
+
staleLabel +
|
|
198
|
+
'<span style="color:var(--muted);font-weight:400;font-size:10px">' + g.items.length + ' items' +
|
|
199
|
+
(done ? ' · ' + done + ' done' : '') + (wip ? ' · ' + wip + ' active' : '') +
|
|
200
|
+
'</span>' +
|
|
201
|
+
(g.prdUpdatedAt ? '<span style="color:var(--muted);font-weight:400;font-size:10px" title="PRD last updated: ' + g.prdUpdatedAt + '">PRD updated ' + timeAgo(g.prdUpdatedAt) + '</span>' : '') +
|
|
202
|
+
runtimeLabel +
|
|
203
|
+
(g.archived
|
|
204
|
+
? '<span style="color:var(--muted);font-weight:400;font-size:10px;margin-left:auto;display:flex;align-items:center;gap:6px">' +
|
|
205
|
+
(g.sourcePlan
|
|
206
|
+
? '<span onclick="event.stopPropagation();planView(\'' + escHtml(g.sourcePlan) + '\')" style="color:var(--blue);cursor:pointer;font-size:10px;text-decoration:underline" title="View source plan">📄 ' + escHtml(g.sourcePlan) + '</span>'
|
|
207
|
+
: '<span>' + escHtml(g.file) + '</span>') +
|
|
208
|
+
'</span>'
|
|
209
|
+
: '<span style="color:var(--muted);font-weight:400;font-size:10px;margin-left:auto;display:flex;align-items:center;gap:6px">' +
|
|
210
|
+
sourcePlanLink +
|
|
211
|
+
pauseResumeBtn +
|
|
212
|
+
deleteBtn +
|
|
213
|
+
'</span>') +
|
|
214
|
+
staleRecovery +
|
|
215
|
+
'</div>';
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// Graph view: render items as a dependency DAG
|
|
219
|
+
const renderGraph = (items) => {
|
|
220
|
+
// Build adjacency: compute depth (longest path from root)
|
|
221
|
+
const byId = {};
|
|
222
|
+
items.forEach(i => { byId[i.id] = i; });
|
|
223
|
+
const depths = {};
|
|
224
|
+
function getDepth(id, visited) {
|
|
225
|
+
if (depths[id] !== undefined) return depths[id];
|
|
226
|
+
if (!visited) visited = new Set();
|
|
227
|
+
if (visited.has(id)) return 0;
|
|
228
|
+
visited.add(id);
|
|
229
|
+
const item = byId[id];
|
|
230
|
+
if (!item || !item.depends_on || item.depends_on.length === 0) { depths[id] = 0; return 0; }
|
|
231
|
+
let maxDep = 0;
|
|
232
|
+
for (const depId of item.depends_on) {
|
|
233
|
+
if (byId[depId]) maxDep = Math.max(maxDep, getDepth(depId, visited) + 1);
|
|
234
|
+
}
|
|
235
|
+
depths[id] = maxDep;
|
|
236
|
+
return maxDep;
|
|
237
|
+
}
|
|
238
|
+
items.forEach(i => getDepth(i.id));
|
|
239
|
+
|
|
240
|
+
// Group by depth
|
|
241
|
+
const columns = {};
|
|
242
|
+
let maxDepth = 0;
|
|
243
|
+
items.forEach(i => {
|
|
244
|
+
const d = depths[i.id] || 0;
|
|
245
|
+
if (!columns[d]) columns[d] = [];
|
|
246
|
+
columns[d].push(i);
|
|
247
|
+
if (d > maxDepth) maxDepth = d;
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const statusColor = (s) => {
|
|
251
|
+
if (s === 'done' || s === 'implemented' || s === 'in-pr') return 'var(--green)'; // in-pr treated as done
|
|
252
|
+
if (s === 'in-progress') return 'var(--yellow)';
|
|
253
|
+
if (s === 'failed') return 'var(--red)';
|
|
254
|
+
if (s === 'paused') return 'var(--muted)';
|
|
255
|
+
return 'var(--border)';
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const wi = wiById;
|
|
259
|
+
let html = '<div style="display:flex;gap:8px;padding:8px;min-height:120px">';
|
|
260
|
+
for (let d = 0; d <= maxDepth; d++) {
|
|
261
|
+
const col = columns[d] || [];
|
|
262
|
+
const colLabel = d === 0 ? 'Root' : 'Wave ' + d;
|
|
263
|
+
html += '<div style="flex:1;min-width:0">' +
|
|
264
|
+
'<div style="font-size:9px;color:var(--muted);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:6px;text-align:center">' + colLabel + '</div>';
|
|
265
|
+
for (const i of col) {
|
|
266
|
+
const borderColor = statusColor(i.status);
|
|
267
|
+
const src = escHtml(i.source || '');
|
|
268
|
+
const iid = escHtml(i.id || '');
|
|
269
|
+
const agent = wi[i.id]?.dispatched_to || '';
|
|
270
|
+
const deps = (i.depends_on || []).join(', ');
|
|
271
|
+
const wipAnim = i.status === 'in-progress' ? 'animation:prdWipPulse 2s infinite;' : '';
|
|
272
|
+
html += '<div onclick="prdItemEdit(\'' + src + '\',\'' + iid + '\')" ' +
|
|
273
|
+
'style="background:var(--surface2);border:1px solid var(--border);border-left:3px solid ' + borderColor + ';' + wipAnim +
|
|
274
|
+
'border-radius:4px;padding:6px 8px;margin-bottom:6px;cursor:pointer;font-size:11px">' +
|
|
275
|
+
'<div style="display:flex;align-items:center;gap:4px;margin-bottom:2px">' +
|
|
276
|
+
statusBadge(i.status) +
|
|
277
|
+
'<span style="font-weight:600;color:var(--text)">' + escHtml(i.id) + '</span>' +
|
|
278
|
+
'</div>' +
|
|
279
|
+
'<div style="color:var(--text);font-size:11px;line-height:1.3;margin-bottom:3px;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical">' + escHtml(i.name) + '</div>' +
|
|
280
|
+
'<div style="display:flex;gap:4px;flex-wrap:wrap;align-items:center">' +
|
|
281
|
+
'<span class="prd-item-priority ' + (i.priority || '') + '" style="font-size:8px;padding:1px 4px">' + escHtml(i.priority || '') + '</span>' +
|
|
282
|
+
(agent ? '<span style="font-size:8px;color:var(--muted)">' + escHtml(agent) + '</span>' : '') +
|
|
283
|
+
(function() {
|
|
284
|
+
const w = wi[i.id];
|
|
285
|
+
if (!w) return '';
|
|
286
|
+
const rq = getPrdRequeueState(w.id);
|
|
287
|
+
if (rq && rq.status === 'pending') {
|
|
288
|
+
return '<span style="font-size:8px;color:var(--yellow);cursor:wait;padding:1px 4px;background:rgba(210,153,34,0.1);border:1px solid rgba(210,153,34,0.35);border-radius:3px">requeuing…</span>';
|
|
289
|
+
}
|
|
290
|
+
if (rq && rq.status === 'queued') {
|
|
291
|
+
return '<span style="font-size:8px;color:var(--green);cursor:default;padding:1px 4px;background:rgba(63,185,80,0.1);border:1px solid rgba(63,185,80,0.35);border-radius:3px">requeued</span>';
|
|
292
|
+
}
|
|
293
|
+
if (rq && rq.status === 'error') {
|
|
294
|
+
return '<span style="font-size:8px;color:var(--red);cursor:default;padding:1px 4px;background:rgba(248,81,73,0.1);border:1px solid rgba(248,81,73,0.35);border-radius:3px" title="' + escHtml(rq.message || 'Retry failed') + '">failed</span>';
|
|
295
|
+
}
|
|
296
|
+
if (i.status !== 'failed') return '';
|
|
297
|
+
return '<span onclick="event.stopPropagation();prdItemRequeue(\'' + escHtml(w.id) + '\',\'' + escHtml(w._source || '') + '\')" style="font-size:8px;color:var(--green);cursor:pointer;padding:1px 4px;background:rgba(63,185,80,0.1);border:1px solid rgba(63,185,80,0.3);border-radius:3px">retry</span>';
|
|
298
|
+
})() +
|
|
299
|
+
(deps ? '<span style="font-size:8px;color:var(--muted)" title="Depends on: ' + escHtml(deps) + '">deps: ' + escHtml(deps) + '</span>' : '') +
|
|
300
|
+
'</div>' +
|
|
301
|
+
((i.prs || []).length ? '<div style="margin-top:3px">' + (i.prs || []).map(function(pr) {
|
|
302
|
+
return '<a href="' + escHtml(pr.url || '#') + '" target="_blank" rel="noopener" onclick="event.stopPropagation()" style="font-size:9px;color:var(--green);text-decoration:underline;cursor:pointer" title="' + escHtml(pr.title || '') + '">' + escHtml(pr.id) + '</a>';
|
|
303
|
+
}).join(' ') + '</div>' : '') +
|
|
304
|
+
'</div>';
|
|
305
|
+
}
|
|
306
|
+
html += '</div>';
|
|
307
|
+
if (d < maxDepth) html += '<div style="display:flex;align-items:center;color:var(--border);font-size:14px;padding:0 2px">→</div>';
|
|
308
|
+
}
|
|
309
|
+
html += '</div>';
|
|
310
|
+
return html;
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// View toggle state
|
|
314
|
+
if (!window._prdViewMode) window._prdViewMode = 'graph';
|
|
315
|
+
|
|
316
|
+
const renderViewToggle = () => {
|
|
317
|
+
const isGraph = window._prdViewMode === 'graph';
|
|
318
|
+
return '<div style="display:flex;gap:4px;margin-bottom:8px;padding:0 8px">' +
|
|
319
|
+
'<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;' + (isGraph ? 'background:var(--blue);color:#fff;border-color:var(--blue)' : '') + '" onclick="window._prdViewMode=\'graph\';refresh()">Graph</button>' +
|
|
320
|
+
'<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;' + (!isGraph ? 'background:var(--blue);color:#fff;border-color:var(--blue)' : '') + '" onclick="window._prdViewMode=\'list\';refresh()">List</button>' +
|
|
321
|
+
'</div>';
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
// E2E / verification PRs — group by sourcePlan
|
|
325
|
+
const allPrs = window._lastStatus?.pullRequests || [];
|
|
326
|
+
const e2eByPlan = {};
|
|
327
|
+
for (const pr of allPrs) {
|
|
328
|
+
// Match by explicit sourcePlan (robust), or fall back to title/branch heuristic (legacy)
|
|
329
|
+
const isE2e = pr.itemType === 'verify' || pr.itemType === 'pr' || pr.e2e || pr.title?.startsWith('[E2E]');
|
|
330
|
+
if (!isE2e) continue;
|
|
331
|
+
const planKey = pr.sourcePlan || keys.find(k => pr.branch?.includes(k.replace('.json', ''))) || '_unlinked';
|
|
332
|
+
if (!e2eByPlan[planKey]) e2eByPlan[planKey] = [];
|
|
333
|
+
e2eByPlan[planKey].push(pr);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Find testing guides in prd/ (verify-*.md files)
|
|
337
|
+
const verifyGuides = (window._lastStatus?.verifyGuides || []);
|
|
338
|
+
const guideByPlan = {};
|
|
339
|
+
for (const g of verifyGuides) {
|
|
340
|
+
guideByPlan[g.planFile] = g;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function renderE2eSection(planFile) {
|
|
344
|
+
const prs = e2eByPlan[planFile] || e2eByPlan['_unlinked'] || [];
|
|
345
|
+
const guide = guideByPlan[planFile];
|
|
346
|
+
if (prs.length === 0 && !guide) return '';
|
|
347
|
+
let html = '<div style="margin:6px 0 10px;padding:6px 10px;background:rgba(56,139,253,0.08);border:1px solid rgba(56,139,253,0.25);border-radius:4px">';
|
|
348
|
+
if (prs.length > 0) {
|
|
349
|
+
html += '<div style="font-size:10px;font-weight:600;color:var(--blue);margin-bottom:4px">E2E Aggregate PRs</div>';
|
|
350
|
+
html += prs.map(pr => {
|
|
351
|
+
const statusColor = pr.status === 'active' ? 'var(--green)' : pr.status === 'merged' ? 'var(--purple)' : 'var(--muted)';
|
|
352
|
+
return '<div style="display:flex;align-items:center;gap:6px;padding:2px 0;font-size:11px">' +
|
|
353
|
+
'<span style="color:' + statusColor + ';font-size:8px;font-weight:600;padding:1px 4px;border:1px solid;border-radius:3px">' + escHtml(pr.status || 'active') + '</span>' +
|
|
354
|
+
'<a href="' + escHtml(pr.url || '#') + '" target="_blank" rel="noopener" style="color:var(--blue);text-decoration:underline;font-weight:500">' + escHtml(pr.id) + '</a>' +
|
|
355
|
+
'<span style="color:var(--text);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escHtml(pr.title || '') + '</span>' +
|
|
356
|
+
'<span style="color:var(--muted);font-size:9px">' + escHtml(pr._project || '') + '</span>' +
|
|
357
|
+
'</div>';
|
|
358
|
+
}).join('');
|
|
359
|
+
}
|
|
360
|
+
if (guide) {
|
|
361
|
+
html += '<div style="display:flex;align-items:center;gap:6px;padding:' + (prs.length ? '4px' : '0') + ' 0 0;font-size:11px">' +
|
|
362
|
+
'<span style="font-size:8px;color:var(--green);font-weight:600;padding:1px 4px;border:1px solid var(--green);border-radius:3px">GUIDE</span>' +
|
|
363
|
+
'<span onclick="openVerifyGuide(\'' + escHtml(guide.file) + '\')" style="color:var(--blue);cursor:pointer;text-decoration:underline;font-weight:500">Manual Testing Guide</span>' +
|
|
364
|
+
'<span style="color:var(--muted);font-size:9px">Build instructions, test steps, known issues</span>' +
|
|
365
|
+
'</div>';
|
|
366
|
+
}
|
|
367
|
+
html += '</div>';
|
|
368
|
+
return html;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const activeKeys = keys.filter(k => !grouped[k].archived);
|
|
372
|
+
const archivedKeys = keys.filter(k => grouped[k].archived);
|
|
373
|
+
|
|
374
|
+
function renderGroup(k) {
|
|
375
|
+
const g = grouped[k];
|
|
376
|
+
const viewContent = window._prdViewMode === 'graph'
|
|
377
|
+
? renderGraph(g.items)
|
|
378
|
+
: '<div class="prd-items-list">' + g.items.map(renderItem).join('') + '</div>';
|
|
379
|
+
return '<div style="margin-bottom:16px">' +
|
|
380
|
+
renderGroupHeader(g) +
|
|
381
|
+
renderE2eSection(g.file) +
|
|
382
|
+
renderGroupStats(g.items) +
|
|
383
|
+
viewContent +
|
|
384
|
+
'</div>';
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
let html = renderViewToggle() + activeKeys.map(renderGroup).join('');
|
|
388
|
+
|
|
389
|
+
if (archivedKeys.length > 0) {
|
|
390
|
+
// Store archived data for the modal
|
|
391
|
+
window._archivedPrdGroups = archivedKeys.map(k => grouped[k]);
|
|
392
|
+
window._archivedPrdRenderGroup = renderGroup;
|
|
393
|
+
html += '<div style="margin-top:8px;text-align:right;position:relative" data-file="prd-archives">' +
|
|
394
|
+
'<button class="pr-pager-btn" style="font-size:10px;padding:3px 10px;color:var(--muted)" onclick="openArchivedPrdModal()">' +
|
|
395
|
+
'View Archives (' + archivedKeys.length + ')' +
|
|
396
|
+
'</button>' +
|
|
397
|
+
'</div>';
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
el.innerHTML = html;
|
|
401
|
+
restoreNotifBadges();
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
let _prdItems = []; // cache for edit lookups
|
|
405
|
+
function _cachePrdItems(prog) { _prdItems = prog?.items || []; }
|
|
406
|
+
|
|
407
|
+
function openArchivedPrdModal() {
|
|
408
|
+
const groups = window._archivedPrdGroups || [];
|
|
409
|
+
const renderGroup = window._archivedPrdRenderGroup;
|
|
410
|
+
if (!groups.length || !renderGroup) return;
|
|
411
|
+
|
|
412
|
+
// If only one archived plan, show detail directly
|
|
413
|
+
// If multiple, show a picker first
|
|
414
|
+
let html = '';
|
|
415
|
+
if (groups.length === 1) {
|
|
416
|
+
showArchivedPrdDetail(0);
|
|
417
|
+
return;
|
|
418
|
+
} else {
|
|
419
|
+
// Picker: list archived plans, click to expand
|
|
420
|
+
html = '<div style="margin-bottom:12px;font-size:12px;color:var(--muted)">Select an archived PRD to view:</div>';
|
|
421
|
+
html += groups.map((g, i) => {
|
|
422
|
+
const done = g.items.filter(it => it.status === 'done' || it.status === 'implemented' || it.status === 'complete').length;
|
|
423
|
+
const failed = g.items.filter(it => it.status === 'failed').length;
|
|
424
|
+
return '<div class="plan-card" style="cursor:pointer;margin-bottom:8px" onclick="showArchivedPrdDetail(' + i + ')">' +
|
|
425
|
+
'<div class="plan-card-title" style="font-size:13px">' + escHtml(g.summary || g.file) + '</div>' +
|
|
426
|
+
'<div class="plan-card-meta">' +
|
|
427
|
+
(g.project ? '<span>' + escHtml(g.project) + '</span>' : '') +
|
|
428
|
+
'<span>' + g.items.length + ' items</span>' +
|
|
429
|
+
'<span style="color:var(--green)">' + done + ' done</span>' +
|
|
430
|
+
(failed ? '<span style="color:var(--red)">' + failed + ' failed</span>' : '') +
|
|
431
|
+
'</div>' +
|
|
432
|
+
'</div>';
|
|
433
|
+
}).join('');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
document.getElementById('modal-title').textContent = 'Archived PRDs';
|
|
437
|
+
document.getElementById('modal-body').innerHTML = html;
|
|
438
|
+
document.getElementById('modal-body').style.fontFamily = "'Segoe UI', system-ui, sans-serif";
|
|
439
|
+
document.getElementById('modal-body').style.whiteSpace = 'normal';
|
|
440
|
+
document.getElementById('modal').classList.add('open');
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function showArchivedPrdDetail(idx) {
|
|
444
|
+
const groups = window._archivedPrdGroups || [];
|
|
445
|
+
const g = groups[idx];
|
|
446
|
+
if (!g) return;
|
|
447
|
+
|
|
448
|
+
// View mode toggle for archived
|
|
449
|
+
const isGraph = window._archivedPrdViewMode !== 'list';
|
|
450
|
+
const toggleHtml = '<div style="display:flex;gap:4px;margin-bottom:8px">' +
|
|
451
|
+
'<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;' + (isGraph ? 'background:var(--blue);color:#fff;border-color:var(--blue)' : '') + '" onclick="window._archivedPrdViewMode=\'graph\';showArchivedPrdDetail(' + idx + ')">Graph</button>' +
|
|
452
|
+
'<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;' + (!isGraph ? 'background:var(--blue);color:#fff;border-color:var(--blue)' : '') + '" onclick="window._archivedPrdViewMode=\'list\';showArchivedPrdDetail(' + idx + ')">List</button>' +
|
|
453
|
+
'<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px;color:var(--green);margin-left:auto" onclick="triggerVerify(\'' + escHtml(g.file) + '\')">Trigger Verify</button>' +
|
|
454
|
+
'<button class="pr-pager-btn" style="font-size:9px;padding:2px 8px" onclick="openArchivedPrdModal()">Back</button>' +
|
|
455
|
+
'</div>';
|
|
456
|
+
|
|
457
|
+
// Reuse the renderGroup logic — temporarily swap view mode
|
|
458
|
+
const prevMode = window._prdViewMode;
|
|
459
|
+
window._prdViewMode = isGraph ? 'graph' : 'list';
|
|
460
|
+
const renderGroup = window._archivedPrdRenderGroup;
|
|
461
|
+
const content = renderGroup ? renderGroup(g.file) : '<p>Error rendering</p>';
|
|
462
|
+
window._prdViewMode = prevMode;
|
|
463
|
+
|
|
464
|
+
document.getElementById('modal-title').textContent = g.summary || g.file;
|
|
465
|
+
document.getElementById('modal-body').innerHTML = toggleHtml + content;
|
|
466
|
+
document.getElementById('modal-body').style.fontFamily = "'Segoe UI', system-ui, sans-serif";
|
|
467
|
+
document.getElementById('modal-body').style.whiteSpace = 'normal';
|
|
468
|
+
document.getElementById('modal').classList.add('open');
|
|
469
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// render-prs.js — PR tracker rendering functions extracted from dashboard.html
|
|
2
|
+
|
|
3
|
+
let allPrs = [];
|
|
4
|
+
let prPage = 0;
|
|
5
|
+
const PR_PER_PAGE = 3;
|
|
6
|
+
|
|
7
|
+
function prRow(pr) {
|
|
8
|
+
// Minions review (agent) state — separate from ADO human review
|
|
9
|
+
const sq = pr.minionsReview || {};
|
|
10
|
+
const reviewSource = sq.status || pr.reviewStatus || 'pending';
|
|
11
|
+
const reviewClass = reviewSource === 'approved' ? 'approved' : (reviewSource === 'changes-requested' || reviewSource === 'rejected') ? 'rejected' : reviewSource === 'waiting' ? 'building' : 'draft';
|
|
12
|
+
const reviewLabel = sq.status === 'waiting' ? 'reviewing (minions)' : sq.status ? sq.status + ' (minions)' : (pr.reviewStatus || 'pending');
|
|
13
|
+
const buildClass = pr.buildStatus === 'passing' ? 'build-pass' : pr.buildStatus === 'failing' ? 'build-fail' : pr.buildStatus === 'running' ? 'building' : 'no-build';
|
|
14
|
+
const buildLabel = pr.buildStatus || 'none';
|
|
15
|
+
const statusClass = pr.status === 'merged' ? 'merged' : pr.status === 'abandoned' ? 'rejected' : pr.status === 'active' ? 'active' : 'draft';
|
|
16
|
+
const statusLabel = pr.status || 'active';
|
|
17
|
+
const url = pr.url || '#';
|
|
18
|
+
const prId = pr.id || '—';
|
|
19
|
+
return '<tr>' +
|
|
20
|
+
'<td><span class="pr-id">' + escHtml(String(prId)) + '</span></td>' +
|
|
21
|
+
'<td><a class="pr-title" href="' + escHtml(url) + '" target="_blank">' + escHtml(pr.title || 'Untitled') + '</a></td>' +
|
|
22
|
+
'<td><span class="pr-agent">' + escHtml(pr.agent || '—') + '</span></td>' +
|
|
23
|
+
'<td><span class="pr-branch">' + escHtml(pr.branch || '—') + '</span></td>' +
|
|
24
|
+
'<td><span class="pr-badge ' + reviewClass + '">' + escHtml(reviewLabel) + '</span></td>' +
|
|
25
|
+
'<td>' + (sq.reviewer && sq.status !== 'waiting' ? '<span class="pr-agent" title="' + escHtml(sq.note || '') + '">' + escHtml(sq.reviewer) + '</span>' : sq.reviewer && sq.status === 'waiting' ? '<span class="pr-agent" style="color:var(--muted)" title="Vote pending confirmation">' + escHtml(sq.reviewer) + '…</span>' : '<span style="color:var(--muted);font-size:11px">—</span>') + '</td>' +
|
|
26
|
+
'<td><span class="pr-badge ' + buildClass + '">' + escHtml(buildLabel) + '</span></td>' +
|
|
27
|
+
'<td><span class="pr-badge ' + statusClass + '">' + escHtml(statusLabel) + '</span></td>' +
|
|
28
|
+
'<td><span class="pr-date">' + escHtml(pr.created || '—') + '</span></td>' +
|
|
29
|
+
'</tr>';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function prTableHtml(rows) {
|
|
33
|
+
return '<div class="pr-table-wrap"><table class="pr-table"><thead><tr>' +
|
|
34
|
+
'<th>PR</th><th>Title</th><th>Agent</th><th>Branch</th><th>Review</th><th>Signed Off By</th><th>Build</th><th>Status</th><th>Created</th><th></th>' +
|
|
35
|
+
'</tr></thead><tbody>' + rows + '</tbody></table></div>';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function renderPrs(prs) {
|
|
39
|
+
allPrs = prs;
|
|
40
|
+
const el = document.getElementById('pr-content');
|
|
41
|
+
const count = document.getElementById('pr-count');
|
|
42
|
+
count.textContent = prs.length;
|
|
43
|
+
if (!prs.length) {
|
|
44
|
+
el.innerHTML = '<p class="pr-empty">No pull requests yet. PRs created by agents will appear here with review, build, and merge status.</p>';
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const totalPages = Math.ceil(prs.length / PR_PER_PAGE);
|
|
48
|
+
if (prPage >= totalPages) prPage = totalPages - 1;
|
|
49
|
+
const start = prPage * PR_PER_PAGE;
|
|
50
|
+
const pagePrs = prs.slice(start, start + PR_PER_PAGE);
|
|
51
|
+
const rows = pagePrs.map(prRow).join('');
|
|
52
|
+
|
|
53
|
+
let pager = '';
|
|
54
|
+
if (prs.length > PR_PER_PAGE) {
|
|
55
|
+
pager = '<div class="pr-pager">' +
|
|
56
|
+
'<span class="pr-page-info">Showing ' + (start+1) + ' to ' + Math.min(start+PR_PER_PAGE, prs.length) + ' of ' + prs.length + '</span>' +
|
|
57
|
+
'<div class="pr-pager-btns">' +
|
|
58
|
+
'<button class="pr-pager-btn ' + (prPage === 0 ? 'disabled' : '') + '" onclick="prPrev()">Prev</button>' +
|
|
59
|
+
'<button class="pr-pager-btn ' + (prPage >= totalPages-1 ? 'disabled' : '') + '" onclick="prNext()">Next</button>' +
|
|
60
|
+
'<button class="pr-pager-btn see-all" onclick="openAllPrs()">See all ' + prs.length + ' PRs</button>' +
|
|
61
|
+
'</div>' +
|
|
62
|
+
'</div>';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
el.innerHTML = prTableHtml(rows) + pager;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function prPrev() { if (prPage > 0) { prPage--; renderPrs(allPrs); } }
|
|
69
|
+
function prNext() { const totalPages = Math.ceil(allPrs.length / PR_PER_PAGE); if (prPage < totalPages-1) { prPage++; renderPrs(allPrs); } }
|
|
70
|
+
|
|
71
|
+
function openAllPrs() {
|
|
72
|
+
const modalEl = document.querySelector('#modal .modal');
|
|
73
|
+
if (modalEl) modalEl.classList.add('modal-wide');
|
|
74
|
+
document.getElementById('modal-title').textContent = 'All Pull Requests (' + allPrs.length + ')';
|
|
75
|
+
document.getElementById('modal-body').innerHTML = prTableHtml(allPrs.map(prRow).join(''));
|
|
76
|
+
document.getElementById('modal-body').style.fontFamily = "'Segoe UI', system-ui, sans-serif";
|
|
77
|
+
document.getElementById('modal-body').style.whiteSpace = 'normal';
|
|
78
|
+
document.getElementById('modal').classList.add('open');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function openModal(i) {
|
|
82
|
+
const item = inboxData[i];
|
|
83
|
+
if (!item) return;
|
|
84
|
+
document.getElementById('modal-title').textContent = item.name;
|
|
85
|
+
document.getElementById('modal-body').innerHTML =
|
|
86
|
+
'<div style="margin-bottom:12px"><button class="pr-pager-btn" style="font-size:10px;padding:3px 10px" onclick="promoteToKB(\'' + escHtml(item.name) + '\')">Add to Knowledge Base</button></div>' +
|
|
87
|
+
'<pre style="white-space:pre-wrap;word-wrap:break-word;margin:0;font-family:Consolas,monospace;font-size:12px;line-height:1.7;color:var(--muted)">' + escHtml(item.content) + '</pre>';
|
|
88
|
+
_modalDocContext = { title: item.name, content: item.content, selection: '' };
|
|
89
|
+
_modalFilePath = 'notes/inbox/' + item.name; showModalQa();
|
|
90
|
+
// Clear notification badge when opening this document
|
|
91
|
+
const card = findCardForFile(_modalFilePath);
|
|
92
|
+
if (card) clearNotifBadge(card);
|
|
93
|
+
document.getElementById('modal').classList.add('open');
|
|
94
|
+
}
|