@oss-autopilot/core 0.43.0 → 0.44.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/dist/cli.bundle.cjs +261 -17002
- package/dist/cli.js +0 -17
- package/dist/commands/config.js +2 -1
- package/dist/commands/daily.js +57 -36
- package/dist/commands/dashboard-data.d.ts +10 -7
- package/dist/commands/dashboard-data.js +36 -3
- package/dist/commands/dashboard-formatters.d.ts +2 -10
- package/dist/commands/dashboard-formatters.js +2 -16
- package/dist/commands/dashboard-lifecycle.d.ts +1 -1
- package/dist/commands/dashboard-lifecycle.js +1 -1
- package/dist/commands/dashboard-scripts.d.ts +1 -1
- package/dist/commands/dashboard-server.js +29 -48
- package/dist/commands/dashboard-templates.d.ts +3 -2
- package/dist/commands/dashboard-templates.js +3 -2
- package/dist/commands/dashboard.d.ts +5 -9
- package/dist/commands/dashboard.js +7 -111
- package/dist/commands/startup.d.ts +3 -2
- package/dist/commands/startup.js +33 -30
- package/dist/commands/vet.js +2 -1
- package/dist/core/concurrency.d.ts +2 -1
- package/dist/core/concurrency.js +12 -2
- package/dist/core/daily-logic.js +1 -1
- package/dist/core/pr-monitor.js +12 -12
- package/dist/core/utils.d.ts +4 -8
- package/dist/core/utils.js +7 -8
- package/dist/formatters/json.d.ts +3 -2
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -45,7 +45,6 @@ const LOCAL_ONLY_COMMANDS = [
|
|
|
45
45
|
'version',
|
|
46
46
|
'setup',
|
|
47
47
|
'checkSetup',
|
|
48
|
-
'dashboard',
|
|
49
48
|
'serve',
|
|
50
49
|
'parse-issue-list',
|
|
51
50
|
'check-integration',
|
|
@@ -537,20 +536,6 @@ dashboardCmd
|
|
|
537
536
|
handleCommandError(err);
|
|
538
537
|
}
|
|
539
538
|
});
|
|
540
|
-
// Keep bare `dashboard` (no subcommand) for backward compat — generates static HTML
|
|
541
|
-
dashboardCmd
|
|
542
|
-
.option('--open', 'Open in browser')
|
|
543
|
-
.option('--json', 'Output as JSON')
|
|
544
|
-
.option('--offline', 'Use cached data only (no GitHub API calls)')
|
|
545
|
-
.action(async (options) => {
|
|
546
|
-
try {
|
|
547
|
-
const { runDashboard } = await import('./commands/dashboard.js');
|
|
548
|
-
await runDashboard({ open: options.open, json: options.json, offline: options.offline });
|
|
549
|
-
}
|
|
550
|
-
catch (err) {
|
|
551
|
-
handleCommandError(err, options.json);
|
|
552
|
-
}
|
|
553
|
-
});
|
|
554
539
|
// Parse issue list command (#82)
|
|
555
540
|
program
|
|
556
541
|
.command('parse-issue-list <path>')
|
|
@@ -673,8 +658,6 @@ program
|
|
|
673
658
|
else {
|
|
674
659
|
console.log(`OSS Autopilot v${data.version}`);
|
|
675
660
|
console.log(data.daily?.briefSummary ?? '');
|
|
676
|
-
if (data.dashboardPath)
|
|
677
|
-
console.log(`Dashboard: ${data.dashboardPath}`);
|
|
678
661
|
}
|
|
679
662
|
}
|
|
680
663
|
}
|
package/dist/commands/config.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Shows or updates configuration
|
|
4
4
|
*/
|
|
5
5
|
import { getStateManager } from '../core/index.js';
|
|
6
|
+
import { validateGitHubUsername } from './validation.js';
|
|
6
7
|
export async function runConfig(options) {
|
|
7
8
|
const stateManager = getStateManager();
|
|
8
9
|
const currentConfig = stateManager.getState().config;
|
|
@@ -17,7 +18,7 @@ export async function runConfig(options) {
|
|
|
17
18
|
// Handle specific config keys
|
|
18
19
|
switch (options.key) {
|
|
19
20
|
case 'username':
|
|
20
|
-
stateManager.updateConfig({ githubUsername: value });
|
|
21
|
+
stateManager.updateConfig({ githubUsername: validateGitHubUsername(value) });
|
|
21
22
|
break;
|
|
22
23
|
case 'add-language':
|
|
23
24
|
if (!currentConfig.languages.includes(value)) {
|
package/dist/commands/daily.js
CHANGED
|
@@ -7,9 +7,22 @@
|
|
|
7
7
|
* orchestration layer that wires up the phases and handles I/O.
|
|
8
8
|
*/
|
|
9
9
|
import { getStateManager, PRMonitor, IssueConversationMonitor, requireGitHubToken, CRITICAL_STATUSES, computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, } from '../core/index.js';
|
|
10
|
-
import { errorMessage } from '../core/errors.js';
|
|
10
|
+
import { errorMessage, getHttpStatusCode } from '../core/errors.js';
|
|
11
|
+
import { warn } from '../core/logger.js';
|
|
11
12
|
import { emptyPRCountsResult } from '../core/github-stats.js';
|
|
12
13
|
import { deduplicateDigest, compactActionableIssues, compactRepoGroups, } from '../formatters/json.js';
|
|
14
|
+
const MODULE = 'daily';
|
|
15
|
+
/** Return true for errors that should propagate (not degrade gracefully). */
|
|
16
|
+
function isRateLimitOrAuthError(err) {
|
|
17
|
+
const status = getHttpStatusCode(err);
|
|
18
|
+
if (status === 401 || status === 429)
|
|
19
|
+
return true;
|
|
20
|
+
if (status === 403) {
|
|
21
|
+
const msg = errorMessage(err).toLowerCase();
|
|
22
|
+
return msg.includes('rate limit') || msg.includes('abuse detection');
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
13
26
|
// Re-export domain functions so existing consumers (tests, dashboard, startup)
|
|
14
27
|
// can continue importing from './daily.js' without changes.
|
|
15
28
|
export { computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, printDigest, CRITICAL_STATUSES, } from '../core/index.js';
|
|
@@ -26,35 +39,45 @@ async function fetchPRData(prMonitor, token) {
|
|
|
26
39
|
const { prs, failures } = await prMonitor.fetchUserOpenPRs();
|
|
27
40
|
// Log any failures (but continue with successful checks)
|
|
28
41
|
if (failures.length > 0) {
|
|
29
|
-
|
|
42
|
+
warn(MODULE, `${failures.length} PR fetch(es) failed`);
|
|
30
43
|
}
|
|
31
44
|
// Fetch merged PR counts, closed PR counts, recently closed PRs, recently merged PRs, and commented issues in parallel
|
|
32
45
|
// All stats fetches are non-critical (cosmetic/scoring), so isolate their failure
|
|
33
46
|
const issueMonitor = new IssueConversationMonitor(token);
|
|
34
47
|
const [mergedResult, closedResult, recentlyClosedPRs, recentlyMergedPRs, issueConversationResult] = await Promise.all([
|
|
35
48
|
prMonitor.fetchUserMergedPRCounts().catch((err) => {
|
|
36
|
-
|
|
49
|
+
if (isRateLimitOrAuthError(err))
|
|
50
|
+
throw err;
|
|
51
|
+
warn(MODULE, `Failed to fetch merged PR counts: ${errorMessage(err)}`);
|
|
37
52
|
return emptyPRCountsResult();
|
|
38
53
|
}),
|
|
39
54
|
prMonitor.fetchUserClosedPRCounts().catch((err) => {
|
|
40
|
-
|
|
55
|
+
if (isRateLimitOrAuthError(err))
|
|
56
|
+
throw err;
|
|
57
|
+
warn(MODULE, `Failed to fetch closed PR counts: ${errorMessage(err)}`);
|
|
41
58
|
return emptyPRCountsResult();
|
|
42
59
|
}),
|
|
43
60
|
prMonitor.fetchRecentlyClosedPRs().catch((err) => {
|
|
44
|
-
|
|
61
|
+
if (isRateLimitOrAuthError(err))
|
|
62
|
+
throw err;
|
|
63
|
+
warn(MODULE, `Failed to fetch recently closed PRs: ${errorMessage(err)}`);
|
|
45
64
|
return [];
|
|
46
65
|
}),
|
|
47
66
|
prMonitor.fetchRecentlyMergedPRs().catch((err) => {
|
|
48
|
-
|
|
67
|
+
if (isRateLimitOrAuthError(err))
|
|
68
|
+
throw err;
|
|
69
|
+
warn(MODULE, `Failed to fetch recently merged PRs: ${errorMessage(err)}`);
|
|
49
70
|
return [];
|
|
50
71
|
}),
|
|
51
72
|
issueMonitor.fetchCommentedIssues().catch((error) => {
|
|
73
|
+
if (isRateLimitOrAuthError(error))
|
|
74
|
+
throw error;
|
|
52
75
|
const msg = errorMessage(error);
|
|
53
76
|
if (msg.includes('No GitHub username configured')) {
|
|
54
|
-
|
|
77
|
+
warn(MODULE, `Issue conversation tracking requires setup: ${msg}`);
|
|
55
78
|
}
|
|
56
79
|
else {
|
|
57
|
-
|
|
80
|
+
warn(MODULE, `Issue conversation fetch failed: ${msg}`);
|
|
58
81
|
}
|
|
59
82
|
return {
|
|
60
83
|
issues: [],
|
|
@@ -64,7 +87,7 @@ async function fetchPRData(prMonitor, token) {
|
|
|
64
87
|
]);
|
|
65
88
|
const commentedIssues = issueConversationResult.issues;
|
|
66
89
|
if (issueConversationResult.failures.length > 0) {
|
|
67
|
-
|
|
90
|
+
warn(MODULE, `${issueConversationResult.failures.length} issue conversation check(s) failed`);
|
|
68
91
|
}
|
|
69
92
|
const { repos: mergedCounts, monthlyCounts, monthlyOpenedCounts: openedFromMerged } = mergedResult;
|
|
70
93
|
const { repos: closedCounts, monthlyCounts: monthlyClosedCounts, monthlyOpenedCounts: openedFromClosed, } = closedResult;
|
|
@@ -94,7 +117,7 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
|
|
|
94
117
|
// skip the reset to avoid wiping scores due to transient API failures.
|
|
95
118
|
const existingReposWithMerges = Object.values(stateManager.getState().repoScores).filter((s) => s.mergedPRCount > 0);
|
|
96
119
|
if (mergedCounts.size === 0 && existingReposWithMerges.length > 0) {
|
|
97
|
-
|
|
120
|
+
warn(MODULE, `Skipping stale repo reset: API returned 0 merged PR results but state has ${existingReposWithMerges.length} repo(s) with merges. Possible API issue.`);
|
|
98
121
|
}
|
|
99
122
|
else {
|
|
100
123
|
for (const score of Object.values(stateManager.getState().repoScores)) {
|
|
@@ -111,18 +134,18 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
|
|
|
111
134
|
}
|
|
112
135
|
catch (error) {
|
|
113
136
|
mergedCountFailures++;
|
|
114
|
-
|
|
137
|
+
warn(MODULE, `Failed to update merged count for ${repo}: ${errorMessage(error)}`);
|
|
115
138
|
}
|
|
116
139
|
}
|
|
117
140
|
if (mergedCountFailures === mergedCounts.size && mergedCounts.size > 0) {
|
|
118
|
-
|
|
141
|
+
warn(MODULE, `[ALL_MERGED_COUNT_UPDATES_FAILED] All ${mergedCounts.size} merged count update(s) failed.`);
|
|
119
142
|
}
|
|
120
143
|
// Populate closedWithoutMergeCount in repo scores.
|
|
121
144
|
// Diagnostic: warn if API returned empty but we have known closed PRs (possible transient API failure).
|
|
122
145
|
// Unlike merged counts above, there is no stale-reset loop for closed counts, so no skip is needed.
|
|
123
146
|
const existingReposWithClosed = Object.values(stateManager.getState().repoScores).filter((s) => (s.closedWithoutMergeCount || 0) > 0);
|
|
124
147
|
if (closedCounts.size === 0 && existingReposWithClosed.length > 0) {
|
|
125
|
-
|
|
148
|
+
warn(MODULE, `API returned 0 closed PR results but state has ${existingReposWithClosed.length} repo(s) with closed PRs. Possible transient API issue.`);
|
|
126
149
|
}
|
|
127
150
|
let closedCountFailures = 0;
|
|
128
151
|
for (const [repo, count] of closedCounts) {
|
|
@@ -131,11 +154,11 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
|
|
|
131
154
|
}
|
|
132
155
|
catch (error) {
|
|
133
156
|
closedCountFailures++;
|
|
134
|
-
|
|
157
|
+
warn(MODULE, `Failed to update closed count for ${repo}: ${errorMessage(error)}`);
|
|
135
158
|
}
|
|
136
159
|
}
|
|
137
160
|
if (closedCountFailures === closedCounts.size && closedCounts.size > 0) {
|
|
138
|
-
|
|
161
|
+
warn(MODULE, `[ALL_CLOSED_COUNT_UPDATES_FAILED] All ${closedCounts.size} closed count update(s) failed.`);
|
|
139
162
|
}
|
|
140
163
|
// Update repo signals from observed open PR data (responsiveness, active maintainers).
|
|
141
164
|
// Only repos with current open PRs get signal updates — repos with no open PRs
|
|
@@ -150,11 +173,11 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
|
|
|
150
173
|
}
|
|
151
174
|
catch (error) {
|
|
152
175
|
signalUpdateFailures++;
|
|
153
|
-
|
|
176
|
+
warn(MODULE, `Failed to update signals for ${repo}: ${errorMessage(error)}`);
|
|
154
177
|
}
|
|
155
178
|
}
|
|
156
179
|
if (signalUpdateFailures === repoSignals.size && repoSignals.size > 0) {
|
|
157
|
-
|
|
180
|
+
warn(MODULE, `[ALL_SIGNAL_UPDATES_FAILED] All ${repoSignals.size} signal update(s) failed. This may indicate corrupted state.`);
|
|
158
181
|
}
|
|
159
182
|
// Fetch star counts for all scored repos (used by dashboard minStars filter, #216)
|
|
160
183
|
const allRepos = Object.keys(stateManager.getState().repoScores);
|
|
@@ -163,8 +186,8 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
|
|
|
163
186
|
starCounts = await prMonitor.fetchRepoStarCounts(allRepos);
|
|
164
187
|
}
|
|
165
188
|
catch (error) {
|
|
166
|
-
|
|
167
|
-
|
|
189
|
+
warn(MODULE, `Failed to fetch repo star counts: ${errorMessage(error)}`);
|
|
190
|
+
warn(MODULE, 'Dashboard minStars filter will use cached star counts (or be skipped for repos without cached data).');
|
|
168
191
|
starCounts = new Map();
|
|
169
192
|
}
|
|
170
193
|
let starUpdateFailures = 0;
|
|
@@ -174,11 +197,11 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
|
|
|
174
197
|
}
|
|
175
198
|
catch (error) {
|
|
176
199
|
starUpdateFailures++;
|
|
177
|
-
|
|
200
|
+
warn(MODULE, `Failed to update star count for ${repo}: ${errorMessage(error)}`);
|
|
178
201
|
}
|
|
179
202
|
}
|
|
180
203
|
if (starUpdateFailures === starCounts.size && starCounts.size > 0) {
|
|
181
|
-
|
|
204
|
+
warn(MODULE, `[ALL_STAR_COUNT_UPDATES_FAILED] All ${starCounts.size} star count update(s) failed.`);
|
|
182
205
|
}
|
|
183
206
|
// Auto-sync trustedProjects from repos with merged PRs
|
|
184
207
|
let trustSyncFailures = 0;
|
|
@@ -188,11 +211,11 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
|
|
|
188
211
|
}
|
|
189
212
|
catch (error) {
|
|
190
213
|
trustSyncFailures++;
|
|
191
|
-
|
|
214
|
+
warn(MODULE, `Failed to sync trusted project ${repo}: ${errorMessage(error)}`);
|
|
192
215
|
}
|
|
193
216
|
}
|
|
194
217
|
if (trustSyncFailures === mergedCounts.size && mergedCounts.size > 0) {
|
|
195
|
-
|
|
218
|
+
warn(MODULE, `[ALL_TRUST_SYNCS_FAILED] All ${mergedCounts.size} trusted project sync(s) failed. This may indicate corrupted state.`);
|
|
196
219
|
}
|
|
197
220
|
}
|
|
198
221
|
/**
|
|
@@ -211,7 +234,7 @@ function updateAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerg
|
|
|
211
234
|
}
|
|
212
235
|
}
|
|
213
236
|
catch (error) {
|
|
214
|
-
|
|
237
|
+
warn(MODULE, `Failed to store monthly merged counts: ${errorMessage(error)}`);
|
|
215
238
|
}
|
|
216
239
|
try {
|
|
217
240
|
if (Object.keys(monthlyClosedCounts).length > 0) {
|
|
@@ -219,7 +242,7 @@ function updateAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerg
|
|
|
219
242
|
}
|
|
220
243
|
}
|
|
221
244
|
catch (error) {
|
|
222
|
-
|
|
245
|
+
warn(MODULE, `Failed to store monthly closed counts: ${errorMessage(error)}`);
|
|
223
246
|
}
|
|
224
247
|
try {
|
|
225
248
|
// Build combined monthly opened counts from merged + closed + currently-open PRs
|
|
@@ -239,7 +262,7 @@ function updateAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerg
|
|
|
239
262
|
}
|
|
240
263
|
}
|
|
241
264
|
catch (error) {
|
|
242
|
-
|
|
265
|
+
warn(MODULE, `Failed to compute/store monthly opened counts: ${errorMessage(error)}`);
|
|
243
266
|
}
|
|
244
267
|
}
|
|
245
268
|
/**
|
|
@@ -254,15 +277,13 @@ function partitionPRs(prMonitor, prs, recentlyClosedPRs, recentlyMergedPRs) {
|
|
|
254
277
|
try {
|
|
255
278
|
const expiredSnoozes = stateManager.expireSnoozes();
|
|
256
279
|
if (expiredSnoozes.length > 0) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
console.error(` - ${url}`);
|
|
260
|
-
}
|
|
280
|
+
const urls = expiredSnoozes.map((url) => ` - ${url}`).join('\n');
|
|
281
|
+
warn(MODULE, `${expiredSnoozes.length} snoozed PR(s) expired and will resurface:\n${urls}`);
|
|
261
282
|
stateManager.save();
|
|
262
283
|
}
|
|
263
284
|
}
|
|
264
285
|
catch (error) {
|
|
265
|
-
|
|
286
|
+
warn(MODULE, `Failed to expire/persist snoozes: ${errorMessage(error)}`);
|
|
266
287
|
}
|
|
267
288
|
// Partition PRs into active vs shelved, auto-unshelving when maintainers engage
|
|
268
289
|
const shelvedPRs = [];
|
|
@@ -322,12 +343,12 @@ function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, fa
|
|
|
322
343
|
if (isNaN(responseTime) || isNaN(dismissTime)) {
|
|
323
344
|
// Invalid timestamp — fail open (include issue to be safe) without
|
|
324
345
|
// permanently removing dismiss record (may be a transient data issue)
|
|
325
|
-
|
|
346
|
+
warn(MODULE, `Invalid timestamp in dismiss check for ${issue.url}, including issue`);
|
|
326
347
|
return true;
|
|
327
348
|
}
|
|
328
349
|
if (responseTime > dismissTime) {
|
|
329
350
|
// New activity after dismiss — auto-undismiss and resurface
|
|
330
|
-
|
|
351
|
+
warn(MODULE, `Auto-undismissing issue ${issue.url}: new response at ${issue.lastResponseAt} after dismiss at ${dismissedAt}`);
|
|
331
352
|
stateManager.undismissIssue(issue.url);
|
|
332
353
|
hasAutoUndismissed = true;
|
|
333
354
|
return true;
|
|
@@ -349,12 +370,12 @@ function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, fa
|
|
|
349
370
|
if (isNaN(activityTime) || isNaN(dismissTime)) {
|
|
350
371
|
// Invalid timestamp — fail open (include PR to be safe) without
|
|
351
372
|
// permanently removing dismiss record (may be a transient data issue)
|
|
352
|
-
|
|
373
|
+
warn(MODULE, `Invalid timestamp in PR dismiss check for ${pr.url}, including PR`);
|
|
353
374
|
return true;
|
|
354
375
|
}
|
|
355
376
|
if (activityTime > dismissTime) {
|
|
356
377
|
// New activity after dismiss — auto-undismiss and resurface
|
|
357
|
-
|
|
378
|
+
warn(MODULE, `Auto-undismissing PR ${pr.url}: new activity at ${pr.updatedAt} after dismiss at ${dismissedAt}`);
|
|
358
379
|
stateManager.undismissIssue(pr.url);
|
|
359
380
|
hasAutoUndismissed = true;
|
|
360
381
|
return true;
|
|
@@ -368,7 +389,7 @@ function generateDigestOutput(digest, activePRs, shelvedPRs, commentedIssues, fa
|
|
|
368
389
|
stateManager.save();
|
|
369
390
|
}
|
|
370
391
|
catch (error) {
|
|
371
|
-
|
|
392
|
+
warn(MODULE, `Failed to persist auto-undismissed state: ${errorMessage(error)}`);
|
|
372
393
|
}
|
|
373
394
|
}
|
|
374
395
|
const actionableIssues = collectActionableIssues(nonDismissedPRs, snoozedUrls);
|
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Dashboard data fetching and aggregation.
|
|
3
3
|
* Handles GitHub API calls, PR grouping, stats computation, and monthly chart data.
|
|
4
|
-
*
|
|
4
|
+
* Consumed by the dashboard HTTP server (dashboard-server.ts) for the SPA API.
|
|
5
5
|
*/
|
|
6
6
|
import type { DailyDigest, AgentState, CommentedIssue } from '../core/types.js';
|
|
7
|
+
export interface DashboardStats {
|
|
8
|
+
activePRs: number;
|
|
9
|
+
shelvedPRs: number;
|
|
10
|
+
mergedPRs: number;
|
|
11
|
+
closedPRs: number;
|
|
12
|
+
mergeRate: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function buildDashboardStats(digest: DailyDigest, state: Readonly<AgentState>): DashboardStats;
|
|
7
15
|
export interface DashboardFetchResult {
|
|
8
16
|
digest: DailyDigest;
|
|
9
17
|
commentedIssues: CommentedIssue[];
|
|
10
18
|
}
|
|
11
|
-
/**
|
|
12
|
-
* Fetch fresh dashboard data from GitHub.
|
|
13
|
-
* Returns the digest and commented issues, updating state as a side effect.
|
|
14
|
-
* Throws if the fetch fails entirely (caller should fall back to cached data).
|
|
15
|
-
*/
|
|
16
19
|
export declare function fetchDashboardData(token: string): Promise<DashboardFetchResult>;
|
|
17
20
|
/**
|
|
18
21
|
* Compute PRs grouped by repository from a digest and state.
|
|
19
|
-
* Used for chart data in
|
|
22
|
+
* Used for chart data in the dashboard API.
|
|
20
23
|
*/
|
|
21
24
|
export declare function computePRsByRepo(digest: DailyDigest, state: Readonly<AgentState>): Record<string, {
|
|
22
25
|
active: number;
|
|
@@ -1,17 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Dashboard data fetching and aggregation.
|
|
3
3
|
* Handles GitHub API calls, PR grouping, stats computation, and monthly chart data.
|
|
4
|
-
*
|
|
4
|
+
* Consumed by the dashboard HTTP server (dashboard-server.ts) for the SPA API.
|
|
5
5
|
*/
|
|
6
6
|
import { getStateManager, PRMonitor, IssueConversationMonitor } from '../core/index.js';
|
|
7
|
-
import { errorMessage } from '../core/errors.js';
|
|
7
|
+
import { errorMessage, getHttpStatusCode } from '../core/errors.js';
|
|
8
8
|
import { emptyPRCountsResult } from '../core/github-stats.js';
|
|
9
9
|
import { toShelvedPRRef } from './daily.js';
|
|
10
|
+
export function buildDashboardStats(digest, state) {
|
|
11
|
+
const summary = digest.summary || {
|
|
12
|
+
totalActivePRs: 0,
|
|
13
|
+
totalMergedAllTime: 0,
|
|
14
|
+
mergeRate: 0,
|
|
15
|
+
totalNeedingAttention: 0,
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
activePRs: summary.totalActivePRs,
|
|
19
|
+
shelvedPRs: (digest.shelvedPRs || []).length,
|
|
20
|
+
mergedPRs: summary.totalMergedAllTime,
|
|
21
|
+
closedPRs: Object.values(state.repoScores || {}).reduce((sum, s) => sum + (s.closedWithoutMergeCount || 0), 0),
|
|
22
|
+
mergeRate: `${(summary.mergeRate ?? 0).toFixed(1)}%`,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
10
25
|
/**
|
|
11
26
|
* Fetch fresh dashboard data from GitHub.
|
|
12
27
|
* Returns the digest and commented issues, updating state as a side effect.
|
|
13
28
|
* Throws if the fetch fails entirely (caller should fall back to cached data).
|
|
14
29
|
*/
|
|
30
|
+
function isRateLimitOrAuthError(err) {
|
|
31
|
+
const status = getHttpStatusCode(err);
|
|
32
|
+
if (status === 401 || status === 429)
|
|
33
|
+
return true;
|
|
34
|
+
if (status === 403) {
|
|
35
|
+
const msg = errorMessage(err).toLowerCase();
|
|
36
|
+
return msg.includes('rate limit') || msg.includes('abuse detection');
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
15
40
|
export async function fetchDashboardData(token) {
|
|
16
41
|
const stateManager = getStateManager();
|
|
17
42
|
const prMonitor = new PRMonitor(token);
|
|
@@ -19,18 +44,26 @@ export async function fetchDashboardData(token) {
|
|
|
19
44
|
const [{ prs, failures }, recentlyClosedPRs, recentlyMergedPRs, mergedResult, closedResult, fetchedIssues] = await Promise.all([
|
|
20
45
|
prMonitor.fetchUserOpenPRs(),
|
|
21
46
|
prMonitor.fetchRecentlyClosedPRs().catch((err) => {
|
|
47
|
+
if (isRateLimitOrAuthError(err))
|
|
48
|
+
throw err;
|
|
22
49
|
console.error(`Warning: Failed to fetch recently closed PRs: ${errorMessage(err)}`);
|
|
23
50
|
return [];
|
|
24
51
|
}),
|
|
25
52
|
prMonitor.fetchRecentlyMergedPRs().catch((err) => {
|
|
53
|
+
if (isRateLimitOrAuthError(err))
|
|
54
|
+
throw err;
|
|
26
55
|
console.error(`Warning: Failed to fetch recently merged PRs: ${errorMessage(err)}`);
|
|
27
56
|
return [];
|
|
28
57
|
}),
|
|
29
58
|
prMonitor.fetchUserMergedPRCounts().catch((err) => {
|
|
59
|
+
if (isRateLimitOrAuthError(err))
|
|
60
|
+
throw err;
|
|
30
61
|
console.error(`Warning: Failed to fetch merged PR counts: ${errorMessage(err)}`);
|
|
31
62
|
return emptyPRCountsResult();
|
|
32
63
|
}),
|
|
33
64
|
prMonitor.fetchUserClosedPRCounts().catch((err) => {
|
|
65
|
+
if (isRateLimitOrAuthError(err))
|
|
66
|
+
throw err;
|
|
34
67
|
console.error(`Warning: Failed to fetch closed PR counts: ${errorMessage(err)}`);
|
|
35
68
|
return emptyPRCountsResult();
|
|
36
69
|
}),
|
|
@@ -113,7 +146,7 @@ export async function fetchDashboardData(token) {
|
|
|
113
146
|
}
|
|
114
147
|
/**
|
|
115
148
|
* Compute PRs grouped by repository from a digest and state.
|
|
116
|
-
* Used for chart data in
|
|
149
|
+
* Used for chart data in the dashboard API.
|
|
117
150
|
*/
|
|
118
151
|
export function computePRsByRepo(digest, state) {
|
|
119
152
|
const prsByRepo = {};
|
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Dashboard
|
|
2
|
+
* Dashboard HTML formatting helpers.
|
|
3
|
+
* Used by dashboard-templates.ts and dashboard-components.ts for static HTML generation.
|
|
3
4
|
*/
|
|
4
|
-
import type { DailyDigest, AgentState } from '../core/types.js';
|
|
5
|
-
export interface DashboardStats {
|
|
6
|
-
activePRs: number;
|
|
7
|
-
shelvedPRs: number;
|
|
8
|
-
mergedPRs: number;
|
|
9
|
-
closedPRs: number;
|
|
10
|
-
mergeRate: string;
|
|
11
|
-
}
|
|
12
5
|
/**
|
|
13
6
|
* Escape HTML special characters to prevent XSS when interpolating
|
|
14
7
|
* user-controlled content (e.g. PR titles, comment bodies, author names) into HTML.
|
|
@@ -17,4 +10,3 @@ export interface DashboardStats {
|
|
|
17
10
|
* the URL scheme if the source is untrusted. GitHub API URLs are trusted.
|
|
18
11
|
*/
|
|
19
12
|
export declare function escapeHtml(text: string): string;
|
|
20
|
-
export declare function buildDashboardStats(digest: DailyDigest, state: Readonly<AgentState>): DashboardStats;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Dashboard
|
|
2
|
+
* Dashboard HTML formatting helpers.
|
|
3
|
+
* Used by dashboard-templates.ts and dashboard-components.ts for static HTML generation.
|
|
3
4
|
*/
|
|
4
5
|
/**
|
|
5
6
|
* Escape HTML special characters to prevent XSS when interpolating
|
|
@@ -16,18 +17,3 @@ export function escapeHtml(text) {
|
|
|
16
17
|
.replace(/"/g, '"')
|
|
17
18
|
.replace(/'/g, ''');
|
|
18
19
|
}
|
|
19
|
-
export function buildDashboardStats(digest, state) {
|
|
20
|
-
const summary = digest.summary || {
|
|
21
|
-
totalActivePRs: 0,
|
|
22
|
-
totalMergedAllTime: 0,
|
|
23
|
-
mergeRate: 0,
|
|
24
|
-
totalNeedingAttention: 0,
|
|
25
|
-
};
|
|
26
|
-
return {
|
|
27
|
-
activePRs: summary.totalActivePRs,
|
|
28
|
-
shelvedPRs: (digest.shelvedPRs || []).length,
|
|
29
|
-
mergedPRs: summary.totalMergedAllTime,
|
|
30
|
-
closedPRs: Object.values(state.repoScores || {}).reduce((sum, s) => sum + (s.closedWithoutMergeCount || 0), 0),
|
|
31
|
-
mergeRate: `${(summary.mergeRate ?? 0).toFixed(1)}%`,
|
|
32
|
-
};
|
|
33
|
-
}
|
|
@@ -12,7 +12,7 @@ export interface LaunchResult {
|
|
|
12
12
|
* Launch the interactive dashboard SPA server as a detached background process.
|
|
13
13
|
*
|
|
14
14
|
* Returns the server URL if launched successfully, or null if the SPA assets
|
|
15
|
-
* are not available (
|
|
15
|
+
* are not available (dashboard is skipped).
|
|
16
16
|
*
|
|
17
17
|
* If a server is already running (detected via PID file + health probe),
|
|
18
18
|
* returns its URL without launching a new one.
|
|
@@ -16,7 +16,7 @@ function sleep(ms) {
|
|
|
16
16
|
* Launch the interactive dashboard SPA server as a detached background process.
|
|
17
17
|
*
|
|
18
18
|
* Returns the server URL if launched successfully, or null if the SPA assets
|
|
19
|
-
* are not available (
|
|
19
|
+
* are not available (dashboard is skipped).
|
|
20
20
|
*
|
|
21
21
|
* If a server is already running (detected via PID file + health probe),
|
|
22
22
|
* returns its URL without launching a new one.
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
* Client-side JavaScript for the dashboard: theme toggle, filtering, Chart.js charts.
|
|
3
3
|
*/
|
|
4
4
|
import type { DailyDigest, AgentState } from '../core/types.js';
|
|
5
|
-
import type { DashboardStats } from './dashboard-
|
|
5
|
+
import type { DashboardStats } from './dashboard-data.js';
|
|
6
6
|
/** Generate the Chart.js JavaScript for the dashboard. */
|
|
7
7
|
export declare function generateDashboardScripts(stats: DashboardStats, monthlyMerged: Record<string, number>, monthlyClosed: Record<string, number>, monthlyOpened: Record<string, number>, digest: DailyDigest, state: Readonly<AgentState>): string;
|
|
@@ -11,8 +11,8 @@ import * as path from 'path';
|
|
|
11
11
|
import { getStateManager, getGitHubToken, getDataDir } from '../core/index.js';
|
|
12
12
|
import { errorMessage, ValidationError } from '../core/errors.js';
|
|
13
13
|
import { validateUrl, validateGitHubUrl, validateMessage, PR_URL_PATTERN } from './validation.js';
|
|
14
|
-
import { fetchDashboardData, computePRsByRepo, computeTopRepos, getMonthlyData } from './dashboard-data.js';
|
|
15
|
-
import {
|
|
14
|
+
import { fetchDashboardData, computePRsByRepo, computeTopRepos, getMonthlyData, buildDashboardStats, } from './dashboard-data.js';
|
|
15
|
+
import { openInBrowser } from './startup.js';
|
|
16
16
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
17
17
|
const VALID_ACTIONS = new Set(['shelve', 'unshelve', 'snooze', 'unsnooze']);
|
|
18
18
|
const MAX_BODY_BYTES = 10_240;
|
|
@@ -109,12 +109,11 @@ export async function findRunningDashboardServer() {
|
|
|
109
109
|
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
110
110
|
/**
|
|
111
111
|
* Build the JSON payload that the SPA expects from GET /api/data.
|
|
112
|
-
* Same shape as the existing `dashboard --json` output.
|
|
113
112
|
*/
|
|
114
113
|
function buildDashboardJson(digest, state, commentedIssues) {
|
|
115
114
|
const prsByRepo = computePRsByRepo(digest, state);
|
|
116
115
|
const topRepos = computeTopRepos(prsByRepo);
|
|
117
|
-
const { monthlyMerged } = getMonthlyData(state);
|
|
116
|
+
const { monthlyMerged, monthlyOpened, monthlyClosed } = getMonthlyData(state);
|
|
118
117
|
const stats = buildDashboardStats(digest, state);
|
|
119
118
|
const issueResponses = commentedIssues.filter((i) => i.status === 'new_response');
|
|
120
119
|
return {
|
|
@@ -122,8 +121,13 @@ function buildDashboardJson(digest, state, commentedIssues) {
|
|
|
122
121
|
prsByRepo,
|
|
123
122
|
topRepos: topRepos.map(([repo, data]) => ({ repo, ...data })),
|
|
124
123
|
monthlyMerged,
|
|
124
|
+
monthlyOpened,
|
|
125
|
+
monthlyClosed,
|
|
125
126
|
activePRs: digest.openPRs || [],
|
|
126
127
|
shelvedPRUrls: state.config.shelvedPRUrls || [],
|
|
128
|
+
recentlyMergedPRs: digest.recentlyMergedPRs || [],
|
|
129
|
+
recentlyClosedPRs: digest.recentlyClosedPRs || [],
|
|
130
|
+
autoUnshelvedPRs: digest.autoUnshelvedPRs || [],
|
|
127
131
|
commentedIssues,
|
|
128
132
|
issueResponses,
|
|
129
133
|
};
|
|
@@ -181,25 +185,11 @@ export async function startDashboardServer(options) {
|
|
|
181
185
|
const stateManager = getStateManager();
|
|
182
186
|
const resolvedAssetsDir = path.resolve(assetsDir);
|
|
183
187
|
// ── Cached data ──────────────────────────────────────────────────────────
|
|
184
|
-
|
|
188
|
+
// Start immediately with state.json data (written by the daily check that
|
|
189
|
+
// precedes this server launch). A background GitHub fetch refreshes the
|
|
190
|
+
// cache after the port is bound, so the startup poller sees us in time.
|
|
191
|
+
let cachedDigest = stateManager.getState().lastDigest;
|
|
185
192
|
let cachedCommentedIssues = [];
|
|
186
|
-
// Fetch initial data
|
|
187
|
-
if (token) {
|
|
188
|
-
try {
|
|
189
|
-
console.error('Fetching dashboard data from GitHub...');
|
|
190
|
-
const result = await fetchDashboardData(token);
|
|
191
|
-
cachedDigest = result.digest;
|
|
192
|
-
cachedCommentedIssues = result.commentedIssues;
|
|
193
|
-
}
|
|
194
|
-
catch (error) {
|
|
195
|
-
console.error('Failed to fetch data from GitHub:', error);
|
|
196
|
-
console.error('Falling back to cached data...');
|
|
197
|
-
cachedDigest = stateManager.getState().lastDigest;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
else {
|
|
201
|
-
cachedDigest = stateManager.getState().lastDigest;
|
|
202
|
-
}
|
|
203
193
|
if (!cachedDigest) {
|
|
204
194
|
console.error('No dashboard data available. Run the daily check first:');
|
|
205
195
|
console.error(' GITHUB_TOKEN=$(gh auth token) npm start -- daily');
|
|
@@ -328,9 +318,7 @@ export async function startDashboardServer(options) {
|
|
|
328
318
|
return;
|
|
329
319
|
}
|
|
330
320
|
// Rebuild dashboard data from cached digest + updated state
|
|
331
|
-
|
|
332
|
-
cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues);
|
|
333
|
-
}
|
|
321
|
+
cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues);
|
|
334
322
|
sendJson(res, 200, cachedJsonData);
|
|
335
323
|
}
|
|
336
324
|
// ── POST /api/refresh handler ────────────────────────────────────────────
|
|
@@ -444,31 +432,24 @@ export async function startDashboardServer(options) {
|
|
|
444
432
|
writeDashboardServerInfo({ pid: process.pid, port: actualPort, startedAt: new Date().toISOString() });
|
|
445
433
|
const serverUrl = `http://localhost:${actualPort}`;
|
|
446
434
|
console.error(`Dashboard server running at ${serverUrl}`);
|
|
435
|
+
// ── Background refresh ─────────────────────────────────────────────────
|
|
436
|
+
// Port is bound and PID file written — now fetch fresh data from GitHub
|
|
437
|
+
// so subsequent /api/data requests get live data instead of cached state.
|
|
438
|
+
if (token) {
|
|
439
|
+
fetchDashboardData(token)
|
|
440
|
+
.then((result) => {
|
|
441
|
+
cachedDigest = result.digest;
|
|
442
|
+
cachedCommentedIssues = result.commentedIssues;
|
|
443
|
+
cachedJsonData = buildDashboardJson(cachedDigest, stateManager.getState(), cachedCommentedIssues);
|
|
444
|
+
console.error('Background data refresh complete');
|
|
445
|
+
})
|
|
446
|
+
.catch((error) => {
|
|
447
|
+
console.error('Background data refresh failed (serving cached data):', errorMessage(error));
|
|
448
|
+
});
|
|
449
|
+
}
|
|
447
450
|
// ── Open browser ─────────────────────────────────────────────────────────
|
|
448
451
|
if (open) {
|
|
449
|
-
|
|
450
|
-
let openCmd;
|
|
451
|
-
let args;
|
|
452
|
-
switch (process.platform) {
|
|
453
|
-
case 'darwin':
|
|
454
|
-
openCmd = 'open';
|
|
455
|
-
args = [serverUrl];
|
|
456
|
-
break;
|
|
457
|
-
case 'win32':
|
|
458
|
-
openCmd = 'cmd';
|
|
459
|
-
args = ['/c', 'start', '', serverUrl];
|
|
460
|
-
break;
|
|
461
|
-
default:
|
|
462
|
-
openCmd = 'xdg-open';
|
|
463
|
-
args = [serverUrl];
|
|
464
|
-
break;
|
|
465
|
-
}
|
|
466
|
-
execFile(openCmd, args, (error) => {
|
|
467
|
-
if (error) {
|
|
468
|
-
console.error('Failed to open browser:', error.message);
|
|
469
|
-
console.error(`Open manually: ${serverUrl}`);
|
|
470
|
-
}
|
|
471
|
-
});
|
|
452
|
+
openInBrowser(serverUrl);
|
|
472
453
|
}
|
|
473
454
|
// ── Clean shutdown ───────────────────────────────────────────────────────
|
|
474
455
|
const shutdown = () => {
|