Refine terminal history scrolling and job failure summaries

This commit is contained in:
npc0-hue
2026-08-10 13:48:23 +08:00
parent fbd24b6a44
commit d1f4dce4f5
7 changed files with 54 additions and 11 deletions
@@ -15,7 +15,7 @@ type TerminalQuickCommand = { label: string; command: string; hint: string };
const terminalBridgeResultPollMs = 1000;
const terminalBridgeResultPollAttempts = 30;
const liveLogHistoryWindow = 100;
const terminalHistoryWindow = 10000;
const terminalInitialHistoryWindow = 500;
const maxLogEntries = 500;
const maxTerminalLines = 10000;
const terminalQuickCommandCatalog: Record<string, TerminalQuickCommand[]> = {
@@ -212,6 +212,7 @@ export function ServerManagementTerminalDrawer({ open, serverId, serverName, plu
const [followLatest, setFollowLatest] = useState(true);
const outputRef = useRef<HTMLDivElement>(null);
const followLatestRef = useRef(true);
const initialHistoryPendingRef = useRef(false);
const quickCommands = useMemo(() => terminalQuickCommandsForPlugin(pluginId), [pluginId]);
const supportsCommands = quickCommands.length > 0;
const terminalStreams = useMemo(() => streams.status === "ready" ? terminalRelevantStreams(streams.data) : [], [streams]);
@@ -221,6 +222,20 @@ export function ServerManagementTerminalDrawer({ open, serverId, serverName, plu
setLines((current) => mergeTerminalLines(current, incoming));
}, []);
const lockTerminalFollow = useCallback(() => {
followLatestRef.current = true;
setFollowLatest(true);
window.requestAnimationFrame(() => {
const output = outputRef.current;
if (output) output.scrollTop = output.scrollHeight;
window.requestAnimationFrame(() => {
const output = outputRef.current;
if (output) output.scrollTop = output.scrollHeight;
initialHistoryPendingRef.current = false;
});
});
}, []);
const loadStreams = useCallback(async (showLoading = true) => {
if (!open) return;
if (showLoading) setStreams({ status: "loading" });
@@ -238,6 +253,7 @@ export function ServerManagementTerminalDrawer({ open, serverId, serverName, plu
setPending(false);
setResult(null);
setHistoryIndex(null);
initialHistoryPendingRef.current = true;
followLatestRef.current = true;
setFollowLatest(true);
setLines([terminalSystemLine("info", supportsCommands ? "连接平台日志推送,先补最近历史再实时追加。" : "该插件暂未声明可用的管理终端命令通道。", "SYSTEM")]);
@@ -245,7 +261,7 @@ export function ServerManagementTerminalDrawer({ open, serverId, serverName, plu
}, [loadStreams, open, supportsCommands]);
useEffect(() => {
if (!open || !followLatestRef.current) return undefined;
if (!open || initialHistoryPendingRef.current || !followLatestRef.current) return undefined;
const frame = window.requestAnimationFrame(() => {
const output = outputRef.current;
if (output) output.scrollTop = output.scrollHeight;
@@ -256,7 +272,7 @@ export function ServerManagementTerminalDrawer({ open, serverId, serverName, plu
useEffect(() => {
if (!open) return undefined;
let ready = false;
const events = platformApiClient.openServerLogEvents(serverId, { historyLimit: terminalHistoryWindow });
const events = platformApiClient.openServerLogEvents(serverId, { historyLimit: terminalInitialHistoryWindow });
events.addEventListener("stream", (event) => {
const stream = parseLogStreamEvent(event);
if (!stream) return;
@@ -266,6 +282,7 @@ export function ServerManagementTerminalDrawer({ open, serverId, serverName, plu
events.addEventListener("ready", () => {
ready = true;
setStreams((current) => current.status === "ready" ? current : { status: "ready", data: [] });
lockTerminalFollow();
});
events.addEventListener("log", (event) => {
const payload = parseServerLogEvent(event);
@@ -279,7 +296,7 @@ export function ServerManagementTerminalDrawer({ open, serverId, serverName, plu
if (!ready) setStreams({ status: "error", reason: "实时日志推送连接失败" });
};
return () => events.close();
}, [appendLines, open, serverId]);
}, [appendLines, lockTerminalFollow, open, serverId]);
function selectQuickCommand(item: TerminalQuickCommand) {
setCommand(item.command);
@@ -67,6 +67,20 @@ describe("operations console contracts", () => {
expect(summaries.find((item) => item.instance.id === "job-failed")?.failedJobs).toBe(1);
});
it("does not count failed jobs older than the latest healthy server projection", () => {
const summaries = summarizeServerOperations(
[server("recovered", "running", "2026-07-18T10:10:00Z")],
new Map(),
[
job("old-failed-start", "failed", "2026-07-18T10:03:00Z", "recovered"),
job("healthy-start", "succeeded", "2026-07-18T10:10:00Z", "recovered")
]
);
expect(summaries[0]?.failedJobs).toBe(0);
expect(summaries[0]?.latestJob?.id).toBe("healthy-start");
});
it("summarizes only endpoint-safe capacity and status values", () => {
const endpoints: RunEndpointResponse[] = [
{
+10 -1
View File
@@ -85,13 +85,22 @@ export function summarizeServerOperations(
instance,
metrics: metrics.get(instance.id),
activeJobs: serverJobs.filter(isActiveJob).length,
failedJobs: serverJobs.filter((job) => job.state === "failed").length,
failedJobs: currentFailedJobsForServer(instance, serverJobs).length,
latestJob: serverJobs[0]
};
})
.sort(compareServerOperations);
}
function currentFailedJobsForServer(instance: ServerInstanceResponse, jobs: JobResponse[]): JobResponse[] {
const failedJobs = jobs.filter((job) => job.state === "failed");
if (instance.state === "failed") {
return failedJobs;
}
const instanceUpdatedAt = timestamp(instance.updatedAt);
return failedJobs.filter((job) => timestamp(job.terminalAt ?? job.updatedAt) >= instanceUpdatedAt);
}
export function summarizeEndpointOperations(endpoints: RunEndpointResponse[]): EndpointOperationsSummary {
return endpoints.reduce<EndpointOperationsSummary>(
(summary, endpoint) => ({
+3 -1
View File
@@ -340,9 +340,11 @@ describe("first-party console pages", () => {
expect(serverLiveOperationsSource).toContain("streams.filter((stream) => stream.latestSeq > 0)");
expect(serverLiveOperationsSource).toContain("SSE 实时推送");
expect(serverLiveOperationsSource).toContain("mergeTerminalLines");
expect(serverLiveOperationsSource).toContain("terminalHistoryWindow = 10000");
expect(serverLiveOperationsSource).toContain("terminalInitialHistoryWindow = 500");
expect(serverLiveOperationsSource).toContain("maxTerminalLines = 10000");
expect(serverLiveOperationsSource).toContain("followLatestRef");
expect(serverLiveOperationsSource).toContain("initialHistoryPendingRef");
expect(serverLiveOperationsSource).toContain("lockTerminalFollow");
expect(serverLiveOperationsSource).toContain("handleTerminalScroll");
expect(serverLiveOperationsSource).toContain("scrollHeight - output.clientHeight - output.scrollTop <= 24");
expect(serverLiveOperationsSource).not.toContain("terminalLogPollMs = 1000");