merge-steward 0.10.4 → 0.11.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/service-queue.d.ts +4 -0
- package/dist/service-queue.js +12 -8
- package/dist/service-runtime.d.ts +2 -1
- package/dist/service-runtime.js +4 -1
- package/dist/service.js +3 -1
- package/dist/watch/HelpBar.js +10 -4
- package/dist/watch/OverviewView.js +16 -6
- package/dist/watch/ProjectDetailView.js +35 -9
- package/dist/watch/StatusBar.js +23 -18
- package/dist/watch/dashboard-model.d.ts +1 -1
- package/dist/watch/dashboard-model.js +17 -10
- package/package.json +1 -1
package/dist/service-queue.d.ts
CHANGED
|
@@ -20,6 +20,10 @@ export declare class MergeStewardQueueCommands {
|
|
|
20
20
|
priority?: number;
|
|
21
21
|
}): QueueEntry | undefined;
|
|
22
22
|
scanStartupAdmissions(): Promise<void>;
|
|
23
|
+
scanEligibleOpenPrs(): Promise<{
|
|
24
|
+
scanned: number;
|
|
25
|
+
admitted: number;
|
|
26
|
+
}>;
|
|
23
27
|
tryAdmit(prNumber: number, branch: string, headSha: string): Promise<boolean>;
|
|
24
28
|
dequeueByPR(prNumber: number): void;
|
|
25
29
|
updateHeadByPR(prNumber: number, headSha: string): void;
|
package/dist/service-queue.js
CHANGED
|
@@ -68,20 +68,24 @@ export class MergeStewardQueueCommands {
|
|
|
68
68
|
async scanStartupAdmissions() {
|
|
69
69
|
this.logger.info({ repoId: this.config.repoId }, "Scanning startup admissions");
|
|
70
70
|
try {
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (await this.tryAdmit(pr.number, pr.branch, pr.headSha))
|
|
75
|
-
admitted += 1;
|
|
76
|
-
}
|
|
77
|
-
if (open.length > 0) {
|
|
78
|
-
this.logger.info({ scanned: open.length, admitted }, "Startup scan for eligible open PRs complete");
|
|
71
|
+
const { scanned, admitted } = await this.scanEligibleOpenPrs();
|
|
72
|
+
if (scanned > 0) {
|
|
73
|
+
this.logger.info({ scanned, admitted }, "Startup scan for eligible open PRs complete");
|
|
79
74
|
}
|
|
80
75
|
}
|
|
81
76
|
catch (error) {
|
|
82
77
|
this.logger.warn({ err: error }, "Startup scan for eligible open PRs failed");
|
|
83
78
|
}
|
|
84
79
|
}
|
|
80
|
+
async scanEligibleOpenPrs() {
|
|
81
|
+
const open = await this.github.listOpenPRs();
|
|
82
|
+
let admitted = 0;
|
|
83
|
+
for (const pr of open) {
|
|
84
|
+
if (await this.tryAdmit(pr.number, pr.branch, pr.headSha))
|
|
85
|
+
admitted += 1;
|
|
86
|
+
}
|
|
87
|
+
return { scanned: open.length, admitted };
|
|
88
|
+
}
|
|
85
89
|
async tryAdmit(prNumber, branch, headSha) {
|
|
86
90
|
if (this.config.excludeBranches.some((pattern) => matchGlob(pattern, branch))) {
|
|
87
91
|
this.logger.debug({ prNumber, branch }, "Branch excluded from admission");
|
|
@@ -14,6 +14,7 @@ export declare class MergeStewardRuntime {
|
|
|
14
14
|
private readonly eviction;
|
|
15
15
|
private readonly specBuilder;
|
|
16
16
|
private readonly logger;
|
|
17
|
+
private readonly beforeTick?;
|
|
17
18
|
private tickTimer;
|
|
18
19
|
private tickInProgress;
|
|
19
20
|
private lastTickStartedAt;
|
|
@@ -21,7 +22,7 @@ export declare class MergeStewardRuntime {
|
|
|
21
22
|
private lastTickOutcome;
|
|
22
23
|
private lastTickError;
|
|
23
24
|
private currentQueueBlock;
|
|
24
|
-
constructor(config: StewardConfig, policy: GitHubPolicyCache, store: QueueStore, git: GitOperations, ci: CIRunner, github: GitHubPRApi, eviction: EvictionReporter, specBuilder: SpeculativeBranchBuilder, logger: Logger);
|
|
25
|
+
constructor(config: StewardConfig, policy: GitHubPolicyCache, store: QueueStore, git: GitOperations, ci: CIRunner, github: GitHubPRApi, eviction: EvictionReporter, specBuilder: SpeculativeBranchBuilder, logger: Logger, beforeTick?: (() => Promise<void>) | undefined);
|
|
25
26
|
start(): Promise<void>;
|
|
26
27
|
stop(): Promise<void>;
|
|
27
28
|
triggerReconcile(): Promise<{
|
package/dist/service-runtime.js
CHANGED
|
@@ -9,6 +9,7 @@ export class MergeStewardRuntime {
|
|
|
9
9
|
eviction;
|
|
10
10
|
specBuilder;
|
|
11
11
|
logger;
|
|
12
|
+
beforeTick;
|
|
12
13
|
tickTimer;
|
|
13
14
|
tickInProgress = false;
|
|
14
15
|
lastTickStartedAt = null;
|
|
@@ -16,7 +17,7 @@ export class MergeStewardRuntime {
|
|
|
16
17
|
lastTickOutcome = "idle";
|
|
17
18
|
lastTickError = null;
|
|
18
19
|
currentQueueBlock = null;
|
|
19
|
-
constructor(config, policy, store, git, ci, github, eviction, specBuilder, logger) {
|
|
20
|
+
constructor(config, policy, store, git, ci, github, eviction, specBuilder, logger, beforeTick) {
|
|
20
21
|
this.config = config;
|
|
21
22
|
this.policy = policy;
|
|
22
23
|
this.store = store;
|
|
@@ -26,6 +27,7 @@ export class MergeStewardRuntime {
|
|
|
26
27
|
this.eviction = eviction;
|
|
27
28
|
this.specBuilder = specBuilder;
|
|
28
29
|
this.logger = logger;
|
|
30
|
+
this.beforeTick = beforeTick;
|
|
29
31
|
}
|
|
30
32
|
async start() {
|
|
31
33
|
this.logger.info({ pollIntervalMs: this.config.pollIntervalMs }, "Steward service starting");
|
|
@@ -79,6 +81,7 @@ export class MergeStewardRuntime {
|
|
|
79
81
|
this.lastTickOutcome = "running";
|
|
80
82
|
this.lastTickError = null;
|
|
81
83
|
try {
|
|
84
|
+
await this.beforeTick?.();
|
|
82
85
|
let tickQueueBlockEvent = null;
|
|
83
86
|
await reconcile({
|
|
84
87
|
store: this.store,
|
package/dist/service.js
CHANGED
|
@@ -28,8 +28,10 @@ export class MergeStewardService {
|
|
|
28
28
|
this.eviction = eviction;
|
|
29
29
|
this.specBuilder = specBuilder;
|
|
30
30
|
this.logger = logger;
|
|
31
|
-
this.runtime = new MergeStewardRuntime(config, policy, store, git, ci, github, eviction, specBuilder, logger);
|
|
32
31
|
this.queueCommands = new MergeStewardQueueCommands(config, policy, store, github, specBuilder, logger);
|
|
32
|
+
this.runtime = new MergeStewardRuntime(config, policy, store, git, ci, github, eviction, specBuilder, logger, async () => {
|
|
33
|
+
await this.queueCommands.scanEligibleOpenPrs();
|
|
34
|
+
});
|
|
33
35
|
this.watchQueries = new MergeStewardWatchQueries(config, store, this.runtime);
|
|
34
36
|
}
|
|
35
37
|
/** Expose the GitHub client for webhook handler branch→PR lookups. */
|
package/dist/watch/HelpBar.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text } from "ink";
|
|
2
|
+
import { Box, Text, useStdout } from "ink";
|
|
3
3
|
export function HelpBar({ view }) {
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
const { stdout } = useStdout();
|
|
5
|
+
const compact = (stdout?.columns ?? 80) < 80;
|
|
6
|
+
const text = compact
|
|
7
|
+
? (view === "overview"
|
|
8
|
+
? "j/k select Enter open r refresh q quit"
|
|
9
|
+
: "j/k select Esc overview a filter r refresh d dequeue q quit")
|
|
10
|
+
: view === "overview"
|
|
11
|
+
? "j/k: select project Enter: open project r: reconcile selected project q: quit"
|
|
12
|
+
: "j/k: select PR Esc: overview a: filter r: reconcile project d: dequeue PR q: quit";
|
|
7
13
|
return (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: text }) }));
|
|
8
14
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text, useStdout } from "ink";
|
|
3
3
|
import { ciStatusIcon, summarizeQueueBlock, truncate } from "./format.js";
|
|
4
|
-
import {
|
|
4
|
+
import { getClusterSummary, getRepoHealth, projectStatsSummary } from "./dashboard-model.js";
|
|
5
|
+
import { getChainEntries } from "./dashboard-model.js";
|
|
5
6
|
function clampWindowStart(selectedIndex, itemCount, maxItems) {
|
|
6
7
|
if (itemCount <= maxItems) {
|
|
7
8
|
return 0;
|
|
@@ -12,12 +13,19 @@ function clampWindowStart(selectedIndex, itemCount, maxItems) {
|
|
|
12
13
|
export function OverviewView({ repos, selectedRepoId, gatewayError }) {
|
|
13
14
|
const { stdout } = useStdout();
|
|
14
15
|
const rows = stdout?.rows ?? 24;
|
|
16
|
+
const width = stdout?.columns ?? 80;
|
|
17
|
+
const compact = width < 90;
|
|
15
18
|
const cluster = getClusterSummary(repos);
|
|
16
19
|
const selectedIndex = Math.max(0, repos.findIndex((repo) => repo.repoId === selectedRepoId));
|
|
17
20
|
const maxItems = Math.max(1, Math.floor((rows - 8) / 2));
|
|
18
21
|
const startIndex = clampWindowStart(selectedIndex, repos.length, maxItems);
|
|
19
22
|
const visibleRepos = repos.slice(startIndex, startIndex + maxItems);
|
|
20
|
-
|
|
23
|
+
const summaryText = compact
|
|
24
|
+
? `${cluster.total}p ${cluster.connected}c ${cluster.active}a ${cluster.blocked}b ${cluster.stuck}s`
|
|
25
|
+
: `${cluster.total} projects · ${cluster.connected} connected · ${cluster.active} active · ${cluster.blocked} blocked · ${cluster.stuck} stuck · ${cluster.attention} need attention`;
|
|
26
|
+
const idWidth = compact ? Math.max(14, Math.min(24, Math.floor(width * 0.44))) : 18;
|
|
27
|
+
const chainWidth = compact ? Math.max(12, width - 10) : 100;
|
|
28
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: "Queue Overview" }), _jsx(Text, { dimColor: true, children: summaryText }), gatewayError ? (_jsx(Text, { color: "red", children: `Gateway offline: ${truncate(gatewayError, 88)}. Run \`merge-steward service restart\`.` })) : null, repos.length === 0 ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["No repositories are attached yet. Run ", "`merge-steward repo attach <owner/repo>`", " first."] }) })) : (_jsx(Box, { marginTop: 1, flexDirection: "column", children: visibleRepos.map((repo) => {
|
|
21
29
|
const health = getRepoHealth(repo);
|
|
22
30
|
const queueBlockSummary = summarizeQueueBlock(repo.snapshot?.queueBlock);
|
|
23
31
|
const chainEntries = getChainEntries(repo.snapshot);
|
|
@@ -25,10 +33,12 @@ export function OverviewView({ repos, selectedRepoId, gatewayError }) {
|
|
|
25
33
|
? chainEntries.length > 0
|
|
26
34
|
? chainEntries.map((entry) => {
|
|
27
35
|
const ci = ciStatusIcon(entry);
|
|
28
|
-
return
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
return compact
|
|
37
|
+
? `#${entry.prNumber}${ci.icon}`
|
|
38
|
+
: `#${entry.prNumber} ${ci.icon}`;
|
|
39
|
+
}).join(" ")
|
|
40
|
+
: compact ? "empty" : "queue is empty"
|
|
31
41
|
: "queue data unavailable";
|
|
32
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: repo.repoId === selectedRepoId ? "cyan" : "gray", children: repo.repoId === selectedRepoId ? "\u25b8" : " " }), _jsx(Text, { bold: true, children: repo.repoId }), _jsx(Text, { dimColor: true, children: ` ${truncate(repo.repoFullName, 28)}` }), _jsx(Text, { children: ` ` }), _jsx(Text, { color: health.color, children: health.label }), _jsx(Text, { dimColor: true, children: ` ${projectStatsSummary(repo.snapshot)}` })] }), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, children: `Queue: ${truncate(chainText,
|
|
42
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: repo.repoId === selectedRepoId ? "cyan" : "gray", children: repo.repoId === selectedRepoId ? "\u25b8" : " " }), _jsx(Text, { bold: true, children: truncate(repo.repoId, idWidth) }), compact ? null : _jsx(Text, { dimColor: true, children: ` ${truncate(repo.repoFullName, 28)}` }), _jsx(Text, { children: ` ` }), _jsx(Text, { color: health.color, children: compact ? health.label.slice(0, 5) : health.label }), _jsx(Text, { dimColor: true, children: ` ${projectStatsSummary(repo.snapshot, compact)}` })] }), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, children: `Queue: ${truncate(chainText, chainWidth)}` }) }), compact ? null : (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { color: health.color, children: truncate(queueBlockSummary ?? health.detail, 110) }) }))] }, repo.repoId));
|
|
33
43
|
}) }))] }));
|
|
34
44
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text, useStdout } from "ink";
|
|
3
3
|
import { buildDisplayEntries } from "./display-filter.js";
|
|
4
4
|
import { describeEntry, getChainEntries, getRepoHealth, projectStatsSummary, runtimeSummary } from "./dashboard-model.js";
|
|
@@ -18,9 +18,31 @@ function selectedEntryIndex(snapshot, filter, entryId) {
|
|
|
18
18
|
const index = entries.findIndex((entry) => entry.id === entryId);
|
|
19
19
|
return index >= 0 ? index : 0;
|
|
20
20
|
}
|
|
21
|
+
function compactEntryStatus(entryStatus) {
|
|
22
|
+
switch (entryStatus) {
|
|
23
|
+
case "queued":
|
|
24
|
+
return "waiting";
|
|
25
|
+
case "preparing_head":
|
|
26
|
+
return "prep";
|
|
27
|
+
case "validating":
|
|
28
|
+
return "testing";
|
|
29
|
+
case "merging":
|
|
30
|
+
return "merging";
|
|
31
|
+
case "merged":
|
|
32
|
+
return "merged";
|
|
33
|
+
case "evicted":
|
|
34
|
+
return "repair";
|
|
35
|
+
case "dequeued":
|
|
36
|
+
return "removed";
|
|
37
|
+
default:
|
|
38
|
+
return entryStatus;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
21
41
|
export function ProjectDetailView({ repo, selectedEntryId, detail, filter, }) {
|
|
22
42
|
const { stdout } = useStdout();
|
|
23
43
|
const rows = stdout?.rows ?? 24;
|
|
44
|
+
const width = stdout?.columns ?? 80;
|
|
45
|
+
const compact = width < 95;
|
|
24
46
|
if (!repo) {
|
|
25
47
|
return (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "No project selected." }) }));
|
|
26
48
|
}
|
|
@@ -42,20 +64,24 @@ export function ProjectDetailView({ repo, selectedEntryId, detail, filter, }) {
|
|
|
42
64
|
const selectedEntry = entries.find((entry) => entry.id === selectedEntryId)
|
|
43
65
|
?? snapshot.entries.find((entry) => entry.id === selectedEntryId)
|
|
44
66
|
?? null;
|
|
67
|
+
const eventRows = compact ? 4 : 6;
|
|
45
68
|
const visibleEvents = detail
|
|
46
|
-
? detail.events.slice(-
|
|
47
|
-
: snapshot.recentEvents.slice(-
|
|
69
|
+
? detail.events.slice(-eventRows)
|
|
70
|
+
: snapshot.recentEvents.slice(-eventRows);
|
|
48
71
|
const chainEntries = getChainEntries(snapshot);
|
|
49
72
|
const chainText = chainEntries.length > 0
|
|
50
73
|
? chainEntries.map((entry) => `#${entry.prNumber}`).join(" -> ")
|
|
51
74
|
: "empty";
|
|
52
|
-
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: repo.repoId }), _jsx(Text, {
|
|
53
|
-
|
|
54
|
-
|
|
75
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: repo.repoId }), _jsx(Text, { children: ` ` }), _jsx(Text, { color: health.color, children: compact ? health.label.slice(0, 5) : health.label })] }), _jsx(Text, { dimColor: true, children: ` ${projectStatsSummary(snapshot, compact)}` }), compact ? null : _jsx(Text, { dimColor: true, children: runtimeSummary(snapshot) }), _jsx(Text, { dimColor: true, children: `Queue: main -> ${truncate(chainText, compact ? width - 18 : 96)}` }), compact ? null : queueBlockSummary ? _jsx(Text, { color: "yellow", children: queueBlockSummary }) : null, compact ? null : (_jsxs(_Fragment, { children: [_jsxs(Text, { dimColor: true, children: ["GitHub required checks: ", snapshot.githubPolicy.requiredChecks.length > 0 ? snapshot.githubPolicy.requiredChecks.join(", ") : "(none)"] }), snapshot.githubPolicy.fetchedAt ? (_jsxs(Text, { dimColor: true, children: ["Policy fetched ", relativeTime(snapshot.githubPolicy.fetchedAt), " ago", snapshot.githubPolicy.lastRefreshReason
|
|
76
|
+
? ` via ${snapshot.githubPolicy.lastRefreshReason}${snapshot.githubPolicy.lastRefreshChanged === null ? "" : snapshot.githubPolicy.lastRefreshChanged ? " (changed)" : " (unchanged)"}`
|
|
77
|
+
: ""] })) : null] })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Pull Requests" }), visibleEntries.length === 0 ? (_jsx(Text, { dimColor: true, children: "No queue entries in this filter." })) : (visibleEntries.map((entry) => {
|
|
55
78
|
const ci = ciStatusIcon(entry);
|
|
56
79
|
const isHead = snapshot.summary.headEntryId === entry.id;
|
|
57
|
-
|
|
58
|
-
|
|
80
|
+
const isSelected = entry.id === selectedEntryId;
|
|
81
|
+
const statusColor = entry.status === "evicted" ? "red" : ci.color;
|
|
82
|
+
const statusText = compact ? compactEntryStatus(entry.status) : humanStatus(entry.status, entry);
|
|
83
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: isSelected ? "cyan" : "gray", children: isSelected ? "\u25b8" : " " }), _jsx(Text, { bold: true, children: `#${entry.prNumber}` }), entry.issueKey ? _jsx(Text, { children: ` ${entry.issueKey}` }) : null, _jsx(Text, { children: ` ` }), _jsx(Text, { color: ci.color, children: ci.icon }), _jsx(Text, { color: statusColor, children: ` ${statusText}` }), compact ? null : (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: ` updated ${relativeTime(entry.updatedAt)} ago` }), isHead ? _jsx(Text, { dimColor: true, children: ` · ${humanStatus(entry.status, entry)} head` }) : null] }))] }), (isSelected || entry.status === "evicted" || (isHead && queueBlockSummary)) && (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, children: truncate(describeEntry(entry, { isHead, queueBlockSummary }), compact ? width - 4 : 110) }) }))] }, entry.id));
|
|
84
|
+
}))] }), selectedEntry ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: `Selected PR #${selectedEntry.prNumber}` }), compact ? (_jsx(Text, { dimColor: true, children: `head ${shortSha(selectedEntry.headSha)} · base ${shortSha(selectedEntry.baseSha)}` })) : (_jsxs(Text, { dimColor: true, children: [selectedEntry.branch, " \u00B7 head ", shortSha(selectedEntry.headSha), " \u00B7 base ", shortSha(selectedEntry.baseSha)] })), compact ? null : _jsx(Text, { dimColor: true, children: `Test branch: ${specChainLabel(selectedEntry, snapshot.entries)}` }), detail?.incidents.length
|
|
59
85
|
? detail.incidents.slice(-2).map((incident) => (_jsxs(Text, { color: "red", children: [relativeTime(incident.at), " ago \u00B7 ", incident.failureClass, " \u00B7 ", incident.outcome] }, incident.id)))
|
|
60
|
-
: null] })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: detail ? "Selected PR Activity" : "Recent Project Activity" }), visibleEvents.length === 0 ? (_jsx(Text, { dimColor: true, children: "No queue events yet." })) : (visibleEvents.map((event) => (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: relativeTime(event.at).padStart(4, " ") }), _jsx(Text, { children: truncate(formatEventNarrative(event), 106) })] }, "id" in event && event.id ? String(event.id) : `${event.entryId}-${event.at}`))))] })] }));
|
|
86
|
+
: null] })) : null, _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: detail ? "Selected PR Activity" : "Recent Project Activity" }), visibleEvents.length === 0 ? (_jsx(Text, { dimColor: true, children: "No queue events yet." })) : (visibleEvents.map((event) => (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: relativeTime(event.at).padStart(4, " ") }), _jsx(Text, { children: truncate(formatEventNarrative(event), compact ? Math.max(12, width - 8) : 106) })] }, "id" in event && event.id ? String(event.id) : `${event.entryId}-${event.at}`))))] })] }));
|
|
61
87
|
}
|
package/dist/watch/StatusBar.js
CHANGED
|
@@ -6,27 +6,32 @@ import { FreshnessBadge } from "./FreshnessBadge.js";
|
|
|
6
6
|
export function StatusBar({ repos, currentRepo, view, filter, lastSnapshotReceivedAt, expectedFreshMs, gatewayError, }) {
|
|
7
7
|
const { stdout } = useStdout();
|
|
8
8
|
const width = stdout?.columns ?? 80;
|
|
9
|
+
const compact = width < 88;
|
|
9
10
|
const cluster = getClusterSummary(repos);
|
|
10
11
|
const connected = cluster.connected > 0;
|
|
11
12
|
const repoHealth = currentRepo ? getRepoHealth(currentRepo) : null;
|
|
12
13
|
const leftParts = view === "project" && currentRepo
|
|
13
|
-
?
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
`${cluster.connected}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
14
|
+
? compact && currentRepo.repoId
|
|
15
|
+
? `ms ${currentRepo.repoId} ${repoHealth?.label ?? ""} a:${currentRepo.snapshot?.summary.active ?? 0} h:${currentRepo.snapshot?.summary.headPrNumber ?? "?"}`
|
|
16
|
+
: [
|
|
17
|
+
"merge-steward dashboard",
|
|
18
|
+
currentRepo.repoId,
|
|
19
|
+
repoHealth?.label ?? null,
|
|
20
|
+
currentRepo.snapshot ? `${currentRepo.snapshot.summary.active} active` : null,
|
|
21
|
+
currentRepo.snapshot?.summary.headPrNumber ? `head #${currentRepo.snapshot.summary.headPrNumber}` : "no head PR",
|
|
22
|
+
`filter:${filter}`,
|
|
23
|
+
].filter(Boolean).join(" | ")
|
|
24
|
+
: compact
|
|
25
|
+
? `${cluster.total}p ${cluster.connected}c ${cluster.active}a ${cluster.blocked}b`
|
|
26
|
+
: [
|
|
27
|
+
"merge-steward dashboard",
|
|
28
|
+
`${cluster.total} projects`,
|
|
29
|
+
`${cluster.connected} connected`,
|
|
30
|
+
`${cluster.active} active`,
|
|
31
|
+
`${cluster.blocked} blocked`,
|
|
32
|
+
`${cluster.stuck} stuck`,
|
|
33
|
+
`${cluster.attention} need attention`,
|
|
34
|
+
].join(" | ");
|
|
35
|
+
const availableLeft = compact ? Math.max(1, width - 14) : Math.max(1, width - 28);
|
|
31
36
|
return (_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { bold: true, children: truncate(leftParts, availableLeft) }), _jsx(FreshnessBadge, { connected: connected, lastSnapshotReceivedAt: lastSnapshotReceivedAt, expectedFreshMs: expectedFreshMs, gatewayError: gatewayError })] }));
|
|
32
37
|
}
|
|
@@ -34,7 +34,7 @@ export declare function getActiveEntries(snapshot: QueueWatchSnapshot | null): Q
|
|
|
34
34
|
export declare function getDefaultEntryId(snapshot: QueueWatchSnapshot | null, filter: "active" | "all", preferredPrNumber?: number | null): string | null;
|
|
35
35
|
export declare function getRepoHealth(repo: DashboardRepoState, now?: number): RepoHealthSummary;
|
|
36
36
|
export declare function getClusterSummary(repos: DashboardRepoState[], now?: number): ClusterSummary;
|
|
37
|
-
export declare function projectStatsSummary(snapshot: QueueWatchSnapshot | null): string;
|
|
37
|
+
export declare function projectStatsSummary(snapshot: QueueWatchSnapshot | null, compact?: boolean): string;
|
|
38
38
|
export declare function runtimeSummary(snapshot: QueueWatchSnapshot | null): string;
|
|
39
39
|
export declare function describeEntry(entry: QueueEntry, options: {
|
|
40
40
|
isHead: boolean;
|
|
@@ -116,7 +116,7 @@ export function getDefaultEntryId(snapshot, filter, preferredPrNumber) {
|
|
|
116
116
|
return match.id;
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
|
-
return snapshot.summary.headEntryId ?? visibleEntries[0]?.id ??
|
|
119
|
+
return snapshot.summary.headEntryId ?? visibleEntries[0]?.id ?? null;
|
|
120
120
|
}
|
|
121
121
|
export function getRepoHealth(repo, now = Date.now()) {
|
|
122
122
|
const snapshot = repo.snapshot;
|
|
@@ -232,7 +232,7 @@ export function getClusterSummary(repos, now = Date.now()) {
|
|
|
232
232
|
return summary;
|
|
233
233
|
}, initial);
|
|
234
234
|
}
|
|
235
|
-
export function projectStatsSummary(snapshot) {
|
|
235
|
+
export function projectStatsSummary(snapshot, compact = false) {
|
|
236
236
|
if (!snapshot) {
|
|
237
237
|
return "No queue data yet.";
|
|
238
238
|
}
|
|
@@ -245,17 +245,24 @@ export function projectStatsSummary(snapshot) {
|
|
|
245
245
|
const validating = latestEntries.filter((entry) => entry.status === "validating" || entry.status === "preparing_head").length;
|
|
246
246
|
const merging = latestEntries.filter((entry) => entry.status === "merging").length;
|
|
247
247
|
const evicted = latestEntries.filter((entry) => entry.status === "evicted").length;
|
|
248
|
-
const parts =
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
248
|
+
const parts = compact
|
|
249
|
+
? [
|
|
250
|
+
`${activeEntries.length}a`,
|
|
251
|
+
`${queued}w`,
|
|
252
|
+
`${validating}t`,
|
|
253
|
+
`${merging}m`,
|
|
254
|
+
]
|
|
255
|
+
: [
|
|
256
|
+
`${activeEntries.length} active`,
|
|
257
|
+
`${queued} waiting`,
|
|
258
|
+
`${validating} testing`,
|
|
259
|
+
`${merging} merging`,
|
|
260
|
+
];
|
|
254
261
|
if (evicted > 0) {
|
|
255
|
-
parts.push(`${evicted} need repair`);
|
|
262
|
+
parts.push(compact ? `${evicted}x` : `${evicted} need repair`);
|
|
256
263
|
}
|
|
257
264
|
if (avgWaitMs > 0) {
|
|
258
|
-
parts.push(`avg wait ${formatDuration(avgWaitMs)}`);
|
|
265
|
+
parts.push(compact ? `w ${formatDuration(avgWaitMs)}` : `avg wait ${formatDuration(avgWaitMs)}`);
|
|
259
266
|
}
|
|
260
267
|
return parts.join(" · ");
|
|
261
268
|
}
|