@oss-autopilot/core 0.44.18 → 0.46.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.
Files changed (46) hide show
  1. package/dist/cli-registry.js +78 -0
  2. package/dist/cli.bundle.cjs +58 -1396
  3. package/dist/cli.bundle.cjs.map +4 -4
  4. package/dist/commands/dashboard-lifecycle.js +1 -1
  5. package/dist/commands/dashboard-process.d.ts +19 -0
  6. package/dist/commands/dashboard-process.js +93 -0
  7. package/dist/commands/dashboard-server.d.ts +1 -15
  8. package/dist/commands/dashboard-server.js +27 -84
  9. package/dist/commands/dashboard.d.ts +0 -10
  10. package/dist/commands/dashboard.js +1 -27
  11. package/dist/commands/index.d.ts +52 -8
  12. package/dist/commands/index.js +57 -9
  13. package/dist/commands/pr-template.d.ts +9 -0
  14. package/dist/commands/pr-template.js +14 -0
  15. package/dist/commands/rate-limiter.d.ts +31 -0
  16. package/dist/commands/rate-limiter.js +36 -0
  17. package/dist/commands/startup.d.ts +1 -1
  18. package/dist/commands/startup.js +21 -22
  19. package/dist/commands/stats.d.ts +15 -0
  20. package/dist/commands/stats.js +57 -0
  21. package/dist/core/github-stats.d.ts +0 -4
  22. package/dist/core/github-stats.js +1 -9
  23. package/dist/core/index.d.ts +3 -1
  24. package/dist/core/index.js +3 -1
  25. package/dist/core/pr-monitor.js +3 -13
  26. package/dist/core/pr-template.d.ts +27 -0
  27. package/dist/core/pr-template.js +65 -0
  28. package/dist/core/state.d.ts +20 -17
  29. package/dist/core/state.js +29 -30
  30. package/dist/core/stats.d.ts +25 -0
  31. package/dist/core/stats.js +33 -0
  32. package/dist/core/types.d.ts +2 -2
  33. package/dist/core/utils.d.ts +16 -9
  34. package/dist/core/utils.js +45 -11
  35. package/dist/formatters/json.d.ts +8 -2
  36. package/package.json +1 -1
  37. package/dist/commands/dashboard-components.d.ts +0 -33
  38. package/dist/commands/dashboard-components.js +0 -57
  39. package/dist/commands/dashboard-formatters.d.ts +0 -12
  40. package/dist/commands/dashboard-formatters.js +0 -19
  41. package/dist/commands/dashboard-scripts.d.ts +0 -7
  42. package/dist/commands/dashboard-scripts.js +0 -281
  43. package/dist/commands/dashboard-styles.d.ts +0 -5
  44. package/dist/commands/dashboard-styles.js +0 -765
  45. package/dist/commands/dashboard-templates.d.ts +0 -12
  46. package/dist/commands/dashboard-templates.js +0 -470
