@oss-autopilot/core 0.44.3 → 0.44.16
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-registry.js +61 -0
- package/dist/cli.bundle.cjs +99 -124
- package/dist/cli.bundle.cjs.map +4 -4
- package/dist/commands/daily.d.ts +6 -1
- package/dist/commands/daily.js +27 -5
- package/dist/commands/dashboard-data.d.ts +10 -2
- package/dist/commands/dashboard-data.js +35 -11
- package/dist/commands/dashboard-lifecycle.js +39 -2
- package/dist/commands/dashboard-scripts.d.ts +1 -1
- package/dist/commands/dashboard-scripts.js +4 -4
- package/dist/commands/dashboard-server.d.ts +2 -1
- package/dist/commands/dashboard-server.js +61 -53
- package/dist/commands/dashboard-templates.js +14 -68
- package/dist/commands/override.d.ts +21 -0
- package/dist/commands/override.js +35 -0
- package/dist/core/daily-logic.d.ts +13 -10
- package/dist/core/daily-logic.js +79 -166
- package/dist/core/display-utils.d.ts +4 -0
- package/dist/core/display-utils.js +53 -54
- package/dist/core/github-stats.d.ts +3 -3
- package/dist/core/github-stats.js +14 -7
- package/dist/core/issue-vetting.js +1 -1
- package/dist/core/pr-monitor.d.ts +26 -3
- package/dist/core/pr-monitor.js +103 -89
- package/dist/core/state.d.ts +22 -1
- package/dist/core/state.js +50 -1
- package/dist/core/test-utils.js +6 -16
- package/dist/core/types.d.ts +50 -38
- package/dist/core/types.js +7 -0
- package/dist/formatters/json.d.ts +1 -13
- package/dist/formatters/json.js +1 -13
- package/package.json +2 -2
package/dist/commands/daily.d.ts
CHANGED
|
@@ -6,9 +6,14 @@
|
|
|
6
6
|
* Domain logic lives in src/core/daily-logic.ts; this file is a thin
|
|
7
7
|
* orchestration layer that wires up the phases and handles I/O.
|
|
8
8
|
*/
|
|
9
|
-
import { type DailyDigest, type CommentedIssue, type PRCheckFailure, type RepoGroup } from '../core/index.js';
|
|
9
|
+
import { type DailyDigest, type CommentedIssue, type PRCheckFailure, type RepoGroup, type AgentState, type StarFilter } from '../core/index.js';
|
|
10
10
|
import { type DailyOutput, type CapacityAssessment, type ActionableIssue, type ActionMenu } from '../formatters/json.js';
|
|
11
11
|
export { computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, printDigest, CRITICAL_STATUSES, } from '../core/index.js';
|
|
12
|
+
/**
|
|
13
|
+
* Build a star filter from state for use in fetchUserPRCounts.
|
|
14
|
+
* Returns undefined if no star data is available (first run).
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildStarFilter(state: Readonly<AgentState>): StarFilter | undefined;
|
|
12
17
|
/**
|
|
13
18
|
* Internal result of the daily check, using full (non-deduplicated) types.
|
|
14
19
|
* Consumed by printDigest() (text mode) and converted to DailyOutput (JSON mode)
|
package/dist/commands/daily.js
CHANGED
|
@@ -16,6 +16,23 @@ const MODULE = 'daily';
|
|
|
16
16
|
// Re-export domain functions so existing consumers (tests, dashboard, startup)
|
|
17
17
|
// can continue importing from './daily.js' without changes.
|
|
18
18
|
export { computeRepoSignals, groupPRsByRepo, assessCapacity, collectActionableIssues, computeActionMenu, toShelvedPRRef, formatBriefSummary, formatSummary, printDigest, CRITICAL_STATUSES, } from '../core/index.js';
|
|
19
|
+
/**
|
|
20
|
+
* Build a star filter from state for use in fetchUserPRCounts.
|
|
21
|
+
* Returns undefined if no star data is available (first run).
|
|
22
|
+
*/
|
|
23
|
+
export function buildStarFilter(state) {
|
|
24
|
+
const minStars = state.config.minStars ?? 50;
|
|
25
|
+
const knownStarCounts = new Map();
|
|
26
|
+
for (const [repo, score] of Object.entries(state.repoScores)) {
|
|
27
|
+
if (score.stargazersCount !== undefined) {
|
|
28
|
+
knownStarCounts.set(repo, score.stargazersCount);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// Only filter if we have some star data to work with
|
|
32
|
+
if (knownStarCounts.size === 0)
|
|
33
|
+
return undefined;
|
|
34
|
+
return { minStars, knownStarCounts };
|
|
35
|
+
}
|
|
19
36
|
// ---------------------------------------------------------------------------
|
|
20
37
|
// Phase functions
|
|
21
38
|
// ---------------------------------------------------------------------------
|
|
@@ -31,17 +48,21 @@ async function fetchPRData(prMonitor, token) {
|
|
|
31
48
|
if (failures.length > 0) {
|
|
32
49
|
warn(MODULE, `${failures.length} PR fetch(es) failed`);
|
|
33
50
|
}
|
|
51
|
+
// Build star filter from cached repoScores so low-star repos are excluded
|
|
52
|
+
// from merged/closed histograms (#576). Repos with no cached star data pass through.
|
|
53
|
+
const state = getStateManager().getState();
|
|
54
|
+
const starFilter = buildStarFilter(state);
|
|
34
55
|
// Fetch merged PR counts, closed PR counts, recently closed PRs, recently merged PRs, and commented issues in parallel
|
|
35
56
|
// All stats fetches are non-critical (cosmetic/scoring), so isolate their failure
|
|
36
57
|
const issueMonitor = new IssueConversationMonitor(token);
|
|
37
58
|
const [mergedResult, closedResult, recentlyClosedPRs, recentlyMergedPRs, issueConversationResult] = await Promise.all([
|
|
38
|
-
prMonitor.fetchUserMergedPRCounts().catch((err) => {
|
|
59
|
+
prMonitor.fetchUserMergedPRCounts(starFilter).catch((err) => {
|
|
39
60
|
if (isRateLimitOrAuthError(err))
|
|
40
61
|
throw err;
|
|
41
62
|
warn(MODULE, `Failed to fetch merged PR counts: ${errorMessage(err)}`);
|
|
42
63
|
return emptyPRCountsResult();
|
|
43
64
|
}),
|
|
44
|
-
prMonitor.fetchUserClosedPRCounts().catch((err) => {
|
|
65
|
+
prMonitor.fetchUserClosedPRCounts(starFilter).catch((err) => {
|
|
45
66
|
if (isRateLimitOrAuthError(err))
|
|
46
67
|
throw err;
|
|
47
68
|
warn(MODULE, `Failed to fetch closed PR counts: ${errorMessage(err)}`);
|
|
@@ -177,7 +198,7 @@ async function updateRepoScores(prMonitor, prs, mergedCounts, closedCounts) {
|
|
|
177
198
|
}
|
|
178
199
|
catch (error) {
|
|
179
200
|
warn(MODULE, `Failed to fetch repo star counts: ${errorMessage(error)}`);
|
|
180
|
-
warn(MODULE, '
|
|
201
|
+
warn(MODULE, 'Repos without cached star data will be excluded from stats until star counts are fetched on the next successful run.');
|
|
181
202
|
starCounts = new Map();
|
|
182
203
|
}
|
|
183
204
|
let starUpdateFailures = 0;
|
|
@@ -243,8 +264,9 @@ function partitionPRs(prMonitor, prs, recentlyClosedPRs, recentlyMergedPRs) {
|
|
|
243
264
|
shelvedPRs.push(toShelvedPRRef(pr));
|
|
244
265
|
}
|
|
245
266
|
}
|
|
246
|
-
else if (pr.
|
|
247
|
-
// Dormant PRs are auto-shelved
|
|
267
|
+
else if (pr.stalenessTier === 'dormant' && !CRITICAL_STATUSES.has(pr.status)) {
|
|
268
|
+
// Dormant PRs are auto-shelved unless they need addressing
|
|
269
|
+
// (e.g. maintainer commented on a stale PR — it should resurface)
|
|
248
270
|
shelvedPRs.push(toShelvedPRRef(pr));
|
|
249
271
|
}
|
|
250
272
|
else {
|
|
@@ -3,7 +3,7 @@
|
|
|
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
|
-
import type
|
|
6
|
+
import { type DailyDigest, type AgentState, type CommentedIssue } from '../core/types.js';
|
|
7
7
|
export interface DashboardStats {
|
|
8
8
|
activePRs: number;
|
|
9
9
|
shelvedPRs: number;
|
|
@@ -12,10 +12,18 @@ export interface DashboardStats {
|
|
|
12
12
|
mergeRate: string;
|
|
13
13
|
}
|
|
14
14
|
export declare function buildDashboardStats(digest: DailyDigest, state: Readonly<AgentState>): DashboardStats;
|
|
15
|
+
/**
|
|
16
|
+
* Merge fresh API counts into existing stored counts.
|
|
17
|
+
* Months present in the fresh data are updated; months only in the existing data are preserved.
|
|
18
|
+
* This prevents historical data loss when the API returns incomplete results
|
|
19
|
+
* (e.g. due to pagination limits or transient failures).
|
|
20
|
+
*/
|
|
21
|
+
export declare function mergeMonthlyCounts(existing: Record<string, number>, fresh: Record<string, number>): Record<string, number>;
|
|
15
22
|
/**
|
|
16
23
|
* Persist monthly chart analytics (merged, closed, opened) to state.
|
|
17
24
|
* Each metric is isolated so partial failures don't produce inconsistent state.
|
|
18
|
-
*
|
|
25
|
+
* Fresh API results are merged into existing data so historical months are preserved.
|
|
26
|
+
* Skips updating when fresh data is empty to avoid wiping chart data on transient API failures.
|
|
19
27
|
*/
|
|
20
28
|
export declare function updateMonthlyAnalytics(prs: Array<{
|
|
21
29
|
createdAt?: string;
|
|
@@ -7,7 +7,8 @@ import { getStateManager, PRMonitor, IssueConversationMonitor } from '../core/in
|
|
|
7
7
|
import { errorMessage, isRateLimitOrAuthError } from '../core/errors.js';
|
|
8
8
|
import { warn } from '../core/logger.js';
|
|
9
9
|
import { emptyPRCountsResult } from '../core/github-stats.js';
|
|
10
|
-
import {
|
|
10
|
+
import { isBelowMinStars, } from '../core/types.js';
|
|
11
|
+
import { toShelvedPRRef, buildStarFilter } from './daily.js';
|
|
11
12
|
const MODULE = 'dashboard-data';
|
|
12
13
|
export function buildDashboardStats(digest, state) {
|
|
13
14
|
const summary = digest.summary || {
|
|
@@ -16,24 +17,40 @@ export function buildDashboardStats(digest, state) {
|
|
|
16
17
|
mergeRate: 0,
|
|
17
18
|
totalNeedingAttention: 0,
|
|
18
19
|
};
|
|
20
|
+
const minStars = state.config.minStars ?? 50;
|
|
19
21
|
return {
|
|
20
22
|
activePRs: summary.totalActivePRs,
|
|
21
23
|
shelvedPRs: (digest.shelvedPRs || []).length,
|
|
22
24
|
mergedPRs: summary.totalMergedAllTime,
|
|
23
|
-
closedPRs: Object.values(state.repoScores || {}).reduce((sum, s) => sum + (s.closedWithoutMergeCount || 0), 0),
|
|
25
|
+
closedPRs: Object.values(state.repoScores || {}).reduce((sum, s) => sum + (isBelowMinStars(s.stargazersCount, minStars) ? 0 : s.closedWithoutMergeCount || 0), 0),
|
|
24
26
|
mergeRate: `${(summary.mergeRate ?? 0).toFixed(1)}%`,
|
|
25
27
|
};
|
|
26
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Merge fresh API counts into existing stored counts.
|
|
31
|
+
* Months present in the fresh data are updated; months only in the existing data are preserved.
|
|
32
|
+
* This prevents historical data loss when the API returns incomplete results
|
|
33
|
+
* (e.g. due to pagination limits or transient failures).
|
|
34
|
+
*/
|
|
35
|
+
export function mergeMonthlyCounts(existing, fresh) {
|
|
36
|
+
const merged = { ...existing };
|
|
37
|
+
for (const [month, count] of Object.entries(fresh)) {
|
|
38
|
+
merged[month] = count;
|
|
39
|
+
}
|
|
40
|
+
return merged;
|
|
41
|
+
}
|
|
27
42
|
/**
|
|
28
43
|
* Persist monthly chart analytics (merged, closed, opened) to state.
|
|
29
44
|
* Each metric is isolated so partial failures don't produce inconsistent state.
|
|
30
|
-
*
|
|
45
|
+
* Fresh API results are merged into existing data so historical months are preserved.
|
|
46
|
+
* Skips updating when fresh data is empty to avoid wiping chart data on transient API failures.
|
|
31
47
|
*/
|
|
32
48
|
export function updateMonthlyAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerged, openedFromClosed) {
|
|
33
49
|
const stateManager = getStateManager();
|
|
50
|
+
const state = stateManager.getState();
|
|
34
51
|
try {
|
|
35
52
|
if (Object.keys(monthlyCounts).length > 0) {
|
|
36
|
-
stateManager.setMonthlyMergedCounts(monthlyCounts);
|
|
53
|
+
stateManager.setMonthlyMergedCounts(mergeMonthlyCounts(state.monthlyMergedCounts || {}, monthlyCounts));
|
|
37
54
|
}
|
|
38
55
|
}
|
|
39
56
|
catch (error) {
|
|
@@ -41,7 +58,7 @@ export function updateMonthlyAnalytics(prs, monthlyCounts, monthlyClosedCounts,
|
|
|
41
58
|
}
|
|
42
59
|
try {
|
|
43
60
|
if (Object.keys(monthlyClosedCounts).length > 0) {
|
|
44
|
-
stateManager.setMonthlyClosedCounts(monthlyClosedCounts);
|
|
61
|
+
stateManager.setMonthlyClosedCounts(mergeMonthlyCounts(state.monthlyClosedCounts || {}, monthlyClosedCounts));
|
|
45
62
|
}
|
|
46
63
|
}
|
|
47
64
|
catch (error) {
|
|
@@ -59,7 +76,7 @@ export function updateMonthlyAnalytics(prs, monthlyCounts, monthlyClosedCounts,
|
|
|
59
76
|
}
|
|
60
77
|
}
|
|
61
78
|
if (Object.keys(combinedOpenedCounts).length > 0) {
|
|
62
|
-
stateManager.setMonthlyOpenedCounts(combinedOpenedCounts);
|
|
79
|
+
stateManager.setMonthlyOpenedCounts(mergeMonthlyCounts(state.monthlyOpenedCounts || {}, combinedOpenedCounts));
|
|
63
80
|
}
|
|
64
81
|
}
|
|
65
82
|
catch (error) {
|
|
@@ -75,6 +92,8 @@ export async function fetchDashboardData(token) {
|
|
|
75
92
|
const stateManager = getStateManager();
|
|
76
93
|
const prMonitor = new PRMonitor(token);
|
|
77
94
|
const issueMonitor = new IssueConversationMonitor(token);
|
|
95
|
+
// Build star filter from cached repoScores (#576)
|
|
96
|
+
const starFilter = buildStarFilter(stateManager.getState());
|
|
78
97
|
const [{ prs, failures }, recentlyClosedPRs, recentlyMergedPRs, mergedResult, closedResult, fetchedIssues] = await Promise.all([
|
|
79
98
|
prMonitor.fetchUserOpenPRs(),
|
|
80
99
|
prMonitor.fetchRecentlyClosedPRs().catch((err) => {
|
|
@@ -89,19 +108,21 @@ export async function fetchDashboardData(token) {
|
|
|
89
108
|
warn(MODULE, `Failed to fetch recently merged PRs: ${errorMessage(err)}`);
|
|
90
109
|
return [];
|
|
91
110
|
}),
|
|
92
|
-
prMonitor.fetchUserMergedPRCounts().catch((err) => {
|
|
111
|
+
prMonitor.fetchUserMergedPRCounts(starFilter).catch((err) => {
|
|
93
112
|
if (isRateLimitOrAuthError(err))
|
|
94
113
|
throw err;
|
|
95
114
|
warn(MODULE, `Failed to fetch merged PR counts: ${errorMessage(err)}`);
|
|
96
115
|
return emptyPRCountsResult();
|
|
97
116
|
}),
|
|
98
|
-
prMonitor.fetchUserClosedPRCounts().catch((err) => {
|
|
117
|
+
prMonitor.fetchUserClosedPRCounts(starFilter).catch((err) => {
|
|
99
118
|
if (isRateLimitOrAuthError(err))
|
|
100
119
|
throw err;
|
|
101
120
|
warn(MODULE, `Failed to fetch closed PR counts: ${errorMessage(err)}`);
|
|
102
121
|
return emptyPRCountsResult();
|
|
103
122
|
}),
|
|
104
123
|
issueMonitor.fetchCommentedIssues().catch((error) => {
|
|
124
|
+
if (isRateLimitOrAuthError(error))
|
|
125
|
+
throw error;
|
|
105
126
|
const msg = errorMessage(error);
|
|
106
127
|
if (msg.includes('No GitHub username configured')) {
|
|
107
128
|
warn(MODULE, `Issue conversation tracking requires setup: ${msg}`);
|
|
@@ -128,9 +149,9 @@ export async function fetchDashboardData(token) {
|
|
|
128
149
|
updateMonthlyAnalytics(prs, monthlyCounts, monthlyClosedCounts, openedFromMerged, openedFromClosed);
|
|
129
150
|
const digest = prMonitor.generateDigest(prs, recentlyClosedPRs, recentlyMergedPRs);
|
|
130
151
|
// Apply shelve partitioning for display (auto-unshelve only runs in daily check)
|
|
131
|
-
// Dormant PRs are treated as shelved
|
|
152
|
+
// Dormant PRs are treated as shelved unless they need addressing
|
|
132
153
|
const shelvedUrls = new Set(stateManager.getState().config.shelvedPRUrls || []);
|
|
133
|
-
const freshShelved = prs.filter((pr) => shelvedUrls.has(pr.url) || pr.
|
|
154
|
+
const freshShelved = prs.filter((pr) => shelvedUrls.has(pr.url) || (pr.stalenessTier === 'dormant' && pr.status !== 'needs_addressing'));
|
|
134
155
|
digest.shelvedPRs = freshShelved.map(toShelvedPRRef);
|
|
135
156
|
digest.autoUnshelvedPRs = [];
|
|
136
157
|
digest.summary.totalActivePRs = prs.length - freshShelved.length;
|
|
@@ -156,8 +177,11 @@ export function computePRsByRepo(digest, state) {
|
|
|
156
177
|
prsByRepo[pr.repo] = { active: 0, merged: 0, closed: 0 };
|
|
157
178
|
prsByRepo[pr.repo].active++;
|
|
158
179
|
}
|
|
159
|
-
// Add merged/closed counts from repo scores (historical data)
|
|
180
|
+
// Add merged/closed counts from repo scores (historical data), filtering by minStars (#576)
|
|
181
|
+
const minStars = state.config.minStars ?? 50;
|
|
160
182
|
for (const [repo, score] of Object.entries(state.repoScores || {})) {
|
|
183
|
+
if (isBelowMinStars(score.stargazersCount, minStars))
|
|
184
|
+
continue;
|
|
161
185
|
if (!prsByRepo[repo])
|
|
162
186
|
prsByRepo[repo] = { active: 0, merged: 0, closed: 0 };
|
|
163
187
|
prsByRepo[repo].merged = score.mergedPRCount;
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* and detecting whether a server is already running.
|
|
5
5
|
*/
|
|
6
6
|
import { spawn } from 'child_process';
|
|
7
|
-
import { findRunningDashboardServer, isDashboardServerRunning, readDashboardServerInfo } from './dashboard-server.js';
|
|
7
|
+
import { findRunningDashboardServer, isDashboardServerRunning, readDashboardServerInfo, removeDashboardServerInfo, } from './dashboard-server.js';
|
|
8
8
|
import { resolveAssetsDir } from './dashboard.js';
|
|
9
|
+
import { getCLIVersion } from '../core/index.js';
|
|
9
10
|
const DEFAULT_PORT = 3000;
|
|
10
11
|
const POLL_INTERVAL_MS = 200;
|
|
11
12
|
const MAX_POLL_ATTEMPTS = 25; // 5 seconds total
|
|
@@ -30,7 +31,43 @@ export async function launchDashboardServer(options) {
|
|
|
30
31
|
// 2. Check if a server is already running
|
|
31
32
|
const existing = await findRunningDashboardServer();
|
|
32
33
|
if (existing) {
|
|
33
|
-
|
|
34
|
+
// If the running server is from a different CLI version, kill it and relaunch
|
|
35
|
+
// so the dashboard uses the current version's code (#548)
|
|
36
|
+
const info = readDashboardServerInfo();
|
|
37
|
+
const currentVersion = getCLIVersion();
|
|
38
|
+
if (!info) {
|
|
39
|
+
// PID file disappeared between health check and now (race condition).
|
|
40
|
+
// Fall through to launch a new server.
|
|
41
|
+
}
|
|
42
|
+
else if (info.version && currentVersion !== '0.0.0' && info.version !== currentVersion) {
|
|
43
|
+
console.error(`[STARTUP] Dashboard server version mismatch (running: ${info.version}, current: ${currentVersion}). Restarting...`);
|
|
44
|
+
let killed = false;
|
|
45
|
+
try {
|
|
46
|
+
process.kill(info.pid, 'SIGTERM');
|
|
47
|
+
killed = true;
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
const code = err.code;
|
|
51
|
+
if (code === 'ESRCH') {
|
|
52
|
+
killed = true; // Already exited
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
console.error(`[STARTUP] Could not kill outdated dashboard (PID ${info.pid}): ${err.message}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (killed) {
|
|
59
|
+
removeDashboardServerInfo();
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
// Could not kill old server (e.g. EPERM); return it rather than
|
|
63
|
+
// attempting a doomed spawn on the same port.
|
|
64
|
+
return { url: existing.url, port: existing.port, alreadyRunning: true };
|
|
65
|
+
}
|
|
66
|
+
// Fall through to launch a new server
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
return { url: existing.url, port: existing.port, alreadyRunning: true };
|
|
70
|
+
}
|
|
34
71
|
}
|
|
35
72
|
// 3. Launch as detached child process
|
|
36
73
|
const port = options?.port ?? DEFAULT_PORT;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Client-side JavaScript for the dashboard: theme toggle, filtering, Chart.js charts.
|
|
3
3
|
*/
|
|
4
|
-
import type
|
|
4
|
+
import { type DailyDigest, type AgentState } from '../core/types.js';
|
|
5
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;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Client-side JavaScript for the dashboard: theme toggle, filtering, Chart.js charts.
|
|
3
3
|
*/
|
|
4
|
+
import { isBelowMinStars } from '../core/types.js';
|
|
4
5
|
/** Static client-side JS: theme toggle + filter/search logic. */
|
|
5
6
|
const THEME_AND_FILTER_SCRIPT = `
|
|
6
7
|
// === Theme Toggle ===
|
|
@@ -141,10 +142,9 @@ export function generateDashboardScripts(stats, monthlyMerged, monthlyClosed, mo
|
|
|
141
142
|
if (exOrgs?.some((o) => o.toLowerCase() === repoLower.split('/')[0]))
|
|
142
143
|
return true;
|
|
143
144
|
const score = (state.repoScores || {})[repo];
|
|
144
|
-
// Fail-
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
if (score?.stargazersCount !== undefined && score.stargazersCount < starThreshold)
|
|
145
|
+
// Fail-closed: repos without cached star data are excluded from charts.
|
|
146
|
+
// Star data is populated by the daily check; repos appear once stars are fetched.
|
|
147
|
+
if (isBelowMinStars(score?.stargazersCount, starThreshold))
|
|
148
148
|
return true;
|
|
149
149
|
return false;
|
|
150
150
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Dashboard HTTP server.
|
|
3
3
|
* Serves the Preact SPA from packages/dashboard/dist/ and provides API endpoints
|
|
4
|
-
* for live data fetching and state mutations (shelve,
|
|
4
|
+
* for live data fetching and state mutations (shelve, unshelve, override, etc.).
|
|
5
5
|
*
|
|
6
6
|
* Uses Node's built-in http module — no Express/Fastify.
|
|
7
7
|
*/
|
|
@@ -15,6 +15,7 @@ export interface DashboardServerInfo {
|
|
|
15
15
|
pid: number;
|
|
16
16
|
port: number;
|
|
17
17
|
startedAt: string;
|
|
18
|
+
version?: string;
|
|
18
19
|
}
|
|
19
20
|
export declare function getDashboardPidPath(): string;
|
|
20
21
|
export declare function writeDashboardServerInfo(info: DashboardServerInfo): void;
|
|
@@ -1,28 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Dashboard HTTP server.
|
|
3
3
|
* Serves the Preact SPA from packages/dashboard/dist/ and provides API endpoints
|
|
4
|
-
* for live data fetching and state mutations (shelve,
|
|
4
|
+
* for live data fetching and state mutations (shelve, unshelve, override, etc.).
|
|
5
5
|
*
|
|
6
6
|
* Uses Node's built-in http module — no Express/Fastify.
|
|
7
7
|
*/
|
|
8
8
|
import * as http from 'http';
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import * as path from 'path';
|
|
11
|
-
import { getStateManager, getGitHubToken, getDataDir } from '../core/index.js';
|
|
11
|
+
import { getStateManager, getGitHubToken, getDataDir, getCLIVersion } from '../core/index.js';
|
|
12
12
|
import { errorMessage, ValidationError } from '../core/errors.js';
|
|
13
13
|
import { warn } from '../core/logger.js';
|
|
14
|
-
import { validateUrl, validateGitHubUrl,
|
|
14
|
+
import { validateUrl, validateGitHubUrl, PR_URL_PATTERN } from './validation.js';
|
|
15
15
|
import { fetchDashboardData, computePRsByRepo, computeTopRepos, getMonthlyData, buildDashboardStats, } from './dashboard-data.js';
|
|
16
16
|
import { openInBrowser } from './startup.js';
|
|
17
17
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
18
|
-
const VALID_ACTIONS = new Set([
|
|
19
|
-
'shelve',
|
|
20
|
-
'unshelve',
|
|
21
|
-
'snooze',
|
|
22
|
-
'unsnooze',
|
|
23
|
-
'dismiss',
|
|
24
|
-
'undismiss',
|
|
25
|
-
]);
|
|
18
|
+
const VALID_ACTIONS = new Set(['shelve', 'unshelve', 'override_status']);
|
|
26
19
|
const MODULE = 'dashboard-server';
|
|
27
20
|
const MAX_BODY_BYTES = 10_240;
|
|
28
21
|
const REQUEST_TIMEOUT_MS = 30_000;
|
|
@@ -35,6 +28,40 @@ const MIME_TYPES = {
|
|
|
35
28
|
'.png': 'image/png',
|
|
36
29
|
'.ico': 'image/x-icon',
|
|
37
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* Apply status overrides from state to the PR list.
|
|
33
|
+
* Overrides are auto-cleared if the PR has new activity since the override was set.
|
|
34
|
+
*/
|
|
35
|
+
function applyStatusOverrides(prs, state) {
|
|
36
|
+
const overrides = state.config.statusOverrides;
|
|
37
|
+
if (!overrides || Object.keys(overrides).length === 0)
|
|
38
|
+
return prs;
|
|
39
|
+
const stateManager = getStateManager();
|
|
40
|
+
// Snapshot keys before iteration — clearStatusOverride mutates the same object
|
|
41
|
+
const overrideUrls = new Set(Object.keys(overrides));
|
|
42
|
+
let didAutoClear = false;
|
|
43
|
+
const result = prs.map((pr) => {
|
|
44
|
+
const override = stateManager.getStatusOverride(pr.url, pr.updatedAt);
|
|
45
|
+
if (!override) {
|
|
46
|
+
if (overrideUrls.has(pr.url))
|
|
47
|
+
didAutoClear = true;
|
|
48
|
+
return pr;
|
|
49
|
+
}
|
|
50
|
+
if (override.status === pr.status)
|
|
51
|
+
return pr;
|
|
52
|
+
return { ...pr, status: override.status };
|
|
53
|
+
});
|
|
54
|
+
// Persist any auto-cleared overrides so they don't resurrect on restart
|
|
55
|
+
if (didAutoClear) {
|
|
56
|
+
try {
|
|
57
|
+
stateManager.save();
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
warn(MODULE, `Failed to persist auto-cleared overrides — they may reappear on restart: ${errorMessage(err)}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
38
65
|
// ── PID File Management ──────────────────────────────────────────────────────
|
|
39
66
|
export function getDashboardPidPath() {
|
|
40
67
|
return path.join(getDataDir(), 'dashboard-server.pid');
|
|
@@ -49,6 +76,8 @@ export function readDashboardServerInfo() {
|
|
|
49
76
|
if (typeof parsed !== 'object' ||
|
|
50
77
|
parsed === null ||
|
|
51
78
|
typeof parsed.pid !== 'number' ||
|
|
79
|
+
!Number.isInteger(parsed.pid) ||
|
|
80
|
+
parsed.pid <= 0 ||
|
|
52
81
|
typeof parsed.port !== 'number' ||
|
|
53
82
|
typeof parsed.startedAt !== 'string') {
|
|
54
83
|
warn(MODULE, 'PID file has invalid structure, ignoring');
|
|
@@ -133,9 +162,8 @@ function buildDashboardJson(digest, state, commentedIssues) {
|
|
|
133
162
|
monthlyMerged,
|
|
134
163
|
monthlyOpened,
|
|
135
164
|
monthlyClosed,
|
|
136
|
-
activePRs: digest.openPRs || [],
|
|
165
|
+
activePRs: applyStatusOverrides(digest.openPRs || [], state),
|
|
137
166
|
shelvedPRUrls: state.config.shelvedPRUrls || [],
|
|
138
|
-
dismissedUrls: Object.keys(state.config.dismissedIssues || {}),
|
|
139
167
|
recentlyMergedPRs: digest.recentlyMergedPRs || [],
|
|
140
168
|
recentlyClosedPRs: digest.recentlyClosedPRs || [],
|
|
141
169
|
autoUnshelvedPRs: digest.autoUnshelvedPRs || [],
|
|
@@ -295,14 +323,10 @@ export async function startDashboardServer(options) {
|
|
|
295
323
|
sendError(res, 400, 'Missing or invalid "url" field');
|
|
296
324
|
return;
|
|
297
325
|
}
|
|
298
|
-
// Validate URL format —
|
|
299
|
-
// Dismiss/undismiss accepts both PR and issue URLs; other actions are PR-only.
|
|
300
|
-
const isDismissAction = body.action === 'dismiss' || body.action === 'undismiss';
|
|
301
|
-
const urlPattern = isDismissAction ? ISSUE_OR_PR_URL_PATTERN : PR_URL_PATTERN;
|
|
302
|
-
const urlType = isDismissAction ? 'issue or PR' : 'PR';
|
|
326
|
+
// Validate URL format — all actions are PR-only now.
|
|
303
327
|
try {
|
|
304
328
|
validateUrl(body.url);
|
|
305
|
-
validateGitHubUrl(body.url,
|
|
329
|
+
validateGitHubUrl(body.url, PR_URL_PATTERN, 'PR');
|
|
306
330
|
}
|
|
307
331
|
catch (err) {
|
|
308
332
|
if (err instanceof ValidationError) {
|
|
@@ -314,51 +338,30 @@ export async function startDashboardServer(options) {
|
|
|
314
338
|
}
|
|
315
339
|
return;
|
|
316
340
|
}
|
|
317
|
-
// Validate
|
|
318
|
-
if (body.action === '
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
sendError(res, 400, 'Snooze days must be a positive finite number');
|
|
341
|
+
// Validate override_status-specific fields
|
|
342
|
+
if (body.action === 'override_status') {
|
|
343
|
+
if (!body.status || (body.status !== 'needs_addressing' && body.status !== 'waiting_on_maintainer')) {
|
|
344
|
+
sendError(res, 400, 'override_status requires a valid "status" field (needs_addressing or waiting_on_maintainer)');
|
|
322
345
|
return;
|
|
323
346
|
}
|
|
324
|
-
if (body.reason !== undefined) {
|
|
325
|
-
try {
|
|
326
|
-
validateMessage(String(body.reason));
|
|
327
|
-
}
|
|
328
|
-
catch (err) {
|
|
329
|
-
if (err instanceof ValidationError) {
|
|
330
|
-
sendError(res, 400, err.message);
|
|
331
|
-
}
|
|
332
|
-
else {
|
|
333
|
-
warn(MODULE, `Unexpected error during message validation: ${errorMessage(err)}`);
|
|
334
|
-
sendError(res, 400, 'Invalid reason');
|
|
335
|
-
}
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
347
|
}
|
|
340
348
|
try {
|
|
341
349
|
switch (body.action) {
|
|
342
350
|
case 'shelve':
|
|
343
351
|
stateManager.shelvePR(body.url);
|
|
344
|
-
stateManager.undismissIssue(body.url); // prevent dual state
|
|
345
352
|
break;
|
|
346
353
|
case 'unshelve':
|
|
347
354
|
stateManager.unshelvePR(body.url);
|
|
348
355
|
break;
|
|
349
|
-
case '
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
stateManager.dismissIssue(body.url, new Date().toISOString());
|
|
357
|
-
stateManager.unshelvePR(body.url); // prevent dual state
|
|
358
|
-
break;
|
|
359
|
-
case 'undismiss':
|
|
360
|
-
stateManager.undismissIssue(body.url);
|
|
356
|
+
case 'override_status': {
|
|
357
|
+
// body.status is validated above — the early return ensures it's defined here
|
|
358
|
+
const overrideStatus = body.status;
|
|
359
|
+
// Find the PR to get its current updatedAt for auto-clear tracking
|
|
360
|
+
const targetPR = (cachedDigest?.openPRs || []).find((pr) => pr.url === body.url);
|
|
361
|
+
const lastActivityAt = targetPR?.updatedAt || new Date().toISOString();
|
|
362
|
+
stateManager.setStatusOverride(body.url, overrideStatus, lastActivityAt);
|
|
361
363
|
break;
|
|
364
|
+
}
|
|
362
365
|
}
|
|
363
366
|
stateManager.save();
|
|
364
367
|
}
|
|
@@ -480,7 +483,12 @@ export async function startDashboardServer(options) {
|
|
|
480
483
|
}
|
|
481
484
|
}
|
|
482
485
|
// Write PID file so other processes can detect this running server
|
|
483
|
-
writeDashboardServerInfo({
|
|
486
|
+
writeDashboardServerInfo({
|
|
487
|
+
pid: process.pid,
|
|
488
|
+
port: actualPort,
|
|
489
|
+
startedAt: new Date().toISOString(),
|
|
490
|
+
version: getCLIVersion(),
|
|
491
|
+
});
|
|
484
492
|
const serverUrl = `http://localhost:${actualPort}`;
|
|
485
493
|
warn(MODULE, `Dashboard server running at ${serverUrl}`);
|
|
486
494
|
// ── Background refresh ─────────────────────────────────────────────────
|