@oss-autopilot/core 0.42.0 → 0.42.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.bundle.cjs +1026 -1018
- package/dist/cli.js +18 -30
- package/dist/commands/check-integration.js +5 -4
- package/dist/commands/comments.js +24 -24
- package/dist/commands/daily.d.ts +0 -1
- package/dist/commands/daily.js +18 -16
- package/dist/commands/dashboard-components.d.ts +33 -0
- package/dist/commands/dashboard-components.js +57 -0
- package/dist/commands/dashboard-data.js +7 -6
- package/dist/commands/dashboard-formatters.d.ts +20 -0
- package/dist/commands/dashboard-formatters.js +33 -0
- package/dist/commands/dashboard-scripts.d.ts +7 -0
- package/dist/commands/dashboard-scripts.js +281 -0
- package/dist/commands/dashboard-server.js +3 -2
- package/dist/commands/dashboard-styles.d.ts +5 -0
- package/dist/commands/dashboard-styles.js +765 -0
- package/dist/commands/dashboard-templates.d.ts +6 -18
- package/dist/commands/dashboard-templates.js +30 -1134
- package/dist/commands/dashboard.js +2 -1
- package/dist/commands/dismiss.d.ts +6 -6
- package/dist/commands/dismiss.js +13 -13
- package/dist/commands/local-repos.js +2 -1
- package/dist/commands/parse-list.js +2 -1
- package/dist/commands/startup.js +6 -16
- package/dist/commands/validation.d.ts +3 -1
- package/dist/commands/validation.js +12 -6
- package/dist/core/errors.d.ts +9 -0
- package/dist/core/errors.js +17 -0
- package/dist/core/github-stats.d.ts +14 -21
- package/dist/core/github-stats.js +84 -138
- package/dist/core/http-cache.d.ts +6 -0
- package/dist/core/http-cache.js +16 -4
- package/dist/core/index.d.ts +3 -2
- package/dist/core/index.js +3 -2
- package/dist/core/issue-conversation.js +4 -4
- package/dist/core/issue-discovery.d.ts +5 -0
- package/dist/core/issue-discovery.js +70 -93
- package/dist/core/issue-vetting.js +17 -17
- package/dist/core/logger.d.ts +5 -0
- package/dist/core/logger.js +8 -0
- package/dist/core/pr-monitor.d.ts +6 -20
- package/dist/core/pr-monitor.js +16 -52
- package/dist/core/review-analysis.js +8 -6
- package/dist/core/state.js +4 -5
- package/dist/core/test-utils.d.ts +14 -0
- package/dist/core/test-utils.js +125 -0
- package/dist/core/utils.d.ts +11 -0
- package/dist/core/utils.js +21 -0
- package/dist/formatters/json.d.ts +0 -1
- package/package.json +1 -1
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side JavaScript for the dashboard: theme toggle, filtering, Chart.js charts.
|
|
3
|
+
*/
|
|
4
|
+
/** Static client-side JS: theme toggle + filter/search logic. */
|
|
5
|
+
const THEME_AND_FILTER_SCRIPT = `
|
|
6
|
+
// === Theme Toggle ===
|
|
7
|
+
(function() {
|
|
8
|
+
var html = document.documentElement;
|
|
9
|
+
var toggle = document.getElementById('themeToggle');
|
|
10
|
+
var sunIcon = document.getElementById('themeIconSun');
|
|
11
|
+
var moonIcon = document.getElementById('themeIconMoon');
|
|
12
|
+
var label = document.getElementById('themeLabel');
|
|
13
|
+
|
|
14
|
+
function getEffectiveTheme() {
|
|
15
|
+
try {
|
|
16
|
+
var stored = localStorage.getItem('oss-dashboard-theme');
|
|
17
|
+
if (stored === 'light' || stored === 'dark') return stored;
|
|
18
|
+
} catch (e) { /* localStorage unavailable (private browsing) */ }
|
|
19
|
+
return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function applyTheme(theme) {
|
|
23
|
+
html.setAttribute('data-theme', theme);
|
|
24
|
+
if (theme === 'light') {
|
|
25
|
+
sunIcon.style.display = 'none';
|
|
26
|
+
moonIcon.style.display = 'block';
|
|
27
|
+
label.textContent = 'Dark';
|
|
28
|
+
} else {
|
|
29
|
+
sunIcon.style.display = 'block';
|
|
30
|
+
moonIcon.style.display = 'none';
|
|
31
|
+
label.textContent = 'Light';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
applyTheme(getEffectiveTheme());
|
|
36
|
+
|
|
37
|
+
toggle.addEventListener('click', function() {
|
|
38
|
+
var current = html.getAttribute('data-theme');
|
|
39
|
+
var next = current === 'dark' ? 'light' : 'dark';
|
|
40
|
+
try { localStorage.setItem('oss-dashboard-theme', next); } catch (e) { /* private browsing */ }
|
|
41
|
+
applyTheme(next);
|
|
42
|
+
});
|
|
43
|
+
})();
|
|
44
|
+
|
|
45
|
+
// === Filtering & Search ===
|
|
46
|
+
(function() {
|
|
47
|
+
var searchInput = document.getElementById('searchInput');
|
|
48
|
+
var statusFilter = document.getElementById('statusFilter');
|
|
49
|
+
var repoFilter = document.getElementById('repoFilter');
|
|
50
|
+
var filterCount = document.getElementById('filterCount');
|
|
51
|
+
|
|
52
|
+
function applyFilters() {
|
|
53
|
+
var query = searchInput.value.toLowerCase().trim();
|
|
54
|
+
var status = statusFilter.value;
|
|
55
|
+
var repo = repoFilter.value;
|
|
56
|
+
var allItems = document.querySelectorAll('.health-item[data-status], .pr-item[data-status]');
|
|
57
|
+
var visible = 0;
|
|
58
|
+
var total = allItems.length;
|
|
59
|
+
|
|
60
|
+
allItems.forEach(function(item) {
|
|
61
|
+
var itemStatus = item.getAttribute('data-status') || '';
|
|
62
|
+
var itemRepo = item.getAttribute('data-repo') || '';
|
|
63
|
+
var itemTitle = item.getAttribute('data-title') || '';
|
|
64
|
+
|
|
65
|
+
var matchesStatus = (status === 'all') || (itemStatus === status);
|
|
66
|
+
var matchesRepo = (repo === 'all') || (itemRepo === repo);
|
|
67
|
+
var matchesSearch = !query || itemTitle.indexOf(query) !== -1;
|
|
68
|
+
|
|
69
|
+
if (matchesStatus && matchesRepo && matchesSearch) {
|
|
70
|
+
item.setAttribute('data-hidden', 'false');
|
|
71
|
+
visible++;
|
|
72
|
+
} else {
|
|
73
|
+
item.setAttribute('data-hidden', 'true');
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Show/hide parent sections if all children are hidden
|
|
78
|
+
var sections = document.querySelectorAll('.health-section, .pr-list-section');
|
|
79
|
+
sections.forEach(function(section) {
|
|
80
|
+
var items = section.querySelectorAll('.health-item[data-status], .pr-item[data-status]');
|
|
81
|
+
if (items.length === 0) return; // sections without filterable items (e.g. empty state)
|
|
82
|
+
var anyVisible = false;
|
|
83
|
+
items.forEach(function(item) {
|
|
84
|
+
if (item.getAttribute('data-hidden') !== 'true') anyVisible = true;
|
|
85
|
+
});
|
|
86
|
+
section.style.display = anyVisible ? '' : 'none';
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
var isFiltering = (status !== 'all' || repo !== 'all' || query.length > 0);
|
|
90
|
+
filterCount.textContent = isFiltering ? (visible + ' of ' + total + ' items') : '';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
searchInput.addEventListener('input', applyFilters);
|
|
94
|
+
statusFilter.addEventListener('change', applyFilters);
|
|
95
|
+
repoFilter.addEventListener('change', applyFilters);
|
|
96
|
+
})();
|
|
97
|
+
`;
|
|
98
|
+
/** Generate the Chart.js JavaScript for the dashboard. */
|
|
99
|
+
export function generateDashboardScripts(stats, monthlyMerged, monthlyClosed, monthlyOpened, digest, state) {
|
|
100
|
+
// === Status Doughnut ===
|
|
101
|
+
const statusChart = `
|
|
102
|
+
Chart.defaults.color = '#6e7681';
|
|
103
|
+
Chart.defaults.borderColor = 'rgba(48, 54, 61, 0.4)';
|
|
104
|
+
Chart.defaults.font.family = "'Geist', sans-serif";
|
|
105
|
+
Chart.defaults.font.size = 11;
|
|
106
|
+
|
|
107
|
+
// === Status Doughnut ===
|
|
108
|
+
new Chart(document.getElementById('statusChart'), {
|
|
109
|
+
type: 'doughnut',
|
|
110
|
+
data: {
|
|
111
|
+
labels: ['Active', 'Shelved', 'Merged', 'Closed'],
|
|
112
|
+
datasets: [{
|
|
113
|
+
data: [${stats.activePRs}, ${stats.shelvedPRs}, ${stats.mergedPRs}, ${stats.closedPRs}],
|
|
114
|
+
backgroundColor: ['#3fb950', '#6e7681', '#a855f7', '#484f58'],
|
|
115
|
+
borderColor: 'rgba(8, 11, 16, 0.8)',
|
|
116
|
+
borderWidth: 2,
|
|
117
|
+
hoverOffset: 8
|
|
118
|
+
}]
|
|
119
|
+
},
|
|
120
|
+
options: {
|
|
121
|
+
responsive: true,
|
|
122
|
+
maintainAspectRatio: false,
|
|
123
|
+
cutout: '65%',
|
|
124
|
+
plugins: {
|
|
125
|
+
legend: {
|
|
126
|
+
position: 'bottom',
|
|
127
|
+
labels: { padding: 16, usePointStyle: true, pointStyle: 'circle', font: { size: 11 } }
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
});`;
|
|
132
|
+
// === Repository Breakdown ===
|
|
133
|
+
const repoChart = (() => {
|
|
134
|
+
// Filter helper: exclude repos matching excludeRepos/excludeOrgs or below minStars (#216)
|
|
135
|
+
const { excludeRepos: exRepos = [], excludeOrgs: exOrgs, minStars } = state.config;
|
|
136
|
+
const starThreshold = minStars ?? 50;
|
|
137
|
+
const shouldExcludeRepo = (repo) => {
|
|
138
|
+
const repoLower = repo.toLowerCase();
|
|
139
|
+
if (exRepos.some((r) => r.toLowerCase() === repoLower))
|
|
140
|
+
return true;
|
|
141
|
+
if (exOrgs?.some((o) => o.toLowerCase() === repoLower.split('/')[0]))
|
|
142
|
+
return true;
|
|
143
|
+
const score = (state.repoScores || {})[repo];
|
|
144
|
+
// Fail-open: repos without cached star data are shown (not excluded).
|
|
145
|
+
// Unlike issue-discovery (fail-closed), the dashboard shows the user's own
|
|
146
|
+
// contribution history — hiding repos just because a star fetch failed would be confusing.
|
|
147
|
+
if (score?.stargazersCount !== undefined && score.stargazersCount < starThreshold)
|
|
148
|
+
return true;
|
|
149
|
+
return false;
|
|
150
|
+
};
|
|
151
|
+
// Sort repos by total PRs (merged + active + closed) and build "Other" bucket
|
|
152
|
+
const allRepoEntries = Object.entries(
|
|
153
|
+
// Rebuild from full prsByRepo to get all repos, not just top 10
|
|
154
|
+
(() => {
|
|
155
|
+
const all = {};
|
|
156
|
+
for (const pr of digest.openPRs || []) {
|
|
157
|
+
if (shouldExcludeRepo(pr.repo))
|
|
158
|
+
continue;
|
|
159
|
+
if (!all[pr.repo])
|
|
160
|
+
all[pr.repo] = { active: 0, merged: 0, closed: 0 };
|
|
161
|
+
all[pr.repo].active++;
|
|
162
|
+
}
|
|
163
|
+
for (const [repo, score] of Object.entries(state.repoScores || {})) {
|
|
164
|
+
if (shouldExcludeRepo(repo))
|
|
165
|
+
continue;
|
|
166
|
+
if (!all[repo])
|
|
167
|
+
all[repo] = { active: 0, merged: 0, closed: 0 };
|
|
168
|
+
all[repo].merged = score.mergedPRCount;
|
|
169
|
+
all[repo].closed = score.closedWithoutMergeCount;
|
|
170
|
+
}
|
|
171
|
+
return all;
|
|
172
|
+
})()).sort((a, b) => {
|
|
173
|
+
const totalA = a[1].merged + a[1].active + a[1].closed;
|
|
174
|
+
const totalB = b[1].merged + b[1].active + b[1].closed;
|
|
175
|
+
return totalB - totalA;
|
|
176
|
+
});
|
|
177
|
+
const displayRepos = allRepoEntries.slice(0, 10);
|
|
178
|
+
const otherRepos = allRepoEntries.slice(10);
|
|
179
|
+
const grandTotal = allRepoEntries.reduce((sum, [, d]) => sum + d.merged + d.active + d.closed, 0);
|
|
180
|
+
if (otherRepos.length > 0) {
|
|
181
|
+
const otherData = otherRepos.reduce((acc, [, d]) => ({
|
|
182
|
+
active: acc.active + d.active,
|
|
183
|
+
merged: acc.merged + d.merged,
|
|
184
|
+
closed: acc.closed + d.closed,
|
|
185
|
+
}), { active: 0, merged: 0, closed: 0 });
|
|
186
|
+
displayRepos.push(['Other', otherData]);
|
|
187
|
+
}
|
|
188
|
+
const repoLabels = displayRepos.map(([repo]) => (repo === 'Other' ? 'Other' : repo.split('/')[1] || repo));
|
|
189
|
+
const mergedData = displayRepos.map(([, d]) => d.merged);
|
|
190
|
+
const activeData = displayRepos.map(([, d]) => d.active);
|
|
191
|
+
const closedData = displayRepos.map(([, d]) => d.closed);
|
|
192
|
+
return `
|
|
193
|
+
new Chart(document.getElementById('reposChart'), {
|
|
194
|
+
type: 'bar',
|
|
195
|
+
data: {
|
|
196
|
+
labels: ${JSON.stringify(repoLabels)},
|
|
197
|
+
datasets: [
|
|
198
|
+
{ label: 'Merged', data: ${JSON.stringify(mergedData)}, backgroundColor: '#a855f7', borderRadius: 3 },
|
|
199
|
+
{ label: 'Active', data: ${JSON.stringify(activeData)}, backgroundColor: '#3fb950', borderRadius: 3 },
|
|
200
|
+
{ label: 'Closed', data: ${JSON.stringify(closedData)}, backgroundColor: '#484f58', borderRadius: 3 }
|
|
201
|
+
]
|
|
202
|
+
},
|
|
203
|
+
options: {
|
|
204
|
+
responsive: true,
|
|
205
|
+
maintainAspectRatio: false,
|
|
206
|
+
scales: {
|
|
207
|
+
x: { stacked: true, grid: { display: false }, ticks: { font: { size: 10 } } },
|
|
208
|
+
y: { stacked: true, grid: { color: 'rgba(48, 54, 61, 0.3)' }, ticks: { stepSize: 1 } }
|
|
209
|
+
},
|
|
210
|
+
plugins: {
|
|
211
|
+
legend: { position: 'bottom', labels: { padding: 16, usePointStyle: true, pointStyle: 'circle', font: { size: 11 } } },
|
|
212
|
+
tooltip: {
|
|
213
|
+
callbacks: {
|
|
214
|
+
afterBody: function(context) {
|
|
215
|
+
const idx = context[0].dataIndex;
|
|
216
|
+
const total = ${JSON.stringify(mergedData)}[idx] + ${JSON.stringify(activeData)}[idx] + ${JSON.stringify(closedData)}[idx];
|
|
217
|
+
const pct = ${grandTotal} > 0 ? ((total / ${grandTotal}) * 100).toFixed(1) : '0.0';
|
|
218
|
+
return pct + '% of all PRs';
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
});`;
|
|
225
|
+
})();
|
|
226
|
+
// === Contribution Timeline ===
|
|
227
|
+
const timelineChart = (() => {
|
|
228
|
+
// Generate a contiguous range of the last 6 months from today
|
|
229
|
+
// This avoids gaps when historical data spans years (e.g. 2019 and 2026)
|
|
230
|
+
const now = new Date();
|
|
231
|
+
const allMonths = [];
|
|
232
|
+
for (let offset = 5; offset >= 0; offset--) {
|
|
233
|
+
const d = new Date(now.getFullYear(), now.getMonth() - offset, 1);
|
|
234
|
+
allMonths.push(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`);
|
|
235
|
+
}
|
|
236
|
+
return `
|
|
237
|
+
const timelineMonths = ${JSON.stringify(allMonths)};
|
|
238
|
+
const openedData = ${JSON.stringify(monthlyOpened)};
|
|
239
|
+
const mergedData = ${JSON.stringify(monthlyMerged)};
|
|
240
|
+
const closedData = ${JSON.stringify(monthlyClosed)};
|
|
241
|
+
new Chart(document.getElementById('monthlyChart'), {
|
|
242
|
+
type: 'bar',
|
|
243
|
+
data: {
|
|
244
|
+
labels: timelineMonths,
|
|
245
|
+
datasets: [
|
|
246
|
+
{
|
|
247
|
+
label: 'Opened',
|
|
248
|
+
data: timelineMonths.map(m => openedData[m] || 0),
|
|
249
|
+
backgroundColor: '#58a6ff',
|
|
250
|
+
borderRadius: 3
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
label: 'Merged',
|
|
254
|
+
data: timelineMonths.map(m => mergedData[m] || 0),
|
|
255
|
+
backgroundColor: '#a855f7',
|
|
256
|
+
borderRadius: 3
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
label: 'Closed',
|
|
260
|
+
data: timelineMonths.map(m => closedData[m] || 0),
|
|
261
|
+
backgroundColor: '#484f58',
|
|
262
|
+
borderRadius: 3
|
|
263
|
+
}
|
|
264
|
+
]
|
|
265
|
+
},
|
|
266
|
+
options: {
|
|
267
|
+
responsive: true,
|
|
268
|
+
maintainAspectRatio: false,
|
|
269
|
+
scales: {
|
|
270
|
+
x: { grid: { display: false } },
|
|
271
|
+
y: { grid: { color: 'rgba(48, 54, 61, 0.3)' }, beginAtZero: true, ticks: { stepSize: 1 } }
|
|
272
|
+
},
|
|
273
|
+
plugins: {
|
|
274
|
+
legend: { position: 'bottom', labels: { padding: 16, usePointStyle: true, pointStyle: 'circle', font: { size: 11 } } }
|
|
275
|
+
},
|
|
276
|
+
interaction: { intersect: false, mode: 'index' }
|
|
277
|
+
}
|
|
278
|
+
});`;
|
|
279
|
+
})();
|
|
280
|
+
return THEME_AND_FILTER_SCRIPT + statusChart + '\n' + repoChart + '\n' + timelineChart;
|
|
281
|
+
}
|
|
@@ -9,6 +9,7 @@ import * as http from 'http';
|
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import * as path from 'path';
|
|
11
11
|
import { getStateManager, getGitHubToken } from '../core/index.js';
|
|
12
|
+
import { errorMessage } from '../core/errors.js';
|
|
12
13
|
import { fetchDashboardData, computePRsByRepo, computeTopRepos, getMonthlyData } from './dashboard-data.js';
|
|
13
14
|
import { buildDashboardStats } from './dashboard-templates.js';
|
|
14
15
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
@@ -203,7 +204,7 @@ export async function startDashboardServer(options) {
|
|
|
203
204
|
}
|
|
204
205
|
catch (error) {
|
|
205
206
|
console.error('Action failed:', body.action, body.url, error);
|
|
206
|
-
sendError(res, 500, `Action failed: ${
|
|
207
|
+
sendError(res, 500, `Action failed: ${errorMessage(error)}`);
|
|
207
208
|
return;
|
|
208
209
|
}
|
|
209
210
|
// Rebuild dashboard data from cached digest + updated state
|
|
@@ -229,7 +230,7 @@ export async function startDashboardServer(options) {
|
|
|
229
230
|
}
|
|
230
231
|
catch (error) {
|
|
231
232
|
console.error('Dashboard refresh failed:', error);
|
|
232
|
-
sendError(res, 500, `Refresh failed: ${
|
|
233
|
+
sendError(res, 500, `Refresh failed: ${errorMessage(error)}`);
|
|
233
234
|
}
|
|
234
235
|
}
|
|
235
236
|
// ── Static file serving ──────────────────────────────────────────────────
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard CSS styles: theme variables, layout, component styles.
|
|
3
|
+
* Extracted from dashboard-templates.ts for maintainability.
|
|
4
|
+
*/
|
|
5
|
+
export declare const DASHBOARD_CSS = "\n :root, [data-theme=\"dark\"] {\n --bg-base: #080b10;\n --bg-surface: rgba(22, 27, 34, 0.65);\n --bg-elevated: rgba(28, 33, 40, 0.8);\n --border: rgba(48, 54, 61, 0.6);\n --border-muted: rgba(33, 38, 45, 0.5);\n --text-primary: #e6edf3;\n --text-secondary: #8b949e;\n --text-muted: #6e7681;\n --accent-merged: #a855f7;\n --accent-merged-dim: rgba(168, 85, 247, 0.12);\n --accent-open: #3fb950;\n --accent-open-dim: rgba(63, 185, 80, 0.12);\n --accent-warning: #d29922;\n --accent-warning-dim: rgba(210, 153, 34, 0.12);\n --accent-error: #f85149;\n --accent-error-dim: rgba(248, 81, 73, 0.10);\n --accent-conflict: #da3633;\n --accent-info: #58a6ff;\n --accent-info-dim: rgba(88, 166, 255, 0.08);\n --chart-border: rgba(8, 11, 16, 0.8);\n --chart-grid: rgba(48, 54, 61, 0.3);\n --scrollbar-track: rgba(28, 33, 40, 0.8);\n --scrollbar-thumb: rgba(48, 54, 61, 0.6);\n }\n\n [data-theme=\"light\"] {\n --bg-base: #f6f8fa;\n --bg-surface: rgba(255, 255, 255, 0.85);\n --bg-elevated: rgba(246, 248, 250, 0.95);\n --border: rgba(208, 215, 222, 0.6);\n --border-muted: rgba(216, 222, 228, 0.5);\n --text-primary: #1f2328;\n --text-secondary: #656d76;\n --text-muted: #8b949e;\n --accent-merged: #8250df;\n --accent-merged-dim: rgba(130, 80, 223, 0.1);\n --accent-open: #1a7f37;\n --accent-open-dim: rgba(26, 127, 55, 0.1);\n --accent-warning: #9a6700;\n --accent-warning-dim: rgba(154, 103, 0, 0.1);\n --accent-error: #cf222e;\n --accent-error-dim: rgba(207, 34, 46, 0.08);\n --accent-conflict: #cf222e;\n --accent-info: #0969da;\n --accent-info-dim: rgba(9, 105, 218, 0.08);\n --chart-border: rgba(255, 255, 255, 0.8);\n --chart-grid: rgba(208, 215, 222, 0.4);\n --scrollbar-track: rgba(246, 248, 250, 0.95);\n --scrollbar-thumb: rgba(208, 215, 222, 0.6);\n }\n\n @media (prefers-color-scheme: light) {\n :root:not([data-theme=\"dark\"]) {\n --bg-base: #f6f8fa;\n --bg-surface: rgba(255, 255, 255, 0.85);\n --bg-elevated: rgba(246, 248, 250, 0.95);\n --border: rgba(208, 215, 222, 0.6);\n --border-muted: rgba(216, 222, 228, 0.5);\n --text-primary: #1f2328;\n --text-secondary: #656d76;\n --text-muted: #8b949e;\n --accent-merged: #8250df;\n --accent-merged-dim: rgba(130, 80, 223, 0.1);\n --accent-open: #1a7f37;\n --accent-open-dim: rgba(26, 127, 55, 0.1);\n --accent-warning: #9a6700;\n --accent-warning-dim: rgba(154, 103, 0, 0.1);\n --accent-error: #cf222e;\n --accent-error-dim: rgba(207, 34, 46, 0.08);\n --accent-conflict: #cf222e;\n --accent-info: #0969da;\n --accent-info-dim: rgba(9, 105, 218, 0.08);\n --chart-border: rgba(255, 255, 255, 0.8);\n --chart-grid: rgba(208, 215, 222, 0.4);\n --scrollbar-track: rgba(246, 248, 250, 0.95);\n --scrollbar-thumb: rgba(208, 215, 222, 0.6);\n }\n }\n\n * { margin: 0; padding: 0; box-sizing: border-box; }\n\n body {\n font-family: 'Geist', -apple-system, BlinkMacSystemFont, sans-serif;\n background: var(--bg-base);\n color: var(--text-primary);\n min-height: 100vh;\n line-height: 1.5;\n overflow-x: hidden;\n }\n\n body::before {\n content: '';\n position: fixed;\n top: -20%; left: -10%;\n width: 60%; height: 60%;\n background: radial-gradient(ellipse, rgba(88, 166, 255, 0.06) 0%, transparent 70%);\n pointer-events: none;\n z-index: 0;\n }\n\n body::after {\n content: '';\n position: fixed;\n bottom: -20%; right: -10%;\n width: 50%; height: 50%;\n background: radial-gradient(ellipse, rgba(168, 85, 247, 0.05) 0%, transparent 70%);\n pointer-events: none;\n z-index: 0;\n }\n\n [data-theme=\"light\"] body::before,\n [data-theme=\"light\"] body::after {\n display: none;\n }\n\n .container {\n max-width: 1400px;\n margin: 0 auto;\n padding: 2rem;\n position: relative;\n z-index: 1;\n }\n\n .header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 1.5rem;\n padding-bottom: 1rem;\n border-bottom: 1px solid var(--border-muted);\n }\n\n .header-left {\n display: flex;\n align-items: center;\n gap: 1rem;\n }\n\n .logo {\n width: 44px;\n height: 44px;\n background: linear-gradient(135deg, var(--accent-info) 0%, var(--accent-merged) 50%, #f778ba 100%);\n border-radius: 12px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 1.5rem;\n box-shadow: 0 0 24px rgba(168, 85, 247, 0.3), 0 0 48px rgba(88, 166, 255, 0.15);\n }\n\n .header h1 {\n font-size: 1.75rem;\n font-weight: 600;\n letter-spacing: -0.02em;\n background: linear-gradient(135deg, var(--text-primary) 0%, var(--text-secondary) 100%);\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n background-clip: text;\n }\n\n .header-subtitle {\n font-family: 'Geist Mono', monospace;\n font-size: 0.75rem;\n color: var(--text-muted);\n text-transform: uppercase;\n letter-spacing: 0.1em;\n }\n\n .timestamp {\n font-family: 'Geist Mono', monospace;\n font-size: 0.8rem;\n color: var(--text-muted);\n display: flex;\n align-items: center;\n gap: 0.5rem;\n }\n\n .timestamp::before {\n content: '';\n width: 8px;\n height: 8px;\n background: var(--accent-open);\n border-radius: 50%;\n animation: pulse 2s ease-in-out infinite;\n }\n\n @keyframes pulse {\n 0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(35, 134, 54, 0.4); }\n 50% { opacity: 0.8; box-shadow: 0 0 0 8px rgba(35, 134, 54, 0); }\n }\n\n .stats-grid {\n display: flex;\n background: var(--bg-surface);\n border: 1px solid var(--border-muted);\n border-radius: 12px;\n margin-bottom: 1.5rem;\n overflow: hidden;\n }\n\n @media (max-width: 768px) {\n .stats-grid { flex-wrap: wrap; }\n .stat-card { flex: 1 1 33%; }\n }\n\n .stat-card {\n flex: 1;\n padding: 1rem 1.25rem;\n position: relative;\n transition: background 0.2s ease;\n }\n\n .stat-card + .stat-card {\n border-left: 1px solid var(--border-muted);\n }\n\n .stat-card:hover {\n background: rgba(255, 255, 255, 0.02);\n }\n\n .stat-card::after {\n content: '';\n position: absolute;\n bottom: 0; left: 0.75rem; right: 0.75rem;\n height: 2px;\n background: var(--accent-color, var(--border));\n border-radius: 2px;\n opacity: 0.7;\n }\n\n .stat-card.active { --accent-color: var(--accent-open); }\n .stat-card.merged { --accent-color: var(--accent-merged); }\n .stat-card.closed { --accent-color: var(--text-muted); }\n .stat-card.rate { --accent-color: var(--accent-info); }\n\n .stat-value {\n font-family: 'Geist Mono', monospace;\n font-size: 1.75rem;\n font-weight: 600;\n line-height: 1;\n margin-bottom: 0.25rem;\n }\n\n .stat-card.active .stat-value { color: var(--accent-open); }\n .stat-card.merged .stat-value { color: var(--accent-merged); }\n .stat-card.closed .stat-value { color: var(--text-muted); }\n .stat-card.rate .stat-value { color: var(--accent-info); }\n\n .stat-label {\n font-size: 0.7rem;\n color: var(--text-secondary);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n }\n\n .health-section {\n background: var(--bg-surface);\n border: 1px solid var(--border-muted);\n border-radius: 10px;\n padding: 1.25rem;\n margin-bottom: 1.25rem;\n }\n\n .health-header {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n margin-bottom: 1rem;\n }\n\n .health-header h2 {\n font-size: 1rem;\n font-weight: 600;\n color: var(--text-primary);\n }\n\n .health-badge {\n font-family: 'Geist Mono', monospace;\n font-size: 0.7rem;\n padding: 0.25rem 0.5rem;\n border-radius: 4px;\n background: var(--accent-error-dim);\n color: var(--accent-error);\n }\n\n .health-items {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));\n gap: 0.75rem;\n }\n\n .health-item {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n padding: 0.75rem 1rem;\n background: var(--bg-elevated);\n border-radius: 8px;\n border-left: 3px solid;\n transition: transform 0.15s ease;\n }\n\n .health-item:hover { transform: translateX(4px); }\n\n .health-item.ci-failing {\n border-left-color: var(--accent-error);\n background: var(--accent-error-dim);\n }\n\n .health-item.conflict {\n border-left-color: var(--accent-conflict);\n background: rgba(218, 54, 51, 0.1);\n }\n\n .health-item.incomplete-checklist {\n border-left-color: var(--accent-info);\n }\n .health-item.needs-response,\n .health-item.needs-changes {\n border-left-color: var(--accent-warning);\n background: var(--accent-warning-dim);\n }\n\n .health-item.changes-addressed,\n .health-item.waiting-maintainer {\n border-left-color: var(--accent-info);\n background: var(--accent-info-dim);\n }\n\n .health-item.ci-not-running {\n border-left-color: var(--text-muted);\n background: rgba(110, 118, 129, 0.1);\n }\n\n .health-item.missing-files {\n border-left-color: var(--accent-warning);\n background: var(--accent-warning-dim);\n }\n\n .health-item.ci-blocked {\n border-left-color: var(--text-muted);\n background: rgba(110, 118, 129, 0.1);\n }\n\n .health-item.needs-rebase {\n border-left-color: var(--accent-warning);\n background: var(--accent-warning-dim);\n }\n\n .health-item.shelved {\n border-left-color: var(--text-muted);\n background: rgba(110, 118, 129, 0.06);\n opacity: 0.6;\n }\n\n .health-item.shelved .health-icon { background: rgba(110, 118, 129, 0.12); color: var(--text-muted); }\n\n .health-item.auto-unshelved {\n border-left-color: var(--accent-info);\n background: var(--accent-info-dim);\n }\n\n .health-item.auto-unshelved .health-icon { background: var(--accent-info-dim); color: var(--accent-info); }\n\n .stat-card.shelved { --accent-color: var(--text-muted); }\n .stat-card.shelved .stat-value { color: var(--text-muted); }\n\n .waiting-section {\n border-color: rgba(88, 166, 255, 0.2);\n }\n\n .health-icon {\n width: 32px;\n height: 32px;\n border-radius: 8px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 1rem;\n flex-shrink: 0;\n }\n\n .health-item.ci-failing .health-icon { background: var(--accent-error-dim); color: var(--accent-error); }\n .health-item.conflict .health-icon { background: rgba(218, 54, 51, 0.15); color: var(--accent-conflict); }\n .health-item.incomplete-checklist .health-icon { background: var(--accent-info-dim); color: var(--accent-info); }\n .health-item.needs-response .health-icon,\n .health-item.needs-changes .health-icon { background: var(--accent-warning-dim); color: var(--accent-warning); }\n .health-item.changes-addressed .health-icon,\n .health-item.waiting-maintainer .health-icon { background: var(--accent-info-dim); color: var(--accent-info); }\n .health-item.ci-not-running .health-icon { background: rgba(110, 118, 129, 0.15); color: var(--text-muted); }\n .health-item.missing-files .health-icon { background: var(--accent-warning-dim); color: var(--accent-warning); }\n .health-item.ci-blocked .health-icon { background: rgba(110, 118, 129, 0.15); color: var(--text-muted); }\n .health-item.needs-rebase .health-icon { background: var(--accent-warning-dim); color: var(--accent-warning); }\n\n .health-content { flex: 1; min-width: 0; }\n\n .health-title {\n font-size: 0.85rem;\n font-weight: 500;\n color: var(--text-primary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n .health-title a { color: inherit; text-decoration: none; }\n .health-title a:hover { color: var(--accent-info); }\n\n .health-meta {\n font-family: 'Geist Mono', monospace;\n font-size: 0.7rem;\n color: var(--text-muted);\n }\n\n .health-empty {\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 2rem;\n color: var(--text-muted);\n font-size: 0.9rem;\n }\n\n .health-empty::before {\n content: '\\2713';\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n background: var(--accent-open-dim);\n color: var(--accent-open);\n border-radius: 50%;\n margin-right: 0.75rem;\n font-weight: bold;\n }\n\n .main-grid {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 1.25rem;\n margin-bottom: 1.25rem;\n }\n\n @media (max-width: 1024px) { .main-grid { grid-template-columns: 1fr; } }\n\n .card {\n background: var(--bg-surface);\n border: 1px solid var(--border-muted);\n border-radius: 10px;\n overflow: hidden;\n }\n\n .card-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 0.75rem 1.125rem;\n border-bottom: 1px solid var(--border-muted);\n }\n\n .card-title {\n font-size: 0.75rem;\n font-weight: 600;\n color: var(--text-secondary);\n text-transform: uppercase;\n letter-spacing: 0.04em;\n }\n\n .card-body { padding: 1rem 1.125rem; }\n\n .chart-container {\n position: relative;\n height: 260px;\n }\n\n .pr-list-section {\n background: var(--bg-surface);\n border: 1px solid var(--border-muted);\n border-radius: 10px;\n overflow: hidden;\n }\n\n .pr-list-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 0.75rem 1.125rem;\n border-bottom: 1px solid var(--border-muted);\n }\n\n .pr-list-title {\n font-size: 0.75rem;\n font-weight: 600;\n color: var(--text-secondary);\n text-transform: uppercase;\n letter-spacing: 0.04em;\n }\n\n .pr-count {\n font-family: 'Geist Mono', monospace;\n font-size: 0.75rem;\n padding: 0.25rem 0.5rem;\n background: var(--accent-open-dim);\n color: var(--accent-open);\n border-radius: 4px;\n }\n\n .pr-list {\n max-height: 600px;\n overflow-y: auto;\n }\n\n .pr-list::-webkit-scrollbar { width: 6px; }\n .pr-list::-webkit-scrollbar-track { background: var(--scrollbar-track, var(--bg-elevated)); }\n .pr-list::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb, var(--border)); border-radius: 3px; }\n\n .pr-item {\n display: flex;\n align-items: flex-start;\n gap: 1rem;\n padding: 1rem 1.25rem;\n border-bottom: 1px solid var(--border-muted);\n transition: background 0.15s ease;\n }\n\n .pr-item:last-child { border-bottom: none; }\n .pr-item:hover { background: var(--bg-elevated); }\n\n .pr-status-indicator {\n width: 40px;\n height: 40px;\n border-radius: 10px;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n font-size: 1.1rem;\n background: var(--accent-open-dim);\n color: var(--accent-open);\n }\n\n .pr-item.has-issues .pr-status-indicator {\n background: var(--accent-error-dim);\n color: var(--accent-error);\n animation: attention-pulse 2s ease-in-out infinite;\n }\n\n .pr-item.stale .pr-status-indicator {\n background: var(--accent-warning-dim);\n color: var(--accent-warning);\n }\n\n @keyframes attention-pulse {\n 0%, 100% { box-shadow: 0 0 0 0 rgba(248, 81, 73, 0.4); }\n 50% { box-shadow: 0 0 0 6px rgba(248, 81, 73, 0); }\n }\n\n .pr-content { flex: 1; min-width: 0; }\n\n .pr-title-row {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n margin-bottom: 0.25rem;\n }\n\n .pr-title {\n font-size: 0.9rem;\n font-weight: 500;\n color: var(--text-primary);\n text-decoration: none;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n .pr-title:hover { color: var(--accent-info); }\n\n .pr-repo {\n font-family: 'Geist Mono', monospace;\n font-size: 0.75rem;\n color: var(--text-muted);\n flex-shrink: 0;\n }\n\n .pr-badges {\n display: flex;\n flex-wrap: wrap;\n gap: 0.5rem;\n }\n\n .badge {\n font-family: 'Geist Mono', monospace;\n font-size: 0.65rem;\n font-weight: 500;\n padding: 0.2rem 0.5rem;\n border-radius: 4px;\n text-transform: uppercase;\n letter-spacing: 0.03em;\n }\n\n .badge-ci-failing { background: var(--accent-error-dim); color: var(--accent-error); }\n .badge-conflict { background: rgba(218, 54, 51, 0.15); color: var(--accent-conflict); }\n .badge-needs-response { background: var(--accent-warning-dim); color: var(--accent-warning); }\n .badge-stale { background: var(--accent-warning-dim); color: var(--accent-warning); }\n .badge-passing { background: var(--accent-open-dim); color: var(--accent-open); }\n .badge-pending { background: var(--accent-info-dim); color: var(--accent-info); }\n .badge-days { background: var(--bg-elevated); color: var(--text-muted); }\n .badge-changes-requested { background: var(--accent-warning-dim); color: var(--accent-warning); }\n .badge-changes-addressed { background: var(--accent-info-dim); color: var(--accent-info); }\n\n .pr-activity {\n font-family: 'Geist Mono', monospace;\n font-size: 0.7rem;\n color: var(--text-muted);\n margin-left: auto;\n text-align: right;\n flex-shrink: 0;\n }\n\n .empty-state {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n padding: 3rem;\n color: var(--text-muted);\n }\n\n .empty-state-icon {\n font-size: 2.5rem;\n margin-bottom: 1rem;\n opacity: 0.5;\n }\n\n .footer {\n text-align: center;\n padding-top: 1.5rem;\n border-top: 1px solid var(--border-muted);\n margin-top: 1.5rem;\n }\n\n .footer p {\n font-family: 'Geist Mono', monospace;\n font-size: 0.7rem;\n color: var(--text-muted);\n }\n\n @keyframes fadeInUp {\n from { opacity: 0; transform: translateY(12px); }\n to { opacity: 1; transform: translateY(0); }\n }\n\n .stats-grid, .health-section, .pr-list-section {\n animation: fadeInUp 0.35s ease;\n }\n\n .theme-toggle {\n background: var(--bg-elevated);\n border: 1px solid var(--border-muted);\n border-radius: 8px;\n padding: 0.4rem 0.6rem;\n cursor: pointer;\n color: var(--text-secondary);\n display: flex;\n align-items: center;\n gap: 0.4rem;\n font-family: 'Geist Mono', monospace;\n font-size: 0.7rem;\n transition: background 0.2s ease, color 0.2s ease;\n }\n\n .theme-toggle:hover {\n background: var(--bg-surface);\n color: var(--text-primary);\n }\n\n .theme-toggle svg { flex-shrink: 0; }\n\n .header-controls {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n }\n\n .filter-toolbar {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n padding: 0.75rem 1rem;\n background: var(--bg-surface);\n border: 1px solid var(--border-muted);\n border-radius: 10px;\n margin-bottom: 1.25rem;\n flex-wrap: wrap;\n }\n\n .filter-toolbar label {\n font-family: 'Geist Mono', monospace;\n font-size: 0.7rem;\n color: var(--text-muted);\n text-transform: uppercase;\n letter-spacing: 0.04em;\n flex-shrink: 0;\n }\n\n .filter-search {\n flex: 1;\n min-width: 180px;\n padding: 0.4rem 0.75rem;\n background: var(--bg-elevated);\n border: 1px solid var(--border-muted);\n border-radius: 6px;\n color: var(--text-primary);\n font-family: 'Geist', sans-serif;\n font-size: 0.8rem;\n outline: none;\n transition: border-color 0.2s ease;\n }\n\n .filter-search:focus {\n border-color: var(--accent-info);\n }\n\n .filter-search::placeholder {\n color: var(--text-muted);\n }\n\n .filter-select {\n padding: 0.4rem 0.75rem;\n background: var(--bg-elevated);\n border: 1px solid var(--border-muted);\n border-radius: 6px;\n color: var(--text-primary);\n font-family: 'Geist', sans-serif;\n font-size: 0.8rem;\n outline: none;\n cursor: pointer;\n transition: border-color 0.2s ease;\n }\n\n .filter-select:focus {\n border-color: var(--accent-info);\n }\n\n .filter-count {\n font-family: 'Geist Mono', monospace;\n font-size: 0.7rem;\n color: var(--text-muted);\n margin-left: auto;\n flex-shrink: 0;\n }\n\n .pr-item[data-hidden=\"true\"],\n .health-item[data-hidden=\"true\"] {\n display: none;\n }\n";
|