@@ -1,12 +0,0 @@
1
- /**
2
- * Dashboard HTML template assembly.
3
- * Imports styles, components, formatters, and scripts from focused sub-modules,
4
- * then weaves them into the final HTML document.
5
- *
6
- * Pure functions with no side effects — all data is passed in as arguments.
7
- */
8
- import type { DailyDigest, AgentState, CommentedIssueWithResponse } from '../core/types.js';
9
- import type { DashboardStats } from './dashboard-data.js';
10
- export { escapeHtml } from './dashboard-formatters.js';
11
- export { buildDashboardStats, type DashboardStats } from './dashboard-data.js';
12
- export declare function generateDashboardHtml(stats: DashboardStats, monthlyMerged: Record<string, number>, monthlyClosed: Record<string, number>, monthlyOpened: Record<string, number>, digest: DailyDigest, state: Readonly<AgentState>, issueResponses?: CommentedIssueWithResponse[]): string;
@@ -1,470 +0,0 @@
1
- /**
2
- * Dashboard HTML template assembly.
3
- * Imports styles, components, formatters, and scripts from focused sub-modules,
4
- * then weaves them into the final HTML document.
5
- *
6
- * Pure functions with no side effects — all data is passed in as arguments.
7
- */
8
- import { escapeHtml } from './dashboard-formatters.js';
9
- import { DASHBOARD_CSS } from './dashboard-styles.js';
10
- import { SVG_ICONS, truncateTitle, renderHealthItems, titleMeta } from './dashboard-components.js';
11
- import { generateDashboardScripts } from './dashboard-scripts.js';
12
- // Re-export public API so consumers can import from this module
13
- export { escapeHtml } from './dashboard-formatters.js';
14
- export { buildDashboardStats } from './dashboard-data.js';
15
- export function generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpened, digest, state, issueResponses = []) {
16
- const shelvedPRs = digest.shelvedPRs || [];
17
- const autoUnshelvedPRs = digest.autoUnshelvedPRs || [];
18
- const recentlyMerged = digest.recentlyMergedPRs || [];
19
- const shelvedUrls = new Set(shelvedPRs.map((pr) => pr.url));
20
- const activePRList = (digest.openPRs || []).filter((pr) => !shelvedUrls.has(pr.url));
21
- // Action Required: contributor must do something
22
- const actionRequired = digest.needsAddressingPRs || [];
23
- // Waiting on Others: informational, no contributor action needed
24
- const waitingOnOthers = digest.waitingOnMaintainerPRs || [];
25
- return `<!DOCTYPE html>
26
- <html lang="en">
27
- <head>
28
- <meta charset="UTF-8">
29
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
30
- <title>OSS Autopilot - Mission Control</title>
31
- <script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.1" integrity="sha384-jb8JQMbMoBUzgWatfe6COACi2ljcDdZQ2OxczGA3bGNeWe+6DChMTBJemed7ZnvJ" crossorigin="anonymous"></script>
32
- <link rel="preconnect" href="https://fonts.googleapis.com">
33
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
34
- <link href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet">
35
- <style>${DASHBOARD_CSS}
36
- </style>
37
- </head>
38
- <body>
39
- <div class="container">
40
- <header class="header">
41
- <div class="header-left">
42
- <div class="logo">
43
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
44
- <circle cx="12" cy="12" r="10"/>
45
- <path d="M12 6v6l4 2"/>
46
- </svg>
47
- </div>
48
- <div>
49
- <h1>OSS Autopilot</h1>
50
- <span class="header-subtitle">Mission Control</span>
51
- </div>
52
- </div>
53
- <div class="header-controls">
54
- <div class="timestamp">
55
- Last updated: ${digest.generatedAt
56
- ? new Date(digest.generatedAt).toLocaleString('en-US', {
57
- weekday: 'short',
58
- month: 'short',
59
- day: 'numeric',
60
- year: 'numeric',
61
- hour: '2-digit',
62
- minute: '2-digit',
63
- second: '2-digit',
64
- hour12: false,
65
- })
66
- : 'Unknown'}
67
- </div>
68
- <button class="theme-toggle" id="themeToggle" title="Toggle light/dark mode">
69
- <svg id="themeIconSun" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
70
- <circle cx="12" cy="12" r="5"/>
71
- <line x1="12" y1="1" x2="12" y2="3"/>
72
- <line x1="12" y1="21" x2="12" y2="23"/>
73
- <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
74
- <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
75
- <line x1="1" y1="12" x2="3" y2="12"/>
76
- <line x1="21" y1="12" x2="23" y2="12"/>
77
- <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
78
- <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
79
- </svg>
80
- <svg id="themeIconMoon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none;">
81
- <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
82
- </svg>
83
- <span id="themeLabel">Light</span>
84
- </button>
85
- </div>
86
- </header>
87
-
88
- <div class="stats-grid">
89
- <div class="stat-card active">
90
- <div class="stat-value">${stats.activePRs}</div>
91
- <div class="stat-label">Active PRs</div>
92
- </div>
93
- <div class="stat-card shelved">
94
- <div class="stat-value">${stats.shelvedPRs}</div>
95
- <div class="stat-label">Shelved</div>
96
- </div>
97
- <div class="stat-card merged">
98
- <div class="stat-value">${stats.mergedPRs}</div>
99
- <div class="stat-label">Merged</div>
100
- </div>
101
- <div class="stat-card closed">
102
- <div class="stat-value">${stats.closedPRs}</div>
103
- <div class="stat-label">Closed</div>
104
- </div>
105
- <div class="stat-card rate">
106
- <div class="stat-value">${stats.mergeRate}</div>
107
- <div class="stat-label">Merge Rate</div>
108
- </div>
109
- </div>
110
-
111
- <div class="filter-toolbar" id="filterToolbar">
112
- <label>Filters</label>
113
- <input type="text" class="filter-search" id="searchInput" placeholder="Search by PR title..." />
114
- <select class="filter-select" id="statusFilter">
115
- <option value="all">All Statuses</option>
116
- <option value="needs-addressing">Needs Addressing</option>
117
- <option value="waiting-on-maintainer">Waiting on Maintainer</option>
118
- <option value="shelved">Shelved</option>
119
- <option value="merged">Recently Merged</option>
120
- <option value="closed">Recently Closed</option>
121
- <option value="auto-unshelved">Auto-Unshelved</option>
122
- </select>
123
- <select class="filter-select" id="repoFilter">
124
- <option value="all">All Repositories</option>
125
- ${(() => {
126
- const repos = new Set();
127
- for (const pr of activePRList)
128
- repos.add(pr.repo);
129
- for (const pr of shelvedPRs)
130
- repos.add(pr.repo);
131
- for (const pr of actionRequired)
132
- repos.add(pr.repo);
133
- for (const pr of waitingOnOthers)
134
- repos.add(pr.repo);
135
- for (const pr of recentlyMerged)
136
- repos.add(pr.repo);
137
- for (const pr of digest.recentlyClosedPRs || [])
138
- repos.add(pr.repo);
139
- for (const pr of autoUnshelvedPRs)
140
- repos.add(pr.repo);
141
- return Array.from(repos)
142
- .sort()
143
- .map((repo) => `<option value="${escapeHtml(repo)}">${escapeHtml(repo)}</option>`)
144
- .join('\n ');
145
- })()}
146
- </select>
147
- <span class="filter-count" id="filterCount"></span>
148
- </div>
149
-
150
- ${actionRequired.length > 0
151
- ? `
152
- <section class="health-section">
153
- <div class="health-header">
154
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--accent-warning)" stroke-width="2">
155
- <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
156
- <line x1="12" y1="9" x2="12" y2="13"/>
157
- <line x1="12" y1="17" x2="12.01" y2="17"/>
158
- </svg>
159
- <h2>Action Required</h2>
160
- <span class="health-badge">${actionRequired.length} issue${actionRequired.length !== 1 ? 's' : ''}</span>
161
- </div>
162
- <div class="health-items">
163
- ${renderHealthItems(actionRequired, 'needs-addressing', SVG_ICONS.xCircle, (pr) => pr.displayLabel, (pr) => escapeHtml(pr.displayDescription))}
164
- </div>
165
- </section>
166
- `
167
- : ''}
168
-
169
- ${waitingOnOthers.length > 0
170
- ? `
171
- <section class="health-section waiting-section">
172
- <div class="health-header">
173
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--accent-info)" stroke-width="2">
174
- <circle cx="12" cy="12" r="10"/>
175
- <polyline points="12 6 12 12 16 14"/>
176
- </svg>
177
- <h2>Waiting on Others</h2>
178
- <span class="health-badge" style="background: var(--accent-info-dim); color: var(--accent-info);">${waitingOnOthers.length} PR${waitingOnOthers.length !== 1 ? 's' : ''}</span>
179
- </div>
180
- <div class="health-items">
181
- ${renderHealthItems(waitingOnOthers, 'waiting-on-maintainer', SVG_ICONS.clock, (pr) => pr.displayLabel, (pr) => escapeHtml(pr.displayDescription))}
182
- </div>
183
- </section>
184
- `
185
- : ''}
186
-
187
- ${actionRequired.length === 0 && waitingOnOthers.length === 0
188
- ? `
189
- <section class="health-section">
190
- <div class="health-header">
191
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--accent-open)" stroke-width="2">
192
- <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
193
- <polyline points="22 4 12 14.01 9 11.01"/>
194
- </svg>
195
- <h2>Health Status</h2>
196
- </div>
197
- <div class="health-empty">
198
- All PRs are on track - no CI failures, conflicts, or pending responses
199
- </div>
200
- </section>
201
- `
202
- : ''}
203
-
204
- ${recentlyMerged.length > 0
205
- ? `
206
- <section class="health-section" style="animation-delay: 0.15s;">
207
- <div class="health-header">
208
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--accent-merged)" stroke-width="2">
209
- ${SVG_ICONS.gitMerge}
210
- </svg>
211
- <h2>Recently Merged</h2>
212
- <span class="health-badge" style="background: var(--accent-merged-dim); color: var(--accent-merged);">${recentlyMerged.length} merged</span>
213
- </div>
214
- <div class="health-items">
215
- ${recentlyMerged
216
- .map((pr) => `
217
- <div class="health-item" style="border-left-color: var(--accent-merged);" data-status="merged" data-repo="${escapeHtml(pr.repo)}" data-title="${escapeHtml(pr.title.toLowerCase())}">
218
- <div class="health-icon" style="background: var(--accent-merged-dim); color: var(--accent-merged);">
219
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
220
- ${SVG_ICONS.gitMerge}
221
- </svg>
222
- </div>
223
- <div class="health-content">
224
- <div class="health-title"><a href="${escapeHtml(pr.url)}" target="_blank">${escapeHtml(pr.repo)}#${pr.number}</a> - Merged</div>
225
- <div class="health-meta">${truncateTitle(pr.title)}${pr.mergedAt ? ` · ${new Date(pr.mergedAt).toLocaleDateString()}` : ''}</div>
226
- </div>
227
- </div>
228
- `)
229
- .join('')}
230
- </div>
231
- </section>
232
- `
233
- : ''}
234
-
235
- ${(digest.recentlyClosedPRs || []).length > 0
236
- ? `
237
- <section class="health-section" style="animation-delay: 0.2s;">
238
- <div class="health-header">
239
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--text-muted)" stroke-width="2">
240
- <circle cx="12" cy="12" r="10"/>
241
- <line x1="15" y1="9" x2="9" y2="15"/>
242
- <line x1="9" y1="9" x2="15" y2="15"/>
243
- </svg>
244
- <h2>Recently Closed</h2>
245
- <span class="health-badge" style="background: rgba(110, 118, 129, 0.15); color: var(--text-muted);">${(digest.recentlyClosedPRs || []).length} closed</span>
246
- </div>
247
- <div class="health-items">
248
- ${(digest.recentlyClosedPRs || [])
249
- .map((pr) => `
250
- <div class="health-item" style="border-left-color: var(--text-muted); opacity: 0.7;" data-status="closed" data-repo="${escapeHtml(pr.repo)}" data-title="${escapeHtml(pr.title.toLowerCase())}">
251
- <div class="health-icon" style="background: rgba(110, 118, 129, 0.15); color: var(--text-muted);">
252
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
253
- <circle cx="12" cy="12" r="10"/>
254
- <line x1="15" y1="9" x2="9" y2="15"/>
255
- <line x1="9" y1="9" x2="15" y2="15"/>
256
- </svg>
257
- </div>
258
- <div class="health-content">
259
- <div class="health-title"><a href="${escapeHtml(pr.url)}" target="_blank">${escapeHtml(pr.repo)}#${pr.number}</a> - Closed</div>
260
- <div class="health-meta">${truncateTitle(pr.title)}${pr.closedAt ? ` · ${new Date(pr.closedAt).toLocaleDateString()}` : ''}</div>
261
- </div>
262
- </div>
263
- `)
264
- .join('')}
265
- </div>
266
- </section>
267
- `
268
- : ''}
269
-
270
- ${autoUnshelvedPRs.length > 0
271
- ? `
272
- <section class="health-section" style="animation-delay: 0.25s;">
273
- <div class="health-header">
274
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--accent-info)" stroke-width="2">
275
- ${SVG_ICONS.bell}
276
- </svg>
277
- <h2>Auto-Unshelved</h2>
278
- <span class="health-badge" style="background: var(--accent-info-dim); color: var(--accent-info);">${autoUnshelvedPRs.length} unshelved</span>
279
- </div>
280
- <div class="health-items">
281
- ${renderHealthItems(autoUnshelvedPRs, 'auto-unshelved', SVG_ICONS.bell, (pr) => `Auto-Unshelved (${pr.status.replace(/_/g, ' ')})`, titleMeta)}
282
- </div>
283
- </section>
284
- `
285
- : ''}
286
-
287
- ${issueResponses.length > 0
288
- ? `
289
- <section class="health-section" style="animation-delay: 0.3s;">
290
- <div class="health-header">
291
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--accent-info)" stroke-width="2">
292
- ${SVG_ICONS.comment}
293
- </svg>
294
- <h2>Issue Conversations</h2>
295
- <span class="health-badge" style="background: var(--accent-info-dim); color: var(--accent-info);">${issueResponses.length} repl${issueResponses.length !== 1 ? 'ies' : 'y'}</span>
296
- </div>
297
- <div class="health-items">
298
- ${issueResponses
299
- .map((issue) => `
300
- <div class="health-item changes-addressed">
301
- <div class="health-icon" style="background: var(--accent-info-dim); color: var(--accent-info);">
302
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
303
- ${SVG_ICONS.comment}
304
- </svg>
305
- </div>
306
- <div class="health-content">
307
- <div class="health-title"><a href="${escapeHtml(issue.url)}" target="_blank">${escapeHtml(issue.repo)}#${issue.number}</a> - ${escapeHtml(issue.title.slice(0, 50))}${issue.title.length > 50 ? '...' : ''}</div>
308
- <div class="health-meta">@${escapeHtml(issue.lastResponseAuthor)}: ${escapeHtml(issue.lastResponseBody.slice(0, 60))}${issue.lastResponseBody.length > 60 ? '...' : ''}</div>
309
- </div>
310
- </div>
311
- `)
312
- .join('')}
313
- </div>
314
- </section>
315
- `
316
- : ''}
317
-
318
- <div class="main-grid">
319
- <div class="card">
320
- <div class="card-header">
321
- <span class="card-title">PR Status Distribution</span>
322
- </div>
323
- <div class="card-body">
324
- <div class="chart-container">
325
- <canvas id="statusChart"></canvas>
326
- </div>
327
- </div>
328
- </div>
329
-
330
- <div class="card">
331
- <div class="card-header">
332
- <span class="card-title">Repository Breakdown</span>
333
- </div>
334
- <div class="card-body">
335
- <div class="chart-container">
336
- <canvas id="reposChart"></canvas>
337
- </div>
338
- </div>
339
- </div>
340
- </div>
341
-
342
- <div class="card" style="margin-bottom: 1.25rem;">
343
- <div class="card-header">
344
- <span class="card-title">Contribution Timeline</span>
345
- </div>
346
- <div class="card-body">
347
- <div class="chart-container" style="height: 250px;">
348
- <canvas id="monthlyChart"></canvas>
349
- </div>
350
- </div>
351
- </div>
352
-
353
-
354
- ${activePRList.length > 0
355
- ? `
356
- <section class="pr-list-section">
357
- <div class="pr-list-header">
358
- <h2 class="pr-list-title">Active Pull Requests</h2>
359
- <span class="pr-count">${activePRList.length} open</span>
360
- </div>
361
- <div class="pr-list">
362
- ${activePRList
363
- .map((pr) => {
364
- const isNeedsAddressing = pr.status === 'needs_addressing';
365
- const isStale = pr.stalenessTier !== 'active';
366
- const itemClass = isNeedsAddressing ? 'has-issues' : isStale ? 'stale' : '';
367
- const prStatus = pr.status === 'needs_addressing' ? 'needs-addressing' : 'waiting-on-maintainer';
368
- return `
369
- <div class="pr-item ${itemClass}" data-status="${prStatus}" data-repo="${escapeHtml(pr.repo)}" data-title="${escapeHtml(pr.title.toLowerCase())}">
370
- <div class="pr-status-indicator">
371
- ${isNeedsAddressing
372
- ? `
373
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
374
- <circle cx="12" cy="12" r="10"/>
375
- <line x1="12" y1="8" x2="12" y2="12"/>
376
- <line x1="12" y1="16" x2="12.01" y2="16"/>
377
- </svg>
378
- `
379
- : `
380
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
381
- <circle cx="12" cy="12" r="10"/>
382
- <line x1="12" y1="16" x2="12" y2="12"/>
383
- <line x1="12" y1="8" x2="12.01" y2="8"/>
384
- </svg>
385
- `}
386
- </div>
387
- <div class="pr-content">
388
- <div class="pr-title-row">
389
- <a href="${escapeHtml(pr.url)}" target="_blank" class="pr-title">${escapeHtml(pr.title)}</a>
390
- <span class="pr-repo">${escapeHtml(pr.repo)}#${pr.number}</span>
391
- </div>
392
- <div class="pr-badges">
393
- <span class="badge ${isNeedsAddressing ? 'badge-ci-failing' : 'badge-passing'}">${escapeHtml(pr.displayLabel)}</span>
394
- ${isStale ? `<span class="badge badge-stale">${pr.daysSinceActivity}d inactive</span>` : ''}
395
- </div>
396
- </div>
397
- <div class="pr-activity">
398
- ${pr.daysSinceActivity === 0 ? 'Today' : pr.daysSinceActivity === 1 ? 'Yesterday' : pr.daysSinceActivity + 'd ago'}
399
- </div>
400
- </div>`;
401
- })
402
- .join('')}
403
- </div>
404
- </section>
405
- `
406
- : `
407
- <section class="pr-list-section">
408
- <div class="pr-list-header">
409
- <h2 class="pr-list-title">Active Pull Requests</h2>
410
- <span class="pr-count">0 open</span>
411
- </div>
412
- <div class="empty-state">
413
- <div class="empty-state-icon">
414
- <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
415
- <circle cx="12" cy="12" r="10"/>
416
- <path d="M8 12h8"/>
417
- </svg>
418
- </div>
419
- <p>No active pull requests</p>
420
- </div>
421
- </section>
422
- `}
423
-
424
- ${shelvedPRs.length > 0
425
- ? `
426
- <section class="pr-list-section" style="margin-top: 1.25rem; opacity: 0.7;">
427
- <div class="pr-list-header">
428
- <h2 class="pr-list-title">Shelved Pull Requests</h2>
429
- <span class="pr-count" style="background: rgba(110, 118, 129, 0.15); color: var(--text-muted);">${shelvedPRs.length} shelved</span>
430
- </div>
431
- <div class="pr-list">
432
- ${shelvedPRs
433
- .map((pr) => `
434
- <div class="pr-item" data-status="shelved" data-repo="${escapeHtml(pr.repo)}" data-title="${escapeHtml(pr.title.toLowerCase())}">
435
- <div class="pr-status-indicator" style="background: rgba(110, 118, 129, 0.1); color: var(--text-muted);">
436
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
437
- ${SVG_ICONS.box}
438
- </svg>
439
- </div>
440
- <div class="pr-content">
441
- <div class="pr-title-row">
442
- <a href="${escapeHtml(pr.url)}" target="_blank" class="pr-title">${escapeHtml(pr.title)}</a>
443
- <span class="pr-repo">${escapeHtml(pr.repo)}#${pr.number}</span>
444
- </div>
445
- <div class="pr-badges">
446
- <span class="badge badge-days">${pr.daysSinceActivity}d inactive</span>
447
- </div>
448
- </div>
449
- <div class="pr-activity">
450
- ${pr.daysSinceActivity === 0 ? 'Today' : pr.daysSinceActivity === 1 ? 'Yesterday' : pr.daysSinceActivity + 'd ago'}
451
- </div>
452
- </div>`)
453
- .join('')}
454
- </div>
455
- </section>
456
- `
457
- : ''}
458
-
459
- <footer class="footer">
460
- <p>OSS Autopilot // Mission Control</p>
461
- <p style="margin-top: 0.25rem;">Dashboard generated: ${digest.generatedAt ? new Date(digest.generatedAt).toISOString() : 'Unknown'}</p>
462
- </footer>
463
- </div>
464
-
465
- <script>
466
- ${generateDashboardScripts(stats, monthlyMerged, monthlyClosed, monthlyOpened, digest, state)}
467
- </script>
468
- </body>
469
- </html>`;
470
- }