fix server log recovery and runtime status
This commit is contained in:
@@ -219,9 +219,11 @@ describe("first-party console pages", () => {
|
||||
expect(serversPageSource).not.toContain("ServerManagementTerminalDrawer");
|
||||
expect(serversPageSource).not.toContain("live-logs");
|
||||
expect(serversPageSource).not.toContain("管理终端");
|
||||
expect(serverDetailPageSource).toContain("ServerLiveLogDrawer");
|
||||
expect(serverDetailPageSource).toContain("ServerManagementTerminalDrawer");
|
||||
expect(serverDetailPageSource).toContain("实时日志");
|
||||
expect(serverDetailPageSource).not.toContain("ServerLiveLogDrawer");
|
||||
expect(serverDetailPageSource).not.toContain('<span>实时日志</span>');
|
||||
expect(serverDetailPageSource).toContain("runtimeObservationFreshness");
|
||||
expect(serverDetailPageSource).toContain("Run 未验证");
|
||||
expect(serverDetailPageSource).toContain("管理终端");
|
||||
});
|
||||
|
||||
@@ -333,11 +335,11 @@ describe("first-party console pages", () => {
|
||||
expect(serverLiveOperationsSource).toContain("handleCommandKeyDown");
|
||||
expect(serverLiveOperationsSource).toContain("commandHistory");
|
||||
expect(serverLiveOperationsSource).toContain("ArrowUp");
|
||||
expect(serverLiveOperationsSource).toContain("listServerLiveLogs");
|
||||
expect(serverLiveOperationsSource).not.toContain("listServerLiveLogs");
|
||||
expect(serverLiveOperationsSource).toContain("openServerLogEvents");
|
||||
expect(serverLiveOperationsSource).toContain("getGameClientBridgeCommand");
|
||||
expect(serverLiveOperationsSource).toContain("terminalLineFromBridgeCommand");
|
||||
expect(serverLiveOperationsSource).toContain("streams.filter((stream) => stream.latestSeq > 0)");
|
||||
expect(serverLiveOperationsSource).not.toContain("terminalRelevantStreams");
|
||||
expect(serverLiveOperationsSource).toContain("SSE 实时推送");
|
||||
expect(serverLiveOperationsSource).toContain("mergeTerminalLines");
|
||||
expect(serverLiveOperationsSource).toContain("terminalInitialHistoryWindow = 500");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Download, MoonStar, PackageOpen, Pencil, ScrollText, ShieldCheck, Sparkles, Square, Terminal, UserRoundMinus, UserRoundPlus, WandSparkles } from "lucide-react";
|
||||
import { Download, MoonStar, PackageOpen, Pencil, ShieldCheck, Sparkles, Square, Terminal, UserRoundMinus, UserRoundPlus, WandSparkles } from "lucide-react";
|
||||
import { type FormEvent, useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { platformApiClient } from "../api/client";
|
||||
@@ -16,7 +16,8 @@ import type {
|
||||
ServerMetricsResponse,
|
||||
ServerDeploymentResponse,
|
||||
MetricSampleResponse,
|
||||
RemoteAdapterDeclarationResponse
|
||||
RemoteAdapterDeclarationResponse,
|
||||
RunEndpointResponse
|
||||
} from "../api/types";
|
||||
import { ConfirmDialog, DiffView, UsageMeter } from "../components/OperationControls";
|
||||
import { ProductionGovernancePanel } from "../components/ProductionGovernancePanel";
|
||||
@@ -24,8 +25,8 @@ import { SourceRCONCommandPanel } from "../components/SourceRCONCommandPanel";
|
||||
import { DiagnosticSummary, EmptyState, ErrorState, LoadingState, ResultBadge } from "../components/StateViews";
|
||||
import type { PageComponentProps } from "../contracts/page";
|
||||
import { jobCapabilityLabel } from "../contracts/jobPresentation";
|
||||
import { canStartServer, canStopServer, serverMetadataFormFromInstance, type ServerMetadataFormState } from "../contracts/serverManagement";
|
||||
import { ServerLiveLogDrawer, ServerManagementTerminalDrawer } from "../components/ServerLiveOperations";
|
||||
import { canStartServer, canStopServer, runtimeObservationFreshness, serverMetadataFormFromInstance, type ServerMetadataFormState } from "../contracts/serverManagement";
|
||||
import { ServerManagementTerminalDrawer } from "../components/ServerLiveOperations";
|
||||
import {
|
||||
serverDetailSections,
|
||||
serverIsOnline,
|
||||
@@ -63,8 +64,8 @@ export function ServerDetailPage(props: PageComponentProps) {
|
||||
const [metricHistory, setMetricHistory] = useState<MetricSampleResponse[]>([]);
|
||||
const [backups, setBackups] = useState<BackupResponse[]>([]);
|
||||
const [remoteAdapters, setRemoteAdapters] = useState<RemoteAdapterDeclarationResponse[]>([]);
|
||||
const [runEndpoint, setRunEndpoint] = useState<RunEndpointResponse | undefined>();
|
||||
const [deployment, setDeployment] = useState<LoadState<ServerDeploymentResponse>>({ status: "loading" });
|
||||
const [liveLogOpen, setLiveLogOpen] = useState(false);
|
||||
const [terminalOpen, setTerminalOpen] = useState(false);
|
||||
const [confirm, setConfirm] = useState<null | { title: string; description: string; danger?: boolean; run: () => Promise<void> }>(null);
|
||||
const [confirmBusy, setConfirmBusy] = useState(false);
|
||||
@@ -76,7 +77,7 @@ export function ServerDetailPage(props: PageComponentProps) {
|
||||
}
|
||||
setInstance({ status: "loading" });
|
||||
try {
|
||||
const [detail, pluginResponse, jobResponse, deploymentResponse, metricHistoryResponse, backupResponse, adapterResponse] = await Promise.all([
|
||||
const [detail, pluginResponse, jobResponse, deploymentResponse, metricHistoryResponse, backupResponse, adapterResponse, endpointResponse] = await Promise.all([
|
||||
platformApiClient.getServerInstance(serverId),
|
||||
platformApiClient.listGamePlugins(),
|
||||
platformApiClient.listJobs(serverId),
|
||||
@@ -86,7 +87,8 @@ export function ServerDetailPage(props: PageComponentProps) {
|
||||
.catch((error): LoadState<ServerDeploymentResponse> => ({ status: "error", reason: error instanceof Error ? error.message : "部署定义加载失败" })),
|
||||
platformApiClient.listMetricHistory(serverId).catch(() => ({ items: [], count: 0 })),
|
||||
platformApiClient.listBackups(serverId).catch(() => ({ items: [], count: 0 })),
|
||||
platformApiClient.listRemoteAdapters(serverId).catch(() => ({ items: [], count: 0 }))
|
||||
platformApiClient.listRemoteAdapters(serverId).catch(() => ({ items: [], count: 0 })),
|
||||
platformApiClient.listRunEndpoints().catch(() => ({ items: [], count: 0 }))
|
||||
]);
|
||||
setInstance({ status: "ready", data: detail });
|
||||
setPlugins(pluginResponse.items);
|
||||
@@ -95,6 +97,7 @@ export function ServerDetailPage(props: PageComponentProps) {
|
||||
setMetricHistory(metricHistoryResponse.items);
|
||||
setBackups(backupResponse.items);
|
||||
setRemoteAdapters(adapterResponse.items);
|
||||
setRunEndpoint(endpointResponse.items.find((endpoint) => endpoint.id === detail.runEndpointId));
|
||||
const artifactLists = await Promise.all(
|
||||
jobResponse.items.slice(0, 20).map((job) =>
|
||||
platformApiClient
|
||||
@@ -107,6 +110,7 @@ export function ServerDetailPage(props: PageComponentProps) {
|
||||
} catch (error) {
|
||||
setInstance({ status: "error", reason: error instanceof Error ? error.message : "加载失败" });
|
||||
setArtifacts([]);
|
||||
setRunEndpoint(undefined);
|
||||
setDeployment({ status: "error", reason: "部署定义加载失败" });
|
||||
setMetricHistory([]);
|
||||
setBackups([]);
|
||||
@@ -127,14 +131,16 @@ export function ServerDetailPage(props: PageComponentProps) {
|
||||
const refreshOperationalState = useCallback(async () => {
|
||||
if (!serverId) return;
|
||||
try {
|
||||
const [detail, jobResponse, metricsResponse] = await Promise.all([
|
||||
const [detail, jobResponse, metricsResponse, endpointResponse] = await Promise.all([
|
||||
platformApiClient.getServerInstance(serverId),
|
||||
platformApiClient.listJobs(serverId),
|
||||
platformApiClient.listServerMetrics()
|
||||
platformApiClient.listServerMetrics(),
|
||||
platformApiClient.listRunEndpoints()
|
||||
]);
|
||||
setInstance({ status: "ready", data: detail });
|
||||
setJobs(jobResponse.items);
|
||||
setMetrics(metricsResponse.items.find((item) => item.serverInstanceId === serverId) ?? null);
|
||||
setRunEndpoint(endpointResponse.items.find((endpoint) => endpoint.id === detail.runEndpointId));
|
||||
} catch {
|
||||
setMetrics(null);
|
||||
}
|
||||
@@ -151,6 +157,8 @@ export function ServerDetailPage(props: PageComponentProps) {
|
||||
);
|
||||
const canManageServers = session.capabilities.includes("servers.manage");
|
||||
const readyPlugin = instance.status === "ready" ? plugins.find((plugin) => plugin.id === instance.data.pluginId) : undefined;
|
||||
const detailFreshness = instance.status === "ready" ? runtimeObservationFreshness(instance.data, runEndpoint) : "unverified";
|
||||
const detailStateText = instance.status === "ready" && detailFreshness === "fresh" ? stateLabel(instance.data.state) : instance.status === "ready" ? `最后观测:${stateLabel(instance.data.state)}(Run 未验证)` : "未验证";
|
||||
const detailSections = useMemo(() => serverDetailSectionEntries(readyPlugin), [readyPlugin]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -243,7 +251,7 @@ export function ServerDetailPage(props: PageComponentProps) {
|
||||
</span>
|
||||
</div>
|
||||
<div className="action-strip">
|
||||
<span className={cx("status-pill", statusClass(instance.data.state))}>{stateLabel(instance.data.state)}</span>
|
||||
<span className={cx("status-pill", detailFreshness === "fresh" ? statusClass(instance.data.state) : "status-disabled")}>{detailStateText}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-command"
|
||||
@@ -262,12 +270,11 @@ export function ServerDetailPage(props: PageComponentProps) {
|
||||
<Square size={15} />
|
||||
<span>停止</span>
|
||||
</button>
|
||||
<button type="button" className="icon-command" onClick={() => setLiveLogOpen(true)}><ScrollText size={15} /><span>实时日志</span></button>
|
||||
<button type="button" className="icon-command" disabled={!canManageServers} title={canManageServers ? "管理终端" : "当前账号没有管理权限"} onClick={() => setTerminalOpen(true)}><Terminal size={15} /><span>管理终端</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="server-detail-stat-strip">
|
||||
<HeaderStat label="状态" value={serverIsOnline(instance.data.state) ? "在线" : "离线"} />
|
||||
<HeaderStat label="状态" value={detailFreshness === "fresh" ? (serverIsOnline(instance.data.state) ? "在线" : "离线") : "未验证"} />
|
||||
<HeaderStat label="玩家" value={metrics?.playerCount !== undefined ? `${metrics.playerCount}${metrics.maxPlayers ? `/${metrics.maxPlayers}` : ""}` : "--"} />
|
||||
<HeaderStat label="TPS" value={metrics?.tps !== undefined ? metrics.tps.toFixed(1) : "--"} />
|
||||
<HeaderStat label="延迟" value={metrics?.latencyMs !== undefined ? `${Math.round(metrics.latencyMs)}ms` : "--"} />
|
||||
@@ -310,7 +317,6 @@ export function ServerDetailPage(props: PageComponentProps) {
|
||||
{section === "config" && <ConfigSection serverId={serverId} instance={instance.data} session={session} operations={operations} />}
|
||||
{section === "llm" && <LlmSection serverId={serverId} instance={instance.data} session={session} operations={operations} />}
|
||||
{section === "history" && <HistorySection serverId={serverId} serverOperations={serverOperations} jobs={jobs} artifacts={artifacts} metricHistory={metricHistory} backups={backups} remoteAdapters={remoteAdapters} />}
|
||||
<ServerLiveLogDrawer open={liveLogOpen} serverId={instance.data.id} serverName={instance.data.name} onClose={() => setLiveLogOpen(false)} />
|
||||
<ServerManagementTerminalDrawer open={terminalOpen} serverId={instance.data.id} serverName={instance.data.name} pluginId={instance.data.pluginId} canManage={canManageServers} onClose={() => setTerminalOpen(false)} />
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user