@oss-autopilot/core 0.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +85 -0
- package/dist/cli.bundle.cjs +17657 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.js +325 -0
- package/dist/commands/check-integration.d.ts +10 -0
- package/dist/commands/check-integration.js +192 -0
- package/dist/commands/comments.d.ts +24 -0
- package/dist/commands/comments.js +311 -0
- package/dist/commands/config.d.ts +11 -0
- package/dist/commands/config.js +82 -0
- package/dist/commands/daily.d.ts +29 -0
- package/dist/commands/daily.js +433 -0
- package/dist/commands/dashboard-data.d.ts +45 -0
- package/dist/commands/dashboard-data.js +132 -0
- package/dist/commands/dashboard-templates.d.ts +23 -0
- package/dist/commands/dashboard-templates.js +1627 -0
- package/dist/commands/dashboard.d.ts +18 -0
- package/dist/commands/dashboard.js +134 -0
- package/dist/commands/dismiss.d.ts +13 -0
- package/dist/commands/dismiss.js +49 -0
- package/dist/commands/init.d.ts +10 -0
- package/dist/commands/init.js +27 -0
- package/dist/commands/local-repos.d.ts +14 -0
- package/dist/commands/local-repos.js +155 -0
- package/dist/commands/parse-list.d.ts +13 -0
- package/dist/commands/parse-list.js +139 -0
- package/dist/commands/read.d.ts +12 -0
- package/dist/commands/read.js +33 -0
- package/dist/commands/search.d.ts +10 -0
- package/dist/commands/search.js +74 -0
- package/dist/commands/setup.d.ts +15 -0
- package/dist/commands/setup.js +276 -0
- package/dist/commands/shelve.d.ts +13 -0
- package/dist/commands/shelve.js +49 -0
- package/dist/commands/snooze.d.ts +18 -0
- package/dist/commands/snooze.js +83 -0
- package/dist/commands/startup.d.ts +33 -0
- package/dist/commands/startup.js +197 -0
- package/dist/commands/status.d.ts +10 -0
- package/dist/commands/status.js +43 -0
- package/dist/commands/track.d.ts +16 -0
- package/dist/commands/track.js +59 -0
- package/dist/commands/validation.d.ts +43 -0
- package/dist/commands/validation.js +112 -0
- package/dist/commands/vet.d.ts +10 -0
- package/dist/commands/vet.js +36 -0
- package/dist/core/checklist-analysis.d.ts +17 -0
- package/dist/core/checklist-analysis.js +39 -0
- package/dist/core/ci-analysis.d.ts +78 -0
- package/dist/core/ci-analysis.js +163 -0
- package/dist/core/comment-utils.d.ts +15 -0
- package/dist/core/comment-utils.js +52 -0
- package/dist/core/concurrency.d.ts +5 -0
- package/dist/core/concurrency.js +15 -0
- package/dist/core/daily-logic.d.ts +77 -0
- package/dist/core/daily-logic.js +512 -0
- package/dist/core/display-utils.d.ts +10 -0
- package/dist/core/display-utils.js +100 -0
- package/dist/core/errors.d.ts +24 -0
- package/dist/core/errors.js +34 -0
- package/dist/core/github-stats.d.ts +73 -0
- package/dist/core/github-stats.js +272 -0
- package/dist/core/github.d.ts +19 -0
- package/dist/core/github.js +60 -0
- package/dist/core/http-cache.d.ts +97 -0
- package/dist/core/http-cache.js +269 -0
- package/dist/core/index.d.ts +15 -0
- package/dist/core/index.js +15 -0
- package/dist/core/issue-conversation.d.ts +29 -0
- package/dist/core/issue-conversation.js +231 -0
- package/dist/core/issue-discovery.d.ts +85 -0
- package/dist/core/issue-discovery.js +589 -0
- package/dist/core/issue-filtering.d.ts +51 -0
- package/dist/core/issue-filtering.js +103 -0
- package/dist/core/issue-scoring.d.ts +40 -0
- package/dist/core/issue-scoring.js +92 -0
- package/dist/core/issue-vetting.d.ts +49 -0
- package/dist/core/issue-vetting.js +536 -0
- package/dist/core/logger.d.ts +21 -0
- package/dist/core/logger.js +49 -0
- package/dist/core/maintainer-analysis.d.ts +10 -0
- package/dist/core/maintainer-analysis.js +59 -0
- package/dist/core/pagination.d.ts +11 -0
- package/dist/core/pagination.js +20 -0
- package/dist/core/pr-monitor.d.ts +109 -0
- package/dist/core/pr-monitor.js +594 -0
- package/dist/core/review-analysis.d.ts +72 -0
- package/dist/core/review-analysis.js +163 -0
- package/dist/core/state.d.ts +371 -0
- package/dist/core/state.js +1089 -0
- package/dist/core/types.d.ts +507 -0
- package/dist/core/types.js +34 -0
- package/dist/core/utils.d.ts +249 -0
- package/dist/core/utils.js +422 -0
- package/dist/formatters/json.d.ts +269 -0
- package/dist/formatters/json.js +88 -0
- package/package.json +67 -0
|
@@ -0,0 +1,1627 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard HTML template generation.
|
|
3
|
+
* Contains all HTML/CSS/JS template strings, escapeHtml(), and rendering helpers.
|
|
4
|
+
* Pure functions with no side effects — all data is passed in as arguments.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Escape HTML special characters to prevent XSS when interpolating
|
|
8
|
+
* user-controlled content (e.g. PR titles, comment bodies, author names) into HTML.
|
|
9
|
+
* Note: This escapes HTML entity characters only. It does not sanitize URL schemes
|
|
10
|
+
* (e.g., javascript:) — callers placing values in href attributes should validate
|
|
11
|
+
* the URL scheme if the source is untrusted. GitHub API URLs are trusted.
|
|
12
|
+
*/
|
|
13
|
+
export function escapeHtml(text) {
|
|
14
|
+
return text
|
|
15
|
+
.replace(/&/g, '&')
|
|
16
|
+
.replace(/</g, '<')
|
|
17
|
+
.replace(/>/g, '>')
|
|
18
|
+
.replace(/"/g, '"')
|
|
19
|
+
.replace(/'/g, ''');
|
|
20
|
+
}
|
|
21
|
+
export function buildDashboardStats(digest, state) {
|
|
22
|
+
const summary = digest.summary || {
|
|
23
|
+
totalActivePRs: 0,
|
|
24
|
+
totalMergedAllTime: 0,
|
|
25
|
+
mergeRate: 0,
|
|
26
|
+
totalNeedingAttention: 0,
|
|
27
|
+
};
|
|
28
|
+
return {
|
|
29
|
+
activePRs: summary.totalActivePRs,
|
|
30
|
+
shelvedPRs: (digest.shelvedPRs || []).length,
|
|
31
|
+
mergedPRs: summary.totalMergedAllTime,
|
|
32
|
+
closedPRs: Object.values(state.repoScores || {}).reduce((sum, s) => sum + (s.closedWithoutMergeCount || 0), 0),
|
|
33
|
+
mergeRate: `${(summary.mergeRate ?? 0).toFixed(1)}%`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function generateDashboardHtml(stats, monthlyMerged, monthlyClosed, monthlyOpened, digest, state, issueResponses = []) {
|
|
37
|
+
const approachingDormantDays = state.config?.approachingDormantDays ?? 25;
|
|
38
|
+
const shelvedPRs = digest.shelvedPRs || [];
|
|
39
|
+
const autoUnshelvedPRs = digest.autoUnshelvedPRs || [];
|
|
40
|
+
const recentlyMerged = digest.recentlyMergedPRs || [];
|
|
41
|
+
const shelvedUrls = new Set(shelvedPRs.map((pr) => pr.url));
|
|
42
|
+
const activePRList = (digest.openPRs || []).filter((pr) => !shelvedUrls.has(pr.url));
|
|
43
|
+
// Action Required: contributor must do something
|
|
44
|
+
const actionRequired = [
|
|
45
|
+
...(digest.prsNeedingResponse || []),
|
|
46
|
+
...(digest.needsChangesPRs || []),
|
|
47
|
+
...(digest.ciFailingPRs || []),
|
|
48
|
+
...(digest.mergeConflictPRs || []),
|
|
49
|
+
...(digest.incompleteChecklistPRs || []),
|
|
50
|
+
...(digest.missingRequiredFilesPRs || []),
|
|
51
|
+
...(digest.needsRebasePRs || []),
|
|
52
|
+
];
|
|
53
|
+
// Waiting on Others: informational, no contributor action needed
|
|
54
|
+
const waitingOnOthers = [
|
|
55
|
+
...(digest.changesAddressedPRs || []),
|
|
56
|
+
...(digest.waitingOnMaintainerPRs || []),
|
|
57
|
+
...(digest.ciBlockedPRs || []),
|
|
58
|
+
...(digest.ciNotRunningPRs || []),
|
|
59
|
+
];
|
|
60
|
+
function truncateTitle(title, max = 50) {
|
|
61
|
+
const truncated = title.length <= max ? title : title.slice(0, max) + '...';
|
|
62
|
+
return escapeHtml(truncated);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Render health status items. labelFn output is automatically HTML-escaped.
|
|
66
|
+
* metaFn output is injected raw — callers must ensure metaFn returns safe HTML
|
|
67
|
+
* (use escapeHtml for any user-controlled content within metaFn).
|
|
68
|
+
*
|
|
69
|
+
* Accepts both full FetchedPR objects and lightweight ShelvedPRRef objects.
|
|
70
|
+
*/
|
|
71
|
+
function renderHealthItems(prs, cssClass, svgPaths, labelFn, metaFn) {
|
|
72
|
+
return prs
|
|
73
|
+
.map((pr) => {
|
|
74
|
+
const rawLabel = typeof labelFn === 'string' ? labelFn : labelFn(pr);
|
|
75
|
+
const label = escapeHtml(rawLabel);
|
|
76
|
+
return `
|
|
77
|
+
<div class="health-item ${cssClass}" data-status="${cssClass}" data-repo="${escapeHtml(pr.repo)}" data-title="${escapeHtml(pr.title.toLowerCase())}">
|
|
78
|
+
<div class="health-icon">
|
|
79
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
80
|
+
${svgPaths}
|
|
81
|
+
</svg>
|
|
82
|
+
</div>
|
|
83
|
+
<div class="health-content">
|
|
84
|
+
<div class="health-title"><a href="${escapeHtml(pr.url)}" target="_blank">${escapeHtml(pr.repo)}#${pr.number}</a> - ${label}</div>
|
|
85
|
+
<div class="health-meta">${metaFn(pr)}</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>`;
|
|
88
|
+
})
|
|
89
|
+
.join('');
|
|
90
|
+
}
|
|
91
|
+
// SVG path constants for health item icons
|
|
92
|
+
const SVG = {
|
|
93
|
+
comment: '<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>',
|
|
94
|
+
edit: '<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>',
|
|
95
|
+
xCircle: '<circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/>',
|
|
96
|
+
conflict: '<path d="M8 3v3a2 2 0 0 1-2 2H3"/><path d="M21 8h-3a2 2 0 0 1-2-2V3"/><path d="M3 16h3a2 2 0 0 1 2 2v3"/><path d="M16 21v-3a2 2 0 0 1 2-2h3"/>',
|
|
97
|
+
checklist: '<path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>',
|
|
98
|
+
file: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="12" y1="18" x2="12" y2="12"/><line x1="9" y1="15" x2="15" y2="15"/>',
|
|
99
|
+
checkCircle: '<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>',
|
|
100
|
+
clock: '<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>',
|
|
101
|
+
lock: '<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
|
|
102
|
+
infoCircle: '<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>',
|
|
103
|
+
refresh: '<polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>',
|
|
104
|
+
box: '<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/>',
|
|
105
|
+
bell: '<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/>',
|
|
106
|
+
gitMerge: '<circle cx="7" cy="18" r="3"/><circle cx="7" cy="6" r="3"/><circle cx="17" cy="12" r="3"/><line x1="7" y1="9" x2="7" y2="15"/><path d="M7 9c0 4 10 3 10 3"/>',
|
|
107
|
+
};
|
|
108
|
+
// Default meta: truncated PR title (works for both FetchedPR and ShelvedPRRef)
|
|
109
|
+
const titleMeta = (pr) => truncateTitle(pr.title);
|
|
110
|
+
return `<!DOCTYPE html>
|
|
111
|
+
<html lang="en">
|
|
112
|
+
<head>
|
|
113
|
+
<meta charset="UTF-8">
|
|
114
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
115
|
+
<title>OSS Autopilot - Mission Control</title>
|
|
116
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
117
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
118
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
119
|
+
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
120
|
+
<style>
|
|
121
|
+
:root, [data-theme="dark"] {
|
|
122
|
+
--bg-base: #080b10;
|
|
123
|
+
--bg-surface: rgba(22, 27, 34, 0.65);
|
|
124
|
+
--bg-elevated: rgba(28, 33, 40, 0.8);
|
|
125
|
+
--border: rgba(48, 54, 61, 0.6);
|
|
126
|
+
--border-muted: rgba(33, 38, 45, 0.5);
|
|
127
|
+
--text-primary: #e6edf3;
|
|
128
|
+
--text-secondary: #8b949e;
|
|
129
|
+
--text-muted: #6e7681;
|
|
130
|
+
--accent-merged: #a855f7;
|
|
131
|
+
--accent-merged-dim: rgba(168, 85, 247, 0.12);
|
|
132
|
+
--accent-open: #3fb950;
|
|
133
|
+
--accent-open-dim: rgba(63, 185, 80, 0.12);
|
|
134
|
+
--accent-warning: #d29922;
|
|
135
|
+
--accent-warning-dim: rgba(210, 153, 34, 0.12);
|
|
136
|
+
--accent-error: #f85149;
|
|
137
|
+
--accent-error-dim: rgba(248, 81, 73, 0.10);
|
|
138
|
+
--accent-conflict: #da3633;
|
|
139
|
+
--accent-info: #58a6ff;
|
|
140
|
+
--accent-info-dim: rgba(88, 166, 255, 0.08);
|
|
141
|
+
--chart-border: rgba(8, 11, 16, 0.8);
|
|
142
|
+
--chart-grid: rgba(48, 54, 61, 0.3);
|
|
143
|
+
--scrollbar-track: rgba(28, 33, 40, 0.8);
|
|
144
|
+
--scrollbar-thumb: rgba(48, 54, 61, 0.6);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
[data-theme="light"] {
|
|
148
|
+
--bg-base: #f6f8fa;
|
|
149
|
+
--bg-surface: rgba(255, 255, 255, 0.85);
|
|
150
|
+
--bg-elevated: rgba(246, 248, 250, 0.95);
|
|
151
|
+
--border: rgba(208, 215, 222, 0.6);
|
|
152
|
+
--border-muted: rgba(216, 222, 228, 0.5);
|
|
153
|
+
--text-primary: #1f2328;
|
|
154
|
+
--text-secondary: #656d76;
|
|
155
|
+
--text-muted: #8b949e;
|
|
156
|
+
--accent-merged: #8250df;
|
|
157
|
+
--accent-merged-dim: rgba(130, 80, 223, 0.1);
|
|
158
|
+
--accent-open: #1a7f37;
|
|
159
|
+
--accent-open-dim: rgba(26, 127, 55, 0.1);
|
|
160
|
+
--accent-warning: #9a6700;
|
|
161
|
+
--accent-warning-dim: rgba(154, 103, 0, 0.1);
|
|
162
|
+
--accent-error: #cf222e;
|
|
163
|
+
--accent-error-dim: rgba(207, 34, 46, 0.08);
|
|
164
|
+
--accent-conflict: #cf222e;
|
|
165
|
+
--accent-info: #0969da;
|
|
166
|
+
--accent-info-dim: rgba(9, 105, 218, 0.08);
|
|
167
|
+
--chart-border: rgba(255, 255, 255, 0.8);
|
|
168
|
+
--chart-grid: rgba(208, 215, 222, 0.4);
|
|
169
|
+
--scrollbar-track: rgba(246, 248, 250, 0.95);
|
|
170
|
+
--scrollbar-thumb: rgba(208, 215, 222, 0.6);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
@media (prefers-color-scheme: light) {
|
|
174
|
+
:root:not([data-theme="dark"]) {
|
|
175
|
+
--bg-base: #f6f8fa;
|
|
176
|
+
--bg-surface: rgba(255, 255, 255, 0.85);
|
|
177
|
+
--bg-elevated: rgba(246, 248, 250, 0.95);
|
|
178
|
+
--border: rgba(208, 215, 222, 0.6);
|
|
179
|
+
--border-muted: rgba(216, 222, 228, 0.5);
|
|
180
|
+
--text-primary: #1f2328;
|
|
181
|
+
--text-secondary: #656d76;
|
|
182
|
+
--text-muted: #8b949e;
|
|
183
|
+
--accent-merged: #8250df;
|
|
184
|
+
--accent-merged-dim: rgba(130, 80, 223, 0.1);
|
|
185
|
+
--accent-open: #1a7f37;
|
|
186
|
+
--accent-open-dim: rgba(26, 127, 55, 0.1);
|
|
187
|
+
--accent-warning: #9a6700;
|
|
188
|
+
--accent-warning-dim: rgba(154, 103, 0, 0.1);
|
|
189
|
+
--accent-error: #cf222e;
|
|
190
|
+
--accent-error-dim: rgba(207, 34, 46, 0.08);
|
|
191
|
+
--accent-conflict: #cf222e;
|
|
192
|
+
--accent-info: #0969da;
|
|
193
|
+
--accent-info-dim: rgba(9, 105, 218, 0.08);
|
|
194
|
+
--chart-border: rgba(255, 255, 255, 0.8);
|
|
195
|
+
--chart-grid: rgba(208, 215, 222, 0.4);
|
|
196
|
+
--scrollbar-track: rgba(246, 248, 250, 0.95);
|
|
197
|
+
--scrollbar-thumb: rgba(208, 215, 222, 0.6);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
202
|
+
|
|
203
|
+
body {
|
|
204
|
+
font-family: 'Geist', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
205
|
+
background: var(--bg-base);
|
|
206
|
+
color: var(--text-primary);
|
|
207
|
+
min-height: 100vh;
|
|
208
|
+
line-height: 1.5;
|
|
209
|
+
overflow-x: hidden;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
body::before {
|
|
213
|
+
content: '';
|
|
214
|
+
position: fixed;
|
|
215
|
+
top: -20%; left: -10%;
|
|
216
|
+
width: 60%; height: 60%;
|
|
217
|
+
background: radial-gradient(ellipse, rgba(88, 166, 255, 0.06) 0%, transparent 70%);
|
|
218
|
+
pointer-events: none;
|
|
219
|
+
z-index: 0;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
body::after {
|
|
223
|
+
content: '';
|
|
224
|
+
position: fixed;
|
|
225
|
+
bottom: -20%; right: -10%;
|
|
226
|
+
width: 50%; height: 50%;
|
|
227
|
+
background: radial-gradient(ellipse, rgba(168, 85, 247, 0.05) 0%, transparent 70%);
|
|
228
|
+
pointer-events: none;
|
|
229
|
+
z-index: 0;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
[data-theme="light"] body::before,
|
|
233
|
+
[data-theme="light"] body::after {
|
|
234
|
+
display: none;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.container {
|
|
238
|
+
max-width: 1400px;
|
|
239
|
+
margin: 0 auto;
|
|
240
|
+
padding: 2rem;
|
|
241
|
+
position: relative;
|
|
242
|
+
z-index: 1;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.header {
|
|
246
|
+
display: flex;
|
|
247
|
+
align-items: center;
|
|
248
|
+
justify-content: space-between;
|
|
249
|
+
margin-bottom: 1.5rem;
|
|
250
|
+
padding-bottom: 1rem;
|
|
251
|
+
border-bottom: 1px solid var(--border-muted);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.header-left {
|
|
255
|
+
display: flex;
|
|
256
|
+
align-items: center;
|
|
257
|
+
gap: 1rem;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.logo {
|
|
261
|
+
width: 44px;
|
|
262
|
+
height: 44px;
|
|
263
|
+
background: linear-gradient(135deg, var(--accent-info) 0%, var(--accent-merged) 50%, #f778ba 100%);
|
|
264
|
+
border-radius: 12px;
|
|
265
|
+
display: flex;
|
|
266
|
+
align-items: center;
|
|
267
|
+
justify-content: center;
|
|
268
|
+
font-size: 1.5rem;
|
|
269
|
+
box-shadow: 0 0 24px rgba(168, 85, 247, 0.3), 0 0 48px rgba(88, 166, 255, 0.15);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.header h1 {
|
|
273
|
+
font-size: 1.75rem;
|
|
274
|
+
font-weight: 600;
|
|
275
|
+
letter-spacing: -0.02em;
|
|
276
|
+
background: linear-gradient(135deg, var(--text-primary) 0%, var(--text-secondary) 100%);
|
|
277
|
+
-webkit-background-clip: text;
|
|
278
|
+
-webkit-text-fill-color: transparent;
|
|
279
|
+
background-clip: text;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.header-subtitle {
|
|
283
|
+
font-family: 'Geist Mono', monospace;
|
|
284
|
+
font-size: 0.75rem;
|
|
285
|
+
color: var(--text-muted);
|
|
286
|
+
text-transform: uppercase;
|
|
287
|
+
letter-spacing: 0.1em;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.timestamp {
|
|
291
|
+
font-family: 'Geist Mono', monospace;
|
|
292
|
+
font-size: 0.8rem;
|
|
293
|
+
color: var(--text-muted);
|
|
294
|
+
display: flex;
|
|
295
|
+
align-items: center;
|
|
296
|
+
gap: 0.5rem;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.timestamp::before {
|
|
300
|
+
content: '';
|
|
301
|
+
width: 8px;
|
|
302
|
+
height: 8px;
|
|
303
|
+
background: var(--accent-open);
|
|
304
|
+
border-radius: 50%;
|
|
305
|
+
animation: pulse 2s ease-in-out infinite;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
@keyframes pulse {
|
|
309
|
+
0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(35, 134, 54, 0.4); }
|
|
310
|
+
50% { opacity: 0.8; box-shadow: 0 0 0 8px rgba(35, 134, 54, 0); }
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.stats-grid {
|
|
314
|
+
display: flex;
|
|
315
|
+
background: var(--bg-surface);
|
|
316
|
+
border: 1px solid var(--border-muted);
|
|
317
|
+
border-radius: 12px;
|
|
318
|
+
margin-bottom: 1.5rem;
|
|
319
|
+
overflow: hidden;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
@media (max-width: 768px) {
|
|
323
|
+
.stats-grid { flex-wrap: wrap; }
|
|
324
|
+
.stat-card { flex: 1 1 33%; }
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.stat-card {
|
|
328
|
+
flex: 1;
|
|
329
|
+
padding: 1rem 1.25rem;
|
|
330
|
+
position: relative;
|
|
331
|
+
transition: background 0.2s ease;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.stat-card + .stat-card {
|
|
335
|
+
border-left: 1px solid var(--border-muted);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.stat-card:hover {
|
|
339
|
+
background: rgba(255, 255, 255, 0.02);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.stat-card::after {
|
|
343
|
+
content: '';
|
|
344
|
+
position: absolute;
|
|
345
|
+
bottom: 0; left: 0.75rem; right: 0.75rem;
|
|
346
|
+
height: 2px;
|
|
347
|
+
background: var(--accent-color, var(--border));
|
|
348
|
+
border-radius: 2px;
|
|
349
|
+
opacity: 0.7;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.stat-card.active { --accent-color: var(--accent-open); }
|
|
353
|
+
.stat-card.merged { --accent-color: var(--accent-merged); }
|
|
354
|
+
.stat-card.closed { --accent-color: var(--text-muted); }
|
|
355
|
+
.stat-card.rate { --accent-color: var(--accent-info); }
|
|
356
|
+
|
|
357
|
+
.stat-value {
|
|
358
|
+
font-family: 'Geist Mono', monospace;
|
|
359
|
+
font-size: 1.75rem;
|
|
360
|
+
font-weight: 600;
|
|
361
|
+
line-height: 1;
|
|
362
|
+
margin-bottom: 0.25rem;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.stat-card.active .stat-value { color: var(--accent-open); }
|
|
366
|
+
.stat-card.merged .stat-value { color: var(--accent-merged); }
|
|
367
|
+
.stat-card.closed .stat-value { color: var(--text-muted); }
|
|
368
|
+
.stat-card.rate .stat-value { color: var(--accent-info); }
|
|
369
|
+
|
|
370
|
+
.stat-label {
|
|
371
|
+
font-size: 0.7rem;
|
|
372
|
+
color: var(--text-secondary);
|
|
373
|
+
text-transform: uppercase;
|
|
374
|
+
letter-spacing: 0.05em;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.health-section {
|
|
378
|
+
background: var(--bg-surface);
|
|
379
|
+
border: 1px solid var(--border-muted);
|
|
380
|
+
border-radius: 10px;
|
|
381
|
+
padding: 1.25rem;
|
|
382
|
+
margin-bottom: 1.25rem;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.health-header {
|
|
386
|
+
display: flex;
|
|
387
|
+
align-items: center;
|
|
388
|
+
gap: 0.75rem;
|
|
389
|
+
margin-bottom: 1rem;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.health-header h2 {
|
|
393
|
+
font-size: 1rem;
|
|
394
|
+
font-weight: 600;
|
|
395
|
+
color: var(--text-primary);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.health-badge {
|
|
399
|
+
font-family: 'Geist Mono', monospace;
|
|
400
|
+
font-size: 0.7rem;
|
|
401
|
+
padding: 0.25rem 0.5rem;
|
|
402
|
+
border-radius: 4px;
|
|
403
|
+
background: var(--accent-error-dim);
|
|
404
|
+
color: var(--accent-error);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.health-items {
|
|
408
|
+
display: grid;
|
|
409
|
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
410
|
+
gap: 0.75rem;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.health-item {
|
|
414
|
+
display: flex;
|
|
415
|
+
align-items: center;
|
|
416
|
+
gap: 0.75rem;
|
|
417
|
+
padding: 0.75rem 1rem;
|
|
418
|
+
background: var(--bg-elevated);
|
|
419
|
+
border-radius: 8px;
|
|
420
|
+
border-left: 3px solid;
|
|
421
|
+
transition: transform 0.15s ease;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.health-item:hover { transform: translateX(4px); }
|
|
425
|
+
|
|
426
|
+
.health-item.ci-failing {
|
|
427
|
+
border-left-color: var(--accent-error);
|
|
428
|
+
background: var(--accent-error-dim);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.health-item.conflict {
|
|
432
|
+
border-left-color: var(--accent-conflict);
|
|
433
|
+
background: rgba(218, 54, 51, 0.1);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.health-item.incomplete-checklist {
|
|
437
|
+
border-left-color: var(--accent-info);
|
|
438
|
+
}
|
|
439
|
+
.health-item.needs-response,
|
|
440
|
+
.health-item.needs-changes {
|
|
441
|
+
border-left-color: var(--accent-warning);
|
|
442
|
+
background: var(--accent-warning-dim);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.health-item.changes-addressed,
|
|
446
|
+
.health-item.waiting-maintainer {
|
|
447
|
+
border-left-color: var(--accent-info);
|
|
448
|
+
background: var(--accent-info-dim);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.health-item.ci-not-running {
|
|
452
|
+
border-left-color: var(--text-muted);
|
|
453
|
+
background: rgba(110, 118, 129, 0.1);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
.health-item.missing-files {
|
|
457
|
+
border-left-color: var(--accent-warning);
|
|
458
|
+
background: var(--accent-warning-dim);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.health-item.ci-blocked {
|
|
462
|
+
border-left-color: var(--text-muted);
|
|
463
|
+
background: rgba(110, 118, 129, 0.1);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.health-item.needs-rebase {
|
|
467
|
+
border-left-color: var(--accent-warning);
|
|
468
|
+
background: var(--accent-warning-dim);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.health-item.shelved {
|
|
472
|
+
border-left-color: var(--text-muted);
|
|
473
|
+
background: rgba(110, 118, 129, 0.06);
|
|
474
|
+
opacity: 0.6;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.health-item.shelved .health-icon { background: rgba(110, 118, 129, 0.12); color: var(--text-muted); }
|
|
478
|
+
|
|
479
|
+
.health-item.auto-unshelved {
|
|
480
|
+
border-left-color: var(--accent-info);
|
|
481
|
+
background: var(--accent-info-dim);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
.health-item.auto-unshelved .health-icon { background: var(--accent-info-dim); color: var(--accent-info); }
|
|
485
|
+
|
|
486
|
+
.stat-card.shelved { --accent-color: var(--text-muted); }
|
|
487
|
+
.stat-card.shelved .stat-value { color: var(--text-muted); }
|
|
488
|
+
|
|
489
|
+
.waiting-section {
|
|
490
|
+
border-color: rgba(88, 166, 255, 0.2);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.health-icon {
|
|
494
|
+
width: 32px;
|
|
495
|
+
height: 32px;
|
|
496
|
+
border-radius: 8px;
|
|
497
|
+
display: flex;
|
|
498
|
+
align-items: center;
|
|
499
|
+
justify-content: center;
|
|
500
|
+
font-size: 1rem;
|
|
501
|
+
flex-shrink: 0;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.health-item.ci-failing .health-icon { background: var(--accent-error-dim); color: var(--accent-error); }
|
|
505
|
+
.health-item.conflict .health-icon { background: rgba(218, 54, 51, 0.15); color: var(--accent-conflict); }
|
|
506
|
+
.health-item.incomplete-checklist .health-icon { background: var(--accent-info-dim); color: var(--accent-info); }
|
|
507
|
+
.health-item.needs-response .health-icon,
|
|
508
|
+
.health-item.needs-changes .health-icon { background: var(--accent-warning-dim); color: var(--accent-warning); }
|
|
509
|
+
.health-item.changes-addressed .health-icon,
|
|
510
|
+
.health-item.waiting-maintainer .health-icon { background: var(--accent-info-dim); color: var(--accent-info); }
|
|
511
|
+
.health-item.ci-not-running .health-icon { background: rgba(110, 118, 129, 0.15); color: var(--text-muted); }
|
|
512
|
+
.health-item.missing-files .health-icon { background: var(--accent-warning-dim); color: var(--accent-warning); }
|
|
513
|
+
.health-item.ci-blocked .health-icon { background: rgba(110, 118, 129, 0.15); color: var(--text-muted); }
|
|
514
|
+
.health-item.needs-rebase .health-icon { background: var(--accent-warning-dim); color: var(--accent-warning); }
|
|
515
|
+
|
|
516
|
+
.health-content { flex: 1; min-width: 0; }
|
|
517
|
+
|
|
518
|
+
.health-title {
|
|
519
|
+
font-size: 0.85rem;
|
|
520
|
+
font-weight: 500;
|
|
521
|
+
color: var(--text-primary);
|
|
522
|
+
white-space: nowrap;
|
|
523
|
+
overflow: hidden;
|
|
524
|
+
text-overflow: ellipsis;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
.health-title a { color: inherit; text-decoration: none; }
|
|
528
|
+
.health-title a:hover { color: var(--accent-info); }
|
|
529
|
+
|
|
530
|
+
.health-meta {
|
|
531
|
+
font-family: 'Geist Mono', monospace;
|
|
532
|
+
font-size: 0.7rem;
|
|
533
|
+
color: var(--text-muted);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
.health-empty {
|
|
537
|
+
display: flex;
|
|
538
|
+
align-items: center;
|
|
539
|
+
justify-content: center;
|
|
540
|
+
padding: 2rem;
|
|
541
|
+
color: var(--text-muted);
|
|
542
|
+
font-size: 0.9rem;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
.health-empty::before {
|
|
546
|
+
content: '\\2713';
|
|
547
|
+
display: inline-flex;
|
|
548
|
+
align-items: center;
|
|
549
|
+
justify-content: center;
|
|
550
|
+
width: 24px;
|
|
551
|
+
height: 24px;
|
|
552
|
+
background: var(--accent-open-dim);
|
|
553
|
+
color: var(--accent-open);
|
|
554
|
+
border-radius: 50%;
|
|
555
|
+
margin-right: 0.75rem;
|
|
556
|
+
font-weight: bold;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
.main-grid {
|
|
560
|
+
display: grid;
|
|
561
|
+
grid-template-columns: 1fr 1fr;
|
|
562
|
+
gap: 1.25rem;
|
|
563
|
+
margin-bottom: 1.25rem;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
@media (max-width: 1024px) { .main-grid { grid-template-columns: 1fr; } }
|
|
567
|
+
|
|
568
|
+
.card {
|
|
569
|
+
background: var(--bg-surface);
|
|
570
|
+
border: 1px solid var(--border-muted);
|
|
571
|
+
border-radius: 10px;
|
|
572
|
+
overflow: hidden;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
.card-header {
|
|
576
|
+
display: flex;
|
|
577
|
+
align-items: center;
|
|
578
|
+
justify-content: space-between;
|
|
579
|
+
padding: 0.75rem 1.125rem;
|
|
580
|
+
border-bottom: 1px solid var(--border-muted);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
.card-title {
|
|
584
|
+
font-size: 0.75rem;
|
|
585
|
+
font-weight: 600;
|
|
586
|
+
color: var(--text-secondary);
|
|
587
|
+
text-transform: uppercase;
|
|
588
|
+
letter-spacing: 0.04em;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
.card-body { padding: 1rem 1.125rem; }
|
|
592
|
+
|
|
593
|
+
.chart-container {
|
|
594
|
+
position: relative;
|
|
595
|
+
height: 260px;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.pr-list-section {
|
|
599
|
+
background: var(--bg-surface);
|
|
600
|
+
border: 1px solid var(--border-muted);
|
|
601
|
+
border-radius: 10px;
|
|
602
|
+
overflow: hidden;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.pr-list-header {
|
|
606
|
+
display: flex;
|
|
607
|
+
align-items: center;
|
|
608
|
+
justify-content: space-between;
|
|
609
|
+
padding: 0.75rem 1.125rem;
|
|
610
|
+
border-bottom: 1px solid var(--border-muted);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
.pr-list-title {
|
|
614
|
+
font-size: 0.75rem;
|
|
615
|
+
font-weight: 600;
|
|
616
|
+
color: var(--text-secondary);
|
|
617
|
+
text-transform: uppercase;
|
|
618
|
+
letter-spacing: 0.04em;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
.pr-count {
|
|
622
|
+
font-family: 'Geist Mono', monospace;
|
|
623
|
+
font-size: 0.75rem;
|
|
624
|
+
padding: 0.25rem 0.5rem;
|
|
625
|
+
background: var(--accent-open-dim);
|
|
626
|
+
color: var(--accent-open);
|
|
627
|
+
border-radius: 4px;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
.pr-list {
|
|
631
|
+
max-height: 600px;
|
|
632
|
+
overflow-y: auto;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
.pr-list::-webkit-scrollbar { width: 6px; }
|
|
636
|
+
.pr-list::-webkit-scrollbar-track { background: var(--scrollbar-track, var(--bg-elevated)); }
|
|
637
|
+
.pr-list::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb, var(--border)); border-radius: 3px; }
|
|
638
|
+
|
|
639
|
+
.pr-item {
|
|
640
|
+
display: flex;
|
|
641
|
+
align-items: flex-start;
|
|
642
|
+
gap: 1rem;
|
|
643
|
+
padding: 1rem 1.25rem;
|
|
644
|
+
border-bottom: 1px solid var(--border-muted);
|
|
645
|
+
transition: background 0.15s ease;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
.pr-item:last-child { border-bottom: none; }
|
|
649
|
+
.pr-item:hover { background: var(--bg-elevated); }
|
|
650
|
+
|
|
651
|
+
.pr-status-indicator {
|
|
652
|
+
width: 40px;
|
|
653
|
+
height: 40px;
|
|
654
|
+
border-radius: 10px;
|
|
655
|
+
display: flex;
|
|
656
|
+
align-items: center;
|
|
657
|
+
justify-content: center;
|
|
658
|
+
flex-shrink: 0;
|
|
659
|
+
font-size: 1.1rem;
|
|
660
|
+
background: var(--accent-open-dim);
|
|
661
|
+
color: var(--accent-open);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
.pr-item.has-issues .pr-status-indicator {
|
|
665
|
+
background: var(--accent-error-dim);
|
|
666
|
+
color: var(--accent-error);
|
|
667
|
+
animation: attention-pulse 2s ease-in-out infinite;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
.pr-item.stale .pr-status-indicator {
|
|
671
|
+
background: var(--accent-warning-dim);
|
|
672
|
+
color: var(--accent-warning);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
@keyframes attention-pulse {
|
|
676
|
+
0%, 100% { box-shadow: 0 0 0 0 rgba(248, 81, 73, 0.4); }
|
|
677
|
+
50% { box-shadow: 0 0 0 6px rgba(248, 81, 73, 0); }
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
.pr-content { flex: 1; min-width: 0; }
|
|
681
|
+
|
|
682
|
+
.pr-title-row {
|
|
683
|
+
display: flex;
|
|
684
|
+
align-items: center;
|
|
685
|
+
gap: 0.5rem;
|
|
686
|
+
margin-bottom: 0.25rem;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
.pr-title {
|
|
690
|
+
font-size: 0.9rem;
|
|
691
|
+
font-weight: 500;
|
|
692
|
+
color: var(--text-primary);
|
|
693
|
+
text-decoration: none;
|
|
694
|
+
white-space: nowrap;
|
|
695
|
+
overflow: hidden;
|
|
696
|
+
text-overflow: ellipsis;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
.pr-title:hover { color: var(--accent-info); }
|
|
700
|
+
|
|
701
|
+
.pr-repo {
|
|
702
|
+
font-family: 'Geist Mono', monospace;
|
|
703
|
+
font-size: 0.75rem;
|
|
704
|
+
color: var(--text-muted);
|
|
705
|
+
flex-shrink: 0;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
.pr-badges {
|
|
709
|
+
display: flex;
|
|
710
|
+
flex-wrap: wrap;
|
|
711
|
+
gap: 0.5rem;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
.badge {
|
|
715
|
+
font-family: 'Geist Mono', monospace;
|
|
716
|
+
font-size: 0.65rem;
|
|
717
|
+
font-weight: 500;
|
|
718
|
+
padding: 0.2rem 0.5rem;
|
|
719
|
+
border-radius: 4px;
|
|
720
|
+
text-transform: uppercase;
|
|
721
|
+
letter-spacing: 0.03em;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
.badge-ci-failing { background: var(--accent-error-dim); color: var(--accent-error); }
|
|
725
|
+
.badge-conflict { background: rgba(218, 54, 51, 0.15); color: var(--accent-conflict); }
|
|
726
|
+
.badge-needs-response { background: var(--accent-warning-dim); color: var(--accent-warning); }
|
|
727
|
+
.badge-stale { background: var(--accent-warning-dim); color: var(--accent-warning); }
|
|
728
|
+
.badge-passing { background: var(--accent-open-dim); color: var(--accent-open); }
|
|
729
|
+
.badge-pending { background: var(--accent-info-dim); color: var(--accent-info); }
|
|
730
|
+
.badge-days { background: var(--bg-elevated); color: var(--text-muted); }
|
|
731
|
+
.badge-changes-requested { background: var(--accent-warning-dim); color: var(--accent-warning); }
|
|
732
|
+
.badge-changes-addressed { background: var(--accent-info-dim); color: var(--accent-info); }
|
|
733
|
+
|
|
734
|
+
.pr-activity {
|
|
735
|
+
font-family: 'Geist Mono', monospace;
|
|
736
|
+
font-size: 0.7rem;
|
|
737
|
+
color: var(--text-muted);
|
|
738
|
+
margin-left: auto;
|
|
739
|
+
text-align: right;
|
|
740
|
+
flex-shrink: 0;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
.empty-state {
|
|
744
|
+
display: flex;
|
|
745
|
+
flex-direction: column;
|
|
746
|
+
align-items: center;
|
|
747
|
+
justify-content: center;
|
|
748
|
+
padding: 3rem;
|
|
749
|
+
color: var(--text-muted);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
.empty-state-icon {
|
|
753
|
+
font-size: 2.5rem;
|
|
754
|
+
margin-bottom: 1rem;
|
|
755
|
+
opacity: 0.5;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
.footer {
|
|
759
|
+
text-align: center;
|
|
760
|
+
padding-top: 1.5rem;
|
|
761
|
+
border-top: 1px solid var(--border-muted);
|
|
762
|
+
margin-top: 1.5rem;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
.footer p {
|
|
766
|
+
font-family: 'Geist Mono', monospace;
|
|
767
|
+
font-size: 0.7rem;
|
|
768
|
+
color: var(--text-muted);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
@keyframes fadeInUp {
|
|
772
|
+
from { opacity: 0; transform: translateY(12px); }
|
|
773
|
+
to { opacity: 1; transform: translateY(0); }
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
.stats-grid, .health-section, .pr-list-section {
|
|
777
|
+
animation: fadeInUp 0.35s ease;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
.theme-toggle {
|
|
781
|
+
background: var(--bg-elevated);
|
|
782
|
+
border: 1px solid var(--border-muted);
|
|
783
|
+
border-radius: 8px;
|
|
784
|
+
padding: 0.4rem 0.6rem;
|
|
785
|
+
cursor: pointer;
|
|
786
|
+
color: var(--text-secondary);
|
|
787
|
+
display: flex;
|
|
788
|
+
align-items: center;
|
|
789
|
+
gap: 0.4rem;
|
|
790
|
+
font-family: 'Geist Mono', monospace;
|
|
791
|
+
font-size: 0.7rem;
|
|
792
|
+
transition: background 0.2s ease, color 0.2s ease;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
.theme-toggle:hover {
|
|
796
|
+
background: var(--bg-surface);
|
|
797
|
+
color: var(--text-primary);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
.theme-toggle svg { flex-shrink: 0; }
|
|
801
|
+
|
|
802
|
+
.header-controls {
|
|
803
|
+
display: flex;
|
|
804
|
+
align-items: center;
|
|
805
|
+
gap: 0.75rem;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
.filter-toolbar {
|
|
809
|
+
display: flex;
|
|
810
|
+
align-items: center;
|
|
811
|
+
gap: 0.75rem;
|
|
812
|
+
padding: 0.75rem 1rem;
|
|
813
|
+
background: var(--bg-surface);
|
|
814
|
+
border: 1px solid var(--border-muted);
|
|
815
|
+
border-radius: 10px;
|
|
816
|
+
margin-bottom: 1.25rem;
|
|
817
|
+
flex-wrap: wrap;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
.filter-toolbar label {
|
|
821
|
+
font-family: 'Geist Mono', monospace;
|
|
822
|
+
font-size: 0.7rem;
|
|
823
|
+
color: var(--text-muted);
|
|
824
|
+
text-transform: uppercase;
|
|
825
|
+
letter-spacing: 0.04em;
|
|
826
|
+
flex-shrink: 0;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
.filter-search {
|
|
830
|
+
flex: 1;
|
|
831
|
+
min-width: 180px;
|
|
832
|
+
padding: 0.4rem 0.75rem;
|
|
833
|
+
background: var(--bg-elevated);
|
|
834
|
+
border: 1px solid var(--border-muted);
|
|
835
|
+
border-radius: 6px;
|
|
836
|
+
color: var(--text-primary);
|
|
837
|
+
font-family: 'Geist', sans-serif;
|
|
838
|
+
font-size: 0.8rem;
|
|
839
|
+
outline: none;
|
|
840
|
+
transition: border-color 0.2s ease;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
.filter-search:focus {
|
|
844
|
+
border-color: var(--accent-info);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
.filter-search::placeholder {
|
|
848
|
+
color: var(--text-muted);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
.filter-select {
|
|
852
|
+
padding: 0.4rem 0.75rem;
|
|
853
|
+
background: var(--bg-elevated);
|
|
854
|
+
border: 1px solid var(--border-muted);
|
|
855
|
+
border-radius: 6px;
|
|
856
|
+
color: var(--text-primary);
|
|
857
|
+
font-family: 'Geist', sans-serif;
|
|
858
|
+
font-size: 0.8rem;
|
|
859
|
+
outline: none;
|
|
860
|
+
cursor: pointer;
|
|
861
|
+
transition: border-color 0.2s ease;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
.filter-select:focus {
|
|
865
|
+
border-color: var(--accent-info);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
.filter-count {
|
|
869
|
+
font-family: 'Geist Mono', monospace;
|
|
870
|
+
font-size: 0.7rem;
|
|
871
|
+
color: var(--text-muted);
|
|
872
|
+
margin-left: auto;
|
|
873
|
+
flex-shrink: 0;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
.pr-item[data-hidden="true"],
|
|
877
|
+
.health-item[data-hidden="true"] {
|
|
878
|
+
display: none;
|
|
879
|
+
}
|
|
880
|
+
</style>
|
|
881
|
+
</head>
|
|
882
|
+
<body>
|
|
883
|
+
<div class="container">
|
|
884
|
+
<header class="header">
|
|
885
|
+
<div class="header-left">
|
|
886
|
+
<div class="logo">
|
|
887
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
888
|
+
<circle cx="12" cy="12" r="10"/>
|
|
889
|
+
<path d="M12 6v6l4 2"/>
|
|
890
|
+
</svg>
|
|
891
|
+
</div>
|
|
892
|
+
<div>
|
|
893
|
+
<h1>OSS Autopilot</h1>
|
|
894
|
+
<span class="header-subtitle">Mission Control</span>
|
|
895
|
+
</div>
|
|
896
|
+
</div>
|
|
897
|
+
<div class="header-controls">
|
|
898
|
+
<div class="timestamp">
|
|
899
|
+
Last updated: ${digest.generatedAt
|
|
900
|
+
? new Date(digest.generatedAt).toLocaleString('en-US', {
|
|
901
|
+
weekday: 'short',
|
|
902
|
+
month: 'short',
|
|
903
|
+
day: 'numeric',
|
|
904
|
+
year: 'numeric',
|
|
905
|
+
hour: '2-digit',
|
|
906
|
+
minute: '2-digit',
|
|
907
|
+
second: '2-digit',
|
|
908
|
+
hour12: false,
|
|
909
|
+
})
|
|
910
|
+
: 'Unknown'}
|
|
911
|
+
</div>
|
|
912
|
+
<button class="theme-toggle" id="themeToggle" title="Toggle light/dark mode">
|
|
913
|
+
<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">
|
|
914
|
+
<circle cx="12" cy="12" r="5"/>
|
|
915
|
+
<line x1="12" y1="1" x2="12" y2="3"/>
|
|
916
|
+
<line x1="12" y1="21" x2="12" y2="23"/>
|
|
917
|
+
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
|
|
918
|
+
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
|
|
919
|
+
<line x1="1" y1="12" x2="3" y2="12"/>
|
|
920
|
+
<line x1="21" y1="12" x2="23" y2="12"/>
|
|
921
|
+
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
|
|
922
|
+
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
|
|
923
|
+
</svg>
|
|
924
|
+
<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;">
|
|
925
|
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
|
926
|
+
</svg>
|
|
927
|
+
<span id="themeLabel">Light</span>
|
|
928
|
+
</button>
|
|
929
|
+
</div>
|
|
930
|
+
</header>
|
|
931
|
+
|
|
932
|
+
<div class="stats-grid">
|
|
933
|
+
<div class="stat-card active">
|
|
934
|
+
<div class="stat-value">${stats.activePRs}</div>
|
|
935
|
+
<div class="stat-label">Active PRs</div>
|
|
936
|
+
</div>
|
|
937
|
+
<div class="stat-card shelved">
|
|
938
|
+
<div class="stat-value">${stats.shelvedPRs}</div>
|
|
939
|
+
<div class="stat-label">Shelved</div>
|
|
940
|
+
</div>
|
|
941
|
+
<div class="stat-card merged">
|
|
942
|
+
<div class="stat-value">${stats.mergedPRs}</div>
|
|
943
|
+
<div class="stat-label">Merged</div>
|
|
944
|
+
</div>
|
|
945
|
+
<div class="stat-card closed">
|
|
946
|
+
<div class="stat-value">${stats.closedPRs}</div>
|
|
947
|
+
<div class="stat-label">Closed</div>
|
|
948
|
+
</div>
|
|
949
|
+
<div class="stat-card rate">
|
|
950
|
+
<div class="stat-value">${stats.mergeRate}</div>
|
|
951
|
+
<div class="stat-label">Merge Rate</div>
|
|
952
|
+
</div>
|
|
953
|
+
</div>
|
|
954
|
+
|
|
955
|
+
<div class="filter-toolbar" id="filterToolbar">
|
|
956
|
+
<label>Filters</label>
|
|
957
|
+
<input type="text" class="filter-search" id="searchInput" placeholder="Search by PR title..." />
|
|
958
|
+
<select class="filter-select" id="statusFilter">
|
|
959
|
+
<option value="all">All Statuses</option>
|
|
960
|
+
<option value="needs-response">Needs Response</option>
|
|
961
|
+
<option value="needs-changes">Needs Changes</option>
|
|
962
|
+
<option value="ci-failing">CI Failing</option>
|
|
963
|
+
<option value="conflict">Merge Conflict</option>
|
|
964
|
+
<option value="changes-addressed">Changes Addressed</option>
|
|
965
|
+
<option value="waiting-maintainer">Waiting on Maintainer</option>
|
|
966
|
+
<option value="ci-blocked">CI Blocked</option>
|
|
967
|
+
<option value="ci-not-running">CI Not Running</option>
|
|
968
|
+
<option value="incomplete-checklist">Incomplete Checklist</option>
|
|
969
|
+
<option value="missing-files">Missing Files</option>
|
|
970
|
+
<option value="needs-rebase">Needs Rebase</option>
|
|
971
|
+
<option value="shelved">Shelved</option>
|
|
972
|
+
<option value="merged">Recently Merged</option>
|
|
973
|
+
<option value="closed">Recently Closed</option>
|
|
974
|
+
<option value="auto-unshelved">Auto-Unshelved</option>
|
|
975
|
+
<option value="active">Active (No Issues)</option>
|
|
976
|
+
</select>
|
|
977
|
+
<select class="filter-select" id="repoFilter">
|
|
978
|
+
<option value="all">All Repositories</option>
|
|
979
|
+
${(() => {
|
|
980
|
+
const repos = new Set();
|
|
981
|
+
for (const pr of activePRList)
|
|
982
|
+
repos.add(pr.repo);
|
|
983
|
+
for (const pr of shelvedPRs)
|
|
984
|
+
repos.add(pr.repo);
|
|
985
|
+
for (const pr of actionRequired)
|
|
986
|
+
repos.add(pr.repo);
|
|
987
|
+
for (const pr of waitingOnOthers)
|
|
988
|
+
repos.add(pr.repo);
|
|
989
|
+
for (const pr of recentlyMerged)
|
|
990
|
+
repos.add(pr.repo);
|
|
991
|
+
for (const pr of digest.recentlyClosedPRs || [])
|
|
992
|
+
repos.add(pr.repo);
|
|
993
|
+
for (const pr of autoUnshelvedPRs)
|
|
994
|
+
repos.add(pr.repo);
|
|
995
|
+
return Array.from(repos)
|
|
996
|
+
.sort()
|
|
997
|
+
.map((repo) => `<option value="${escapeHtml(repo)}">${escapeHtml(repo)}</option>`)
|
|
998
|
+
.join('\n ');
|
|
999
|
+
})()}
|
|
1000
|
+
</select>
|
|
1001
|
+
<span class="filter-count" id="filterCount"></span>
|
|
1002
|
+
</div>
|
|
1003
|
+
|
|
1004
|
+
${actionRequired.length > 0
|
|
1005
|
+
? `
|
|
1006
|
+
<section class="health-section">
|
|
1007
|
+
<div class="health-header">
|
|
1008
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--accent-warning)" stroke-width="2">
|
|
1009
|
+
<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"/>
|
|
1010
|
+
<line x1="12" y1="9" x2="12" y2="13"/>
|
|
1011
|
+
<line x1="12" y1="17" x2="12.01" y2="17"/>
|
|
1012
|
+
</svg>
|
|
1013
|
+
<h2>Action Required</h2>
|
|
1014
|
+
<span class="health-badge">${actionRequired.length} issue${actionRequired.length !== 1 ? 's' : ''}</span>
|
|
1015
|
+
</div>
|
|
1016
|
+
<div class="health-items">
|
|
1017
|
+
${renderHealthItems(digest.prsNeedingResponse || [], 'needs-response', SVG.comment, 'Needs Response', (pr) => pr.lastMaintainerComment
|
|
1018
|
+
? `@${escapeHtml(pr.lastMaintainerComment.author)}: ${truncateTitle(pr.lastMaintainerComment.body, 40)}`
|
|
1019
|
+
: truncateTitle(pr.title))}
|
|
1020
|
+
${renderHealthItems(digest.needsChangesPRs || [], 'needs-changes', SVG.edit, 'Needs Changes', titleMeta)}
|
|
1021
|
+
${renderHealthItems(digest.ciFailingPRs || [], 'ci-failing', SVG.xCircle, 'CI Failing', titleMeta)}
|
|
1022
|
+
${renderHealthItems(digest.mergeConflictPRs || [], 'conflict', SVG.conflict, 'Merge Conflict', titleMeta)}
|
|
1023
|
+
${renderHealthItems(digest.incompleteChecklistPRs || [], 'incomplete-checklist', SVG.checklist, (pr) => `Incomplete Checklist${pr.checklistStats ? ` (${pr.checklistStats.checked}/${pr.checklistStats.total})` : ''}`, titleMeta)}
|
|
1024
|
+
${renderHealthItems(digest.missingRequiredFilesPRs || [], 'missing-files', SVG.file, 'Missing Required Files', (pr) => (pr.missingRequiredFiles ? escapeHtml(pr.missingRequiredFiles.join(', ')) : truncateTitle(pr.title)))}
|
|
1025
|
+
${renderHealthItems(digest.needsRebasePRs || [], 'needs-rebase', SVG.refresh, (pr) => `Needs Rebase${pr.commitsBehindUpstream ? ` (${pr.commitsBehindUpstream} behind)` : ''}`, titleMeta)}
|
|
1026
|
+
</div>
|
|
1027
|
+
</section>
|
|
1028
|
+
`
|
|
1029
|
+
: ''}
|
|
1030
|
+
|
|
1031
|
+
${waitingOnOthers.length > 0
|
|
1032
|
+
? `
|
|
1033
|
+
<section class="health-section waiting-section">
|
|
1034
|
+
<div class="health-header">
|
|
1035
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--accent-info)" stroke-width="2">
|
|
1036
|
+
<circle cx="12" cy="12" r="10"/>
|
|
1037
|
+
<polyline points="12 6 12 12 16 14"/>
|
|
1038
|
+
</svg>
|
|
1039
|
+
<h2>Waiting on Others</h2>
|
|
1040
|
+
<span class="health-badge" style="background: var(--accent-info-dim); color: var(--accent-info);">${waitingOnOthers.length} PR${waitingOnOthers.length !== 1 ? 's' : ''}</span>
|
|
1041
|
+
</div>
|
|
1042
|
+
<div class="health-items">
|
|
1043
|
+
${renderHealthItems(digest.changesAddressedPRs || [], 'changes-addressed', SVG.checkCircle, 'Changes Addressed', (pr) => `Awaiting re-review${pr.lastMaintainerComment ? ` from @${escapeHtml(pr.lastMaintainerComment.author)}` : ''}`)}
|
|
1044
|
+
${renderHealthItems(digest.waitingOnMaintainerPRs || [], 'waiting-maintainer', SVG.clock, 'Waiting on Maintainer', titleMeta)}
|
|
1045
|
+
${renderHealthItems(digest.ciBlockedPRs || [], 'ci-blocked', SVG.lock, 'CI Blocked', titleMeta)}
|
|
1046
|
+
${renderHealthItems(digest.ciNotRunningPRs || [], 'ci-not-running', SVG.infoCircle, 'CI Not Running', titleMeta)}
|
|
1047
|
+
</div>
|
|
1048
|
+
</section>
|
|
1049
|
+
`
|
|
1050
|
+
: ''}
|
|
1051
|
+
|
|
1052
|
+
${actionRequired.length === 0 && waitingOnOthers.length === 0
|
|
1053
|
+
? `
|
|
1054
|
+
<section class="health-section">
|
|
1055
|
+
<div class="health-header">
|
|
1056
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--accent-open)" stroke-width="2">
|
|
1057
|
+
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
|
|
1058
|
+
<polyline points="22 4 12 14.01 9 11.01"/>
|
|
1059
|
+
</svg>
|
|
1060
|
+
<h2>Health Status</h2>
|
|
1061
|
+
</div>
|
|
1062
|
+
<div class="health-empty">
|
|
1063
|
+
All PRs are healthy - no CI failures, conflicts, or pending responses
|
|
1064
|
+
</div>
|
|
1065
|
+
</section>
|
|
1066
|
+
`
|
|
1067
|
+
: ''}
|
|
1068
|
+
|
|
1069
|
+
${recentlyMerged.length > 0
|
|
1070
|
+
? `
|
|
1071
|
+
<section class="health-section" style="animation-delay: 0.15s;">
|
|
1072
|
+
<div class="health-header">
|
|
1073
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--accent-merged)" stroke-width="2">
|
|
1074
|
+
${SVG.gitMerge}
|
|
1075
|
+
</svg>
|
|
1076
|
+
<h2>Recently Merged</h2>
|
|
1077
|
+
<span class="health-badge" style="background: var(--accent-merged-dim); color: var(--accent-merged);">${recentlyMerged.length} merged</span>
|
|
1078
|
+
</div>
|
|
1079
|
+
<div class="health-items">
|
|
1080
|
+
${recentlyMerged
|
|
1081
|
+
.map((pr) => `
|
|
1082
|
+
<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())}">
|
|
1083
|
+
<div class="health-icon" style="background: var(--accent-merged-dim); color: var(--accent-merged);">
|
|
1084
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1085
|
+
${SVG.gitMerge}
|
|
1086
|
+
</svg>
|
|
1087
|
+
</div>
|
|
1088
|
+
<div class="health-content">
|
|
1089
|
+
<div class="health-title"><a href="${escapeHtml(pr.url)}" target="_blank">${escapeHtml(pr.repo)}#${pr.number}</a> - Merged</div>
|
|
1090
|
+
<div class="health-meta">${truncateTitle(pr.title)}${pr.mergedAt ? ` · ${new Date(pr.mergedAt).toLocaleDateString()}` : ''}</div>
|
|
1091
|
+
</div>
|
|
1092
|
+
</div>
|
|
1093
|
+
`)
|
|
1094
|
+
.join('')}
|
|
1095
|
+
</div>
|
|
1096
|
+
</section>
|
|
1097
|
+
`
|
|
1098
|
+
: ''}
|
|
1099
|
+
|
|
1100
|
+
${(digest.recentlyClosedPRs || []).length > 0
|
|
1101
|
+
? `
|
|
1102
|
+
<section class="health-section" style="animation-delay: 0.2s;">
|
|
1103
|
+
<div class="health-header">
|
|
1104
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--text-muted)" stroke-width="2">
|
|
1105
|
+
<circle cx="12" cy="12" r="10"/>
|
|
1106
|
+
<line x1="15" y1="9" x2="9" y2="15"/>
|
|
1107
|
+
<line x1="9" y1="9" x2="15" y2="15"/>
|
|
1108
|
+
</svg>
|
|
1109
|
+
<h2>Recently Closed</h2>
|
|
1110
|
+
<span class="health-badge" style="background: rgba(110, 118, 129, 0.15); color: var(--text-muted);">${(digest.recentlyClosedPRs || []).length} closed</span>
|
|
1111
|
+
</div>
|
|
1112
|
+
<div class="health-items">
|
|
1113
|
+
${(digest.recentlyClosedPRs || [])
|
|
1114
|
+
.map((pr) => `
|
|
1115
|
+
<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())}">
|
|
1116
|
+
<div class="health-icon" style="background: rgba(110, 118, 129, 0.15); color: var(--text-muted);">
|
|
1117
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1118
|
+
<circle cx="12" cy="12" r="10"/>
|
|
1119
|
+
<line x1="15" y1="9" x2="9" y2="15"/>
|
|
1120
|
+
<line x1="9" y1="9" x2="15" y2="15"/>
|
|
1121
|
+
</svg>
|
|
1122
|
+
</div>
|
|
1123
|
+
<div class="health-content">
|
|
1124
|
+
<div class="health-title"><a href="${escapeHtml(pr.url)}" target="_blank">${escapeHtml(pr.repo)}#${pr.number}</a> - Closed</div>
|
|
1125
|
+
<div class="health-meta">${truncateTitle(pr.title)}${pr.closedAt ? ` · ${new Date(pr.closedAt).toLocaleDateString()}` : ''}</div>
|
|
1126
|
+
</div>
|
|
1127
|
+
</div>
|
|
1128
|
+
`)
|
|
1129
|
+
.join('')}
|
|
1130
|
+
</div>
|
|
1131
|
+
</section>
|
|
1132
|
+
`
|
|
1133
|
+
: ''}
|
|
1134
|
+
|
|
1135
|
+
${autoUnshelvedPRs.length > 0
|
|
1136
|
+
? `
|
|
1137
|
+
<section class="health-section" style="animation-delay: 0.25s;">
|
|
1138
|
+
<div class="health-header">
|
|
1139
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--accent-info)" stroke-width="2">
|
|
1140
|
+
${SVG.bell}
|
|
1141
|
+
</svg>
|
|
1142
|
+
<h2>Auto-Unshelved</h2>
|
|
1143
|
+
<span class="health-badge" style="background: var(--accent-info-dim); color: var(--accent-info);">${autoUnshelvedPRs.length} unshelved</span>
|
|
1144
|
+
</div>
|
|
1145
|
+
<div class="health-items">
|
|
1146
|
+
${renderHealthItems(autoUnshelvedPRs, 'auto-unshelved', SVG.bell, (pr) => 'Auto-Unshelved (' + pr.status.replace(/_/g, ' ') + ')', titleMeta)}
|
|
1147
|
+
</div>
|
|
1148
|
+
</section>
|
|
1149
|
+
`
|
|
1150
|
+
: ''}
|
|
1151
|
+
|
|
1152
|
+
${issueResponses.length > 0
|
|
1153
|
+
? `
|
|
1154
|
+
<section class="health-section" style="animation-delay: 0.3s;">
|
|
1155
|
+
<div class="health-header">
|
|
1156
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--accent-info)" stroke-width="2">
|
|
1157
|
+
${SVG.comment}
|
|
1158
|
+
</svg>
|
|
1159
|
+
<h2>Issue Conversations</h2>
|
|
1160
|
+
<span class="health-badge" style="background: var(--accent-info-dim); color: var(--accent-info);">${issueResponses.length} repl${issueResponses.length !== 1 ? 'ies' : 'y'}</span>
|
|
1161
|
+
</div>
|
|
1162
|
+
<div class="health-items">
|
|
1163
|
+
${issueResponses
|
|
1164
|
+
.map((issue) => `
|
|
1165
|
+
<div class="health-item changes-addressed">
|
|
1166
|
+
<div class="health-icon" style="background: var(--accent-info-dim); color: var(--accent-info);">
|
|
1167
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1168
|
+
${SVG.comment}
|
|
1169
|
+
</svg>
|
|
1170
|
+
</div>
|
|
1171
|
+
<div class="health-content">
|
|
1172
|
+
<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>
|
|
1173
|
+
<div class="health-meta">@${escapeHtml(issue.lastResponseAuthor)}: ${escapeHtml(issue.lastResponseBody.slice(0, 60))}${issue.lastResponseBody.length > 60 ? '...' : ''}</div>
|
|
1174
|
+
</div>
|
|
1175
|
+
</div>
|
|
1176
|
+
`)
|
|
1177
|
+
.join('')}
|
|
1178
|
+
</div>
|
|
1179
|
+
</section>
|
|
1180
|
+
`
|
|
1181
|
+
: ''}
|
|
1182
|
+
|
|
1183
|
+
<div class="main-grid">
|
|
1184
|
+
<div class="card">
|
|
1185
|
+
<div class="card-header">
|
|
1186
|
+
<span class="card-title">PR Status Distribution</span>
|
|
1187
|
+
</div>
|
|
1188
|
+
<div class="card-body">
|
|
1189
|
+
<div class="chart-container">
|
|
1190
|
+
<canvas id="statusChart"></canvas>
|
|
1191
|
+
</div>
|
|
1192
|
+
</div>
|
|
1193
|
+
</div>
|
|
1194
|
+
|
|
1195
|
+
<div class="card">
|
|
1196
|
+
<div class="card-header">
|
|
1197
|
+
<span class="card-title">Repository Breakdown</span>
|
|
1198
|
+
</div>
|
|
1199
|
+
<div class="card-body">
|
|
1200
|
+
<div class="chart-container">
|
|
1201
|
+
<canvas id="reposChart"></canvas>
|
|
1202
|
+
</div>
|
|
1203
|
+
</div>
|
|
1204
|
+
</div>
|
|
1205
|
+
</div>
|
|
1206
|
+
|
|
1207
|
+
<div class="card" style="margin-bottom: 1.25rem;">
|
|
1208
|
+
<div class="card-header">
|
|
1209
|
+
<span class="card-title">Contribution Timeline</span>
|
|
1210
|
+
</div>
|
|
1211
|
+
<div class="card-body">
|
|
1212
|
+
<div class="chart-container" style="height: 250px;">
|
|
1213
|
+
<canvas id="monthlyChart"></canvas>
|
|
1214
|
+
</div>
|
|
1215
|
+
</div>
|
|
1216
|
+
</div>
|
|
1217
|
+
|
|
1218
|
+
|
|
1219
|
+
${activePRList.length > 0
|
|
1220
|
+
? `
|
|
1221
|
+
<section class="pr-list-section">
|
|
1222
|
+
<div class="pr-list-header">
|
|
1223
|
+
<h2 class="pr-list-title">Active Pull Requests</h2>
|
|
1224
|
+
<span class="pr-count">${activePRList.length} open</span>
|
|
1225
|
+
</div>
|
|
1226
|
+
<div class="pr-list">
|
|
1227
|
+
${activePRList
|
|
1228
|
+
.map((pr) => {
|
|
1229
|
+
const hasIssues = pr.ciStatus === 'failing' ||
|
|
1230
|
+
pr.hasMergeConflict ||
|
|
1231
|
+
(pr.hasUnrespondedComment && pr.status !== 'changes_addressed') ||
|
|
1232
|
+
pr.status === 'needs_changes';
|
|
1233
|
+
const isStale = pr.daysSinceActivity >= approachingDormantDays;
|
|
1234
|
+
const itemClass = hasIssues ? 'has-issues' : isStale ? 'stale' : '';
|
|
1235
|
+
const prStatus = pr.ciStatus === 'failing'
|
|
1236
|
+
? 'ci-failing'
|
|
1237
|
+
: pr.hasMergeConflict
|
|
1238
|
+
? 'conflict'
|
|
1239
|
+
: pr.hasUnrespondedComment && pr.status !== 'changes_addressed' && pr.status !== 'failing_ci'
|
|
1240
|
+
? 'needs-response'
|
|
1241
|
+
: pr.status === 'needs_changes'
|
|
1242
|
+
? 'needs-changes'
|
|
1243
|
+
: pr.status === 'changes_addressed'
|
|
1244
|
+
? 'changes-addressed'
|
|
1245
|
+
: 'active';
|
|
1246
|
+
return `
|
|
1247
|
+
<div class="pr-item ${itemClass}" data-status="${prStatus}" data-repo="${escapeHtml(pr.repo)}" data-title="${escapeHtml(pr.title.toLowerCase())}">
|
|
1248
|
+
<div class="pr-status-indicator">
|
|
1249
|
+
${hasIssues
|
|
1250
|
+
? `
|
|
1251
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1252
|
+
<circle cx="12" cy="12" r="10"/>
|
|
1253
|
+
<line x1="12" y1="8" x2="12" y2="12"/>
|
|
1254
|
+
<line x1="12" y1="16" x2="12.01" y2="16"/>
|
|
1255
|
+
</svg>
|
|
1256
|
+
`
|
|
1257
|
+
: `
|
|
1258
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1259
|
+
<circle cx="12" cy="12" r="10"/>
|
|
1260
|
+
<line x1="12" y1="16" x2="12" y2="12"/>
|
|
1261
|
+
<line x1="12" y1="8" x2="12.01" y2="8"/>
|
|
1262
|
+
</svg>
|
|
1263
|
+
`}
|
|
1264
|
+
</div>
|
|
1265
|
+
<div class="pr-content">
|
|
1266
|
+
<div class="pr-title-row">
|
|
1267
|
+
<a href="${escapeHtml(pr.url)}" target="_blank" class="pr-title">${escapeHtml(pr.title)}</a>
|
|
1268
|
+
<span class="pr-repo">${escapeHtml(pr.repo)}#${pr.number}</span>
|
|
1269
|
+
</div>
|
|
1270
|
+
<div class="pr-badges">
|
|
1271
|
+
${pr.ciStatus === 'failing' ? '<span class="badge badge-ci-failing">CI Failing</span>' : ''}
|
|
1272
|
+
${pr.ciStatus === 'passing' ? '<span class="badge badge-passing">CI Passing</span>' : ''}
|
|
1273
|
+
${pr.ciStatus === 'pending' ? '<span class="badge badge-pending">CI Pending</span>' : ''}
|
|
1274
|
+
${pr.hasMergeConflict ? '<span class="badge badge-conflict">Merge Conflict</span>' : ''}
|
|
1275
|
+
${pr.hasUnrespondedComment && pr.status === 'changes_addressed' ? '<span class="badge badge-changes-addressed">Changes Addressed</span>' : ''}
|
|
1276
|
+
${pr.hasUnrespondedComment && pr.status !== 'changes_addressed' && pr.status !== 'failing_ci' ? '<span class="badge badge-needs-response">Needs Response</span>' : ''}
|
|
1277
|
+
${pr.reviewDecision === 'changes_requested' ? '<span class="badge badge-changes-requested">Changes Requested</span>' : ''}
|
|
1278
|
+
${isStale ? `<span class="badge badge-stale">${pr.daysSinceActivity}d inactive</span>` : ''}
|
|
1279
|
+
</div>
|
|
1280
|
+
</div>
|
|
1281
|
+
<div class="pr-activity">
|
|
1282
|
+
${pr.daysSinceActivity === 0 ? 'Today' : pr.daysSinceActivity === 1 ? 'Yesterday' : pr.daysSinceActivity + 'd ago'}
|
|
1283
|
+
</div>
|
|
1284
|
+
</div>`;
|
|
1285
|
+
})
|
|
1286
|
+
.join('')}
|
|
1287
|
+
</div>
|
|
1288
|
+
</section>
|
|
1289
|
+
`
|
|
1290
|
+
: `
|
|
1291
|
+
<section class="pr-list-section">
|
|
1292
|
+
<div class="pr-list-header">
|
|
1293
|
+
<h2 class="pr-list-title">Active Pull Requests</h2>
|
|
1294
|
+
<span class="pr-count">0 open</span>
|
|
1295
|
+
</div>
|
|
1296
|
+
<div class="empty-state">
|
|
1297
|
+
<div class="empty-state-icon">
|
|
1298
|
+
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
1299
|
+
<circle cx="12" cy="12" r="10"/>
|
|
1300
|
+
<path d="M8 12h8"/>
|
|
1301
|
+
</svg>
|
|
1302
|
+
</div>
|
|
1303
|
+
<p>No active pull requests</p>
|
|
1304
|
+
</div>
|
|
1305
|
+
</section>
|
|
1306
|
+
`}
|
|
1307
|
+
|
|
1308
|
+
${shelvedPRs.length > 0
|
|
1309
|
+
? `
|
|
1310
|
+
<section class="pr-list-section" style="margin-top: 1.25rem; opacity: 0.7;">
|
|
1311
|
+
<div class="pr-list-header">
|
|
1312
|
+
<h2 class="pr-list-title">Shelved Pull Requests</h2>
|
|
1313
|
+
<span class="pr-count" style="background: rgba(110, 118, 129, 0.15); color: var(--text-muted);">${shelvedPRs.length} shelved</span>
|
|
1314
|
+
</div>
|
|
1315
|
+
<div class="pr-list">
|
|
1316
|
+
${shelvedPRs
|
|
1317
|
+
.map((pr) => `
|
|
1318
|
+
<div class="pr-item" data-status="shelved" data-repo="${escapeHtml(pr.repo)}" data-title="${escapeHtml(pr.title.toLowerCase())}">
|
|
1319
|
+
<div class="pr-status-indicator" style="background: rgba(110, 118, 129, 0.1); color: var(--text-muted);">
|
|
1320
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1321
|
+
${SVG.box}
|
|
1322
|
+
</svg>
|
|
1323
|
+
</div>
|
|
1324
|
+
<div class="pr-content">
|
|
1325
|
+
<div class="pr-title-row">
|
|
1326
|
+
<a href="${escapeHtml(pr.url)}" target="_blank" class="pr-title">${escapeHtml(pr.title)}</a>
|
|
1327
|
+
<span class="pr-repo">${escapeHtml(pr.repo)}#${pr.number}</span>
|
|
1328
|
+
</div>
|
|
1329
|
+
<div class="pr-badges">
|
|
1330
|
+
<span class="badge badge-days">${pr.daysSinceActivity}d inactive</span>
|
|
1331
|
+
</div>
|
|
1332
|
+
</div>
|
|
1333
|
+
<div class="pr-activity">
|
|
1334
|
+
${pr.daysSinceActivity === 0 ? 'Today' : pr.daysSinceActivity === 1 ? 'Yesterday' : pr.daysSinceActivity + 'd ago'}
|
|
1335
|
+
</div>
|
|
1336
|
+
</div>`)
|
|
1337
|
+
.join('')}
|
|
1338
|
+
</div>
|
|
1339
|
+
</section>
|
|
1340
|
+
`
|
|
1341
|
+
: ''}
|
|
1342
|
+
|
|
1343
|
+
<footer class="footer">
|
|
1344
|
+
<p>OSS Autopilot // Mission Control</p>
|
|
1345
|
+
<p style="margin-top: 0.25rem;">Dashboard generated: ${digest.generatedAt ? new Date(digest.generatedAt).toISOString() : 'Unknown'}</p>
|
|
1346
|
+
</footer>
|
|
1347
|
+
</div>
|
|
1348
|
+
|
|
1349
|
+
<script>
|
|
1350
|
+
// === Theme Toggle ===
|
|
1351
|
+
(function() {
|
|
1352
|
+
var html = document.documentElement;
|
|
1353
|
+
var toggle = document.getElementById('themeToggle');
|
|
1354
|
+
var sunIcon = document.getElementById('themeIconSun');
|
|
1355
|
+
var moonIcon = document.getElementById('themeIconMoon');
|
|
1356
|
+
var label = document.getElementById('themeLabel');
|
|
1357
|
+
|
|
1358
|
+
function getEffectiveTheme() {
|
|
1359
|
+
try {
|
|
1360
|
+
var stored = localStorage.getItem('oss-dashboard-theme');
|
|
1361
|
+
if (stored === 'light' || stored === 'dark') return stored;
|
|
1362
|
+
} catch (e) { /* localStorage unavailable (private browsing) */ }
|
|
1363
|
+
return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
function applyTheme(theme) {
|
|
1367
|
+
html.setAttribute('data-theme', theme);
|
|
1368
|
+
if (theme === 'light') {
|
|
1369
|
+
sunIcon.style.display = 'none';
|
|
1370
|
+
moonIcon.style.display = 'block';
|
|
1371
|
+
label.textContent = 'Dark';
|
|
1372
|
+
} else {
|
|
1373
|
+
sunIcon.style.display = 'block';
|
|
1374
|
+
moonIcon.style.display = 'none';
|
|
1375
|
+
label.textContent = 'Light';
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
applyTheme(getEffectiveTheme());
|
|
1380
|
+
|
|
1381
|
+
toggle.addEventListener('click', function() {
|
|
1382
|
+
var current = html.getAttribute('data-theme');
|
|
1383
|
+
var next = current === 'dark' ? 'light' : 'dark';
|
|
1384
|
+
try { localStorage.setItem('oss-dashboard-theme', next); } catch (e) { /* private browsing */ }
|
|
1385
|
+
applyTheme(next);
|
|
1386
|
+
});
|
|
1387
|
+
})();
|
|
1388
|
+
|
|
1389
|
+
// === Filtering & Search ===
|
|
1390
|
+
(function() {
|
|
1391
|
+
var searchInput = document.getElementById('searchInput');
|
|
1392
|
+
var statusFilter = document.getElementById('statusFilter');
|
|
1393
|
+
var repoFilter = document.getElementById('repoFilter');
|
|
1394
|
+
var filterCount = document.getElementById('filterCount');
|
|
1395
|
+
|
|
1396
|
+
function applyFilters() {
|
|
1397
|
+
var query = searchInput.value.toLowerCase().trim();
|
|
1398
|
+
var status = statusFilter.value;
|
|
1399
|
+
var repo = repoFilter.value;
|
|
1400
|
+
var allItems = document.querySelectorAll('.health-item[data-status], .pr-item[data-status]');
|
|
1401
|
+
var visible = 0;
|
|
1402
|
+
var total = allItems.length;
|
|
1403
|
+
|
|
1404
|
+
allItems.forEach(function(item) {
|
|
1405
|
+
var itemStatus = item.getAttribute('data-status') || '';
|
|
1406
|
+
var itemRepo = item.getAttribute('data-repo') || '';
|
|
1407
|
+
var itemTitle = item.getAttribute('data-title') || '';
|
|
1408
|
+
|
|
1409
|
+
var matchesStatus = (status === 'all') || (itemStatus === status);
|
|
1410
|
+
var matchesRepo = (repo === 'all') || (itemRepo === repo);
|
|
1411
|
+
var matchesSearch = !query || itemTitle.indexOf(query) !== -1;
|
|
1412
|
+
|
|
1413
|
+
if (matchesStatus && matchesRepo && matchesSearch) {
|
|
1414
|
+
item.setAttribute('data-hidden', 'false');
|
|
1415
|
+
visible++;
|
|
1416
|
+
} else {
|
|
1417
|
+
item.setAttribute('data-hidden', 'true');
|
|
1418
|
+
}
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1421
|
+
// Show/hide parent sections if all children are hidden
|
|
1422
|
+
var sections = document.querySelectorAll('.health-section, .pr-list-section');
|
|
1423
|
+
sections.forEach(function(section) {
|
|
1424
|
+
var items = section.querySelectorAll('.health-item[data-status], .pr-item[data-status]');
|
|
1425
|
+
if (items.length === 0) return; // sections without filterable items (e.g. empty state)
|
|
1426
|
+
var anyVisible = false;
|
|
1427
|
+
items.forEach(function(item) {
|
|
1428
|
+
if (item.getAttribute('data-hidden') !== 'true') anyVisible = true;
|
|
1429
|
+
});
|
|
1430
|
+
section.style.display = anyVisible ? '' : 'none';
|
|
1431
|
+
});
|
|
1432
|
+
|
|
1433
|
+
var isFiltering = (status !== 'all' || repo !== 'all' || query.length > 0);
|
|
1434
|
+
filterCount.textContent = isFiltering ? (visible + ' of ' + total + ' items') : '';
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
searchInput.addEventListener('input', applyFilters);
|
|
1438
|
+
statusFilter.addEventListener('change', applyFilters);
|
|
1439
|
+
repoFilter.addEventListener('change', applyFilters);
|
|
1440
|
+
})();
|
|
1441
|
+
|
|
1442
|
+
// === Chart.js Configuration ===
|
|
1443
|
+
Chart.defaults.color = '#6e7681';
|
|
1444
|
+
Chart.defaults.borderColor = 'rgba(48, 54, 61, 0.4)';
|
|
1445
|
+
Chart.defaults.font.family = "'Geist', sans-serif";
|
|
1446
|
+
Chart.defaults.font.size = 11;
|
|
1447
|
+
|
|
1448
|
+
// === Status Doughnut ===
|
|
1449
|
+
new Chart(document.getElementById('statusChart'), {
|
|
1450
|
+
type: 'doughnut',
|
|
1451
|
+
data: {
|
|
1452
|
+
labels: ['Active', 'Shelved', 'Merged', 'Closed'],
|
|
1453
|
+
datasets: [{
|
|
1454
|
+
data: [${stats.activePRs}, ${stats.shelvedPRs}, ${stats.mergedPRs}, ${stats.closedPRs}],
|
|
1455
|
+
backgroundColor: ['#3fb950', '#6e7681', '#a855f7', '#484f58'],
|
|
1456
|
+
borderColor: 'rgba(8, 11, 16, 0.8)',
|
|
1457
|
+
borderWidth: 2,
|
|
1458
|
+
hoverOffset: 8
|
|
1459
|
+
}]
|
|
1460
|
+
},
|
|
1461
|
+
options: {
|
|
1462
|
+
responsive: true,
|
|
1463
|
+
maintainAspectRatio: false,
|
|
1464
|
+
cutout: '65%',
|
|
1465
|
+
plugins: {
|
|
1466
|
+
legend: {
|
|
1467
|
+
position: 'bottom',
|
|
1468
|
+
labels: { padding: 16, usePointStyle: true, pointStyle: 'circle', font: { size: 11 } }
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1474
|
+
// === Repository Breakdown (with "Other" bucket + percentage tooltips) ===
|
|
1475
|
+
${(() => {
|
|
1476
|
+
// Filter helper: exclude repos matching excludeRepos/excludeOrgs or below minStars (#216)
|
|
1477
|
+
const { excludeRepos: exRepos = [], excludeOrgs: exOrgs, minStars } = state.config;
|
|
1478
|
+
const starThreshold = minStars ?? 50;
|
|
1479
|
+
const shouldExcludeRepo = (repo) => {
|
|
1480
|
+
const repoLower = repo.toLowerCase();
|
|
1481
|
+
if (exRepos.some((r) => r.toLowerCase() === repoLower))
|
|
1482
|
+
return true;
|
|
1483
|
+
if (exOrgs?.some((o) => o.toLowerCase() === repoLower.split('/')[0]))
|
|
1484
|
+
return true;
|
|
1485
|
+
const score = (state.repoScores || {})[repo];
|
|
1486
|
+
// Fail-open: repos without cached star data are shown (not excluded).
|
|
1487
|
+
// Unlike issue-discovery (fail-closed), the dashboard shows the user's own
|
|
1488
|
+
// contribution history — hiding repos just because a star fetch failed would be confusing.
|
|
1489
|
+
if (score?.stargazersCount !== undefined && score.stargazersCount < starThreshold)
|
|
1490
|
+
return true;
|
|
1491
|
+
return false;
|
|
1492
|
+
};
|
|
1493
|
+
// Sort repos by total PRs (merged + active + closed) and build "Other" bucket
|
|
1494
|
+
const allRepoEntries = Object.entries(
|
|
1495
|
+
// Rebuild from full prsByRepo to get all repos, not just top 10
|
|
1496
|
+
(() => {
|
|
1497
|
+
const all = {};
|
|
1498
|
+
for (const pr of digest.openPRs || []) {
|
|
1499
|
+
if (shouldExcludeRepo(pr.repo))
|
|
1500
|
+
continue;
|
|
1501
|
+
if (!all[pr.repo])
|
|
1502
|
+
all[pr.repo] = { active: 0, merged: 0, closed: 0 };
|
|
1503
|
+
all[pr.repo].active++;
|
|
1504
|
+
}
|
|
1505
|
+
for (const [repo, score] of Object.entries(state.repoScores || {})) {
|
|
1506
|
+
if (shouldExcludeRepo(repo))
|
|
1507
|
+
continue;
|
|
1508
|
+
if (!all[repo])
|
|
1509
|
+
all[repo] = { active: 0, merged: 0, closed: 0 };
|
|
1510
|
+
all[repo].merged = score.mergedPRCount;
|
|
1511
|
+
all[repo].closed = score.closedWithoutMergeCount;
|
|
1512
|
+
}
|
|
1513
|
+
return all;
|
|
1514
|
+
})()).sort((a, b) => {
|
|
1515
|
+
const totalA = a[1].merged + a[1].active + a[1].closed;
|
|
1516
|
+
const totalB = b[1].merged + b[1].active + b[1].closed;
|
|
1517
|
+
return totalB - totalA;
|
|
1518
|
+
});
|
|
1519
|
+
const displayRepos = allRepoEntries.slice(0, 10);
|
|
1520
|
+
const otherRepos = allRepoEntries.slice(10);
|
|
1521
|
+
const grandTotal = allRepoEntries.reduce((sum, [, d]) => sum + d.merged + d.active + d.closed, 0);
|
|
1522
|
+
if (otherRepos.length > 0) {
|
|
1523
|
+
const otherData = otherRepos.reduce((acc, [, d]) => ({
|
|
1524
|
+
active: acc.active + d.active,
|
|
1525
|
+
merged: acc.merged + d.merged,
|
|
1526
|
+
closed: acc.closed + d.closed,
|
|
1527
|
+
}), { active: 0, merged: 0, closed: 0 });
|
|
1528
|
+
displayRepos.push(['Other', otherData]);
|
|
1529
|
+
}
|
|
1530
|
+
const repoLabels = displayRepos.map(([repo]) => (repo === 'Other' ? 'Other' : repo.split('/')[1] || repo));
|
|
1531
|
+
const mergedData = displayRepos.map(([, d]) => d.merged);
|
|
1532
|
+
const activeData = displayRepos.map(([, d]) => d.active);
|
|
1533
|
+
const closedData = displayRepos.map(([, d]) => d.closed);
|
|
1534
|
+
return `
|
|
1535
|
+
new Chart(document.getElementById('reposChart'), {
|
|
1536
|
+
type: 'bar',
|
|
1537
|
+
data: {
|
|
1538
|
+
labels: ${JSON.stringify(repoLabels)},
|
|
1539
|
+
datasets: [
|
|
1540
|
+
{ label: 'Merged', data: ${JSON.stringify(mergedData)}, backgroundColor: '#a855f7', borderRadius: 3 },
|
|
1541
|
+
{ label: 'Active', data: ${JSON.stringify(activeData)}, backgroundColor: '#3fb950', borderRadius: 3 },
|
|
1542
|
+
{ label: 'Closed', data: ${JSON.stringify(closedData)}, backgroundColor: '#484f58', borderRadius: 3 }
|
|
1543
|
+
]
|
|
1544
|
+
},
|
|
1545
|
+
options: {
|
|
1546
|
+
responsive: true,
|
|
1547
|
+
maintainAspectRatio: false,
|
|
1548
|
+
scales: {
|
|
1549
|
+
x: { stacked: true, grid: { display: false }, ticks: { font: { size: 10 } } },
|
|
1550
|
+
y: { stacked: true, grid: { color: 'rgba(48, 54, 61, 0.3)' }, ticks: { stepSize: 1 } }
|
|
1551
|
+
},
|
|
1552
|
+
plugins: {
|
|
1553
|
+
legend: { position: 'bottom', labels: { padding: 16, usePointStyle: true, pointStyle: 'circle', font: { size: 11 } } },
|
|
1554
|
+
tooltip: {
|
|
1555
|
+
callbacks: {
|
|
1556
|
+
afterBody: function(context) {
|
|
1557
|
+
const idx = context[0].dataIndex;
|
|
1558
|
+
const total = ${JSON.stringify(mergedData)}[idx] + ${JSON.stringify(activeData)}[idx] + ${JSON.stringify(closedData)}[idx];
|
|
1559
|
+
const pct = ${grandTotal} > 0 ? ((total / ${grandTotal}) * 100).toFixed(1) : '0.0';
|
|
1560
|
+
return pct + '% of all PRs';
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
});`;
|
|
1567
|
+
})()}
|
|
1568
|
+
|
|
1569
|
+
// === Contribution Timeline (grouped bar: Opened/Merged/Closed) ===
|
|
1570
|
+
${(() => {
|
|
1571
|
+
// Generate a contiguous range of the last 6 months from today
|
|
1572
|
+
// This avoids gaps when historical data spans years (e.g. 2019 and 2026)
|
|
1573
|
+
const now = new Date();
|
|
1574
|
+
const allMonths = [];
|
|
1575
|
+
for (let offset = 5; offset >= 0; offset--) {
|
|
1576
|
+
const d = new Date(now.getFullYear(), now.getMonth() - offset, 1);
|
|
1577
|
+
allMonths.push(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`);
|
|
1578
|
+
}
|
|
1579
|
+
return `
|
|
1580
|
+
const timelineMonths = ${JSON.stringify(allMonths)};
|
|
1581
|
+
const openedData = ${JSON.stringify(monthlyOpened)};
|
|
1582
|
+
const mergedData = ${JSON.stringify(monthlyMerged)};
|
|
1583
|
+
const closedData = ${JSON.stringify(monthlyClosed)};
|
|
1584
|
+
new Chart(document.getElementById('monthlyChart'), {
|
|
1585
|
+
type: 'bar',
|
|
1586
|
+
data: {
|
|
1587
|
+
labels: timelineMonths,
|
|
1588
|
+
datasets: [
|
|
1589
|
+
{
|
|
1590
|
+
label: 'Opened',
|
|
1591
|
+
data: timelineMonths.map(m => openedData[m] || 0),
|
|
1592
|
+
backgroundColor: '#58a6ff',
|
|
1593
|
+
borderRadius: 3
|
|
1594
|
+
},
|
|
1595
|
+
{
|
|
1596
|
+
label: 'Merged',
|
|
1597
|
+
data: timelineMonths.map(m => mergedData[m] || 0),
|
|
1598
|
+
backgroundColor: '#a855f7',
|
|
1599
|
+
borderRadius: 3
|
|
1600
|
+
},
|
|
1601
|
+
{
|
|
1602
|
+
label: 'Closed',
|
|
1603
|
+
data: timelineMonths.map(m => closedData[m] || 0),
|
|
1604
|
+
backgroundColor: '#484f58',
|
|
1605
|
+
borderRadius: 3
|
|
1606
|
+
}
|
|
1607
|
+
]
|
|
1608
|
+
},
|
|
1609
|
+
options: {
|
|
1610
|
+
responsive: true,
|
|
1611
|
+
maintainAspectRatio: false,
|
|
1612
|
+
scales: {
|
|
1613
|
+
x: { grid: { display: false } },
|
|
1614
|
+
y: { grid: { color: 'rgba(48, 54, 61, 0.3)' }, beginAtZero: true, ticks: { stepSize: 1 } }
|
|
1615
|
+
},
|
|
1616
|
+
plugins: {
|
|
1617
|
+
legend: { position: 'bottom', labels: { padding: 16, usePointStyle: true, pointStyle: 'circle', font: { size: 11 } } }
|
|
1618
|
+
},
|
|
1619
|
+
interaction: { intersect: false, mode: 'index' }
|
|
1620
|
+
}
|
|
1621
|
+
});`;
|
|
1622
|
+
})()}
|
|
1623
|
+
|
|
1624
|
+
</script>
|
|
1625
|
+
</body>
|
|
1626
|
+
</html>`;
|
|
1627
|
+
}
|