Remove duplicate server detail metrics
This commit is contained in:
@@ -52,7 +52,7 @@ Existing platform APIs already cover server lifecycle, jobs, log stream metadata
|
|||||||
- `PUT /api/v1/users/current/profile` (`UserProfileUpdateRequest`/`CurrentUserResponse`): implemented current-user profile updates such as display name, avatar reference, phone, QQ, and bounded contact fields.
|
- `PUT /api/v1/users/current/profile` (`UserProfileUpdateRequest`/`CurrentUserResponse`): implemented current-user profile updates such as display name, avatar reference, phone, QQ, and bounded contact fields.
|
||||||
- `PUT /api/v1/users/current/theme` (`UserThemePreferenceRequest`/`UserThemePreferenceResponse`): implemented per-user theme preferences, including selected palette IDs such as `mecha-black` or `magical-girl`, uploaded background reference or safe persisted data URL metadata, and readable overlay preference.
|
- `PUT /api/v1/users/current/theme` (`UserThemePreferenceRequest`/`UserThemePreferenceResponse`): implemented per-user theme preferences, including selected palette IDs such as `mecha-black` or `magical-girl`, uploaded background reference or safe persisted data URL metadata, and readable overlay preference.
|
||||||
- `GET /api/v1/metrics/platform` (`PlatformResourceUsageResponse`): implemented platform-level CPU/memory/disk usage and LLM connectivity summary for the overview first screen.
|
- `GET /api/v1/metrics/platform` (`PlatformResourceUsageResponse`): implemented platform-level CPU/memory/disk usage and LLM connectivity summary for the overview first screen.
|
||||||
- `GET /api/v1/metrics/server-instances` (`ServerMetricsListResponse`): implemented per-server online state, player count, TPS, latency, CPU/memory/disk for server cards and the server detail header.
|
- `GET /api/v1/metrics/server-instances` (`ServerMetricsListResponse`): implemented per-server online state, player count, TPS, latency, CPU/memory/disk for server cards on the server list.
|
||||||
- Server-scoped raw config routes (`GET /api/v1/server-instances/{id}/config`, `POST .../config/diff`, `POST .../config/approve`) are removed from the product API. AI configuration assistance uses `/api/v1/ai/invocations` plus reviewable AI config-diff approval APIs; plugin pages do not receive raw config text.
|
- Server-scoped raw config routes (`GET /api/v1/server-instances/{id}/config`, `POST .../config/diff`, `POST .../config/approve`) are removed from the product API. AI configuration assistance uses `/api/v1/ai/invocations` plus reviewable AI config-diff approval APIs; plugin pages do not receive raw config text.
|
||||||
- `POST /api/v1/file-operations/dispatch` (`FileOperationDispatchRequest`/`FileOperationDispatchResponse`): implemented scoped file operation dispatch using logical keys and refs only.
|
- `POST /api/v1/file-operations/dispatch` (`FileOperationDispatchRequest`/`FileOperationDispatchResponse`): implemented scoped file operation dispatch using logical keys and refs only.
|
||||||
- `POST /api/v1/ai/config-suggestions` (`LlmConfigSuggestionRequest`/`LlmConfigSuggestionResponse`) and `POST /api/v1/ai/invocations` (`AIInvocationRequest`/`AIInvocationResponse`): platform-mediated AI recommendation or diff scoped to one server. Provider keys stay in `platform/`; responses carry only recommendation text, usage metadata, and reviewable suggestions, never keys or provider secrets.
|
- `POST /api/v1/ai/config-suggestions` (`LlmConfigSuggestionRequest`/`LlmConfigSuggestionResponse`) and `POST /api/v1/ai/invocations` (`AIInvocationRequest`/`AIInvocationResponse`): platform-mediated AI recommendation or diff scoped to one server. Provider keys stay in `platform/`; responses carry only recommendation text, usage metadata, and reviewable suggestions, never keys or provider secrets.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { configDiffViewFromPreview } from "./ServerDetailPage";
|
import { configDiffViewFromPreview } from "./ServerDetailPage";
|
||||||
|
import serversPageSource from "./ServersPage.tsx?raw";
|
||||||
import serverManagementTerminalSource from "../components/ServerManagementTerminalDrawer.tsx?raw";
|
import serverManagementTerminalSource from "../components/ServerManagementTerminalDrawer.tsx?raw";
|
||||||
import serverDetailPageSource from "./ServerDetailPage.tsx?raw";
|
import serverDetailPageSource from "./ServerDetailPage.tsx?raw";
|
||||||
import type { ServerConfigDiffPreviewResponse } from "../api/types";
|
import type { ServerConfigDiffPreviewResponse } from "../api/types";
|
||||||
@@ -22,6 +23,15 @@ const preview: ServerConfigDiffPreviewResponse = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe("ServerDetailPage config write approval", () => {
|
describe("ServerDetailPage config write approval", () => {
|
||||||
|
it("keeps server overview metrics on the server list instead of the detail header", () => {
|
||||||
|
expect(serverDetailPageSource).not.toContain("listServerMetrics");
|
||||||
|
expect(serverDetailPageSource).not.toContain("server-detail-stat-strip");
|
||||||
|
expect(serverDetailPageSource).not.toContain("server-detail-meter-strip");
|
||||||
|
expect(serverDetailPageSource).not.toContain("<UsageMeter");
|
||||||
|
expect(serversPageSource).toContain("listServerMetrics");
|
||||||
|
expect(serversPageSource).toContain("<UsageMeter");
|
||||||
|
});
|
||||||
|
|
||||||
it("maps platform diff preview responses into the display diff without losing approval metadata", () => {
|
it("maps platform diff preview responses into the display diff without losing approval metadata", () => {
|
||||||
const view = configDiffViewFromPreview(preview);
|
const view = configDiffViewFromPreview(preview);
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import type {
|
|||||||
JobResponse,
|
JobResponse,
|
||||||
ServerInstanceResponse,
|
ServerInstanceResponse,
|
||||||
ServerMemberResponse,
|
ServerMemberResponse,
|
||||||
ServerMetricsResponse,
|
|
||||||
ServerDeploymentResponse,
|
ServerDeploymentResponse,
|
||||||
ServerConfigDiffPreviewResponse,
|
ServerConfigDiffPreviewResponse,
|
||||||
RunEndpointResponse,
|
RunEndpointResponse,
|
||||||
@@ -17,14 +16,13 @@ import type {
|
|||||||
ServerFileListResponse,
|
ServerFileListResponse,
|
||||||
ServerFileWorkspaceResponse
|
ServerFileWorkspaceResponse
|
||||||
} from "../api/types";
|
} from "../api/types";
|
||||||
import { ConfirmDialog, UsageMeter } from "../components/OperationControls";
|
import { ConfirmDialog } from "../components/OperationControls";
|
||||||
import { ServerManagementTerminalDrawer } from "../components/ServerManagementTerminalDrawer";
|
import { ServerManagementTerminalDrawer } from "../components/ServerManagementTerminalDrawer";
|
||||||
import { EmptyState, ErrorState, LoadingState, ResultBadge } from "../components/StateViews";
|
import { EmptyState, ErrorState, LoadingState, ResultBadge } from "../components/StateViews";
|
||||||
import type { PageComponentProps } from "../contracts/page";
|
import type { PageComponentProps } from "../contracts/page";
|
||||||
import { canStartServer, canStopServer, runtimeObservationFreshness, serverMetadataFormFromInstance, type ServerMetadataFormState } from "../contracts/serverManagement";
|
import { canStartServer, canStopServer, runtimeObservationFreshness, serverMetadataFormFromInstance, type ServerMetadataFormState } from "../contracts/serverManagement";
|
||||||
import {
|
import {
|
||||||
serverDetailSections,
|
serverDetailSections,
|
||||||
serverIsOnline,
|
|
||||||
type ConfigDiffView,
|
type ConfigDiffView,
|
||||||
type LlmSuggestionView,
|
type LlmSuggestionView,
|
||||||
type ServerDetailSection
|
type ServerDetailSection
|
||||||
@@ -41,14 +39,12 @@ import { PluginPageHostPage } from "./PluginPageHostPage";
|
|||||||
type LoadState<T> = { status: "loading" } | { status: "error"; reason: string } | { status: "ready"; data: T };
|
type LoadState<T> = { status: "loading" } | { status: "error"; reason: string } | { status: "ready"; data: T };
|
||||||
|
|
||||||
const serverDetailRefreshMs = 5000;
|
const serverDetailRefreshMs = 5000;
|
||||||
const serverMetricFreshMs = 30000;
|
|
||||||
|
|
||||||
export function ServerDetailPage(props: PageComponentProps) {
|
export function ServerDetailPage(props: PageComponentProps) {
|
||||||
const { session, params, operations, onNavigate } = props;
|
const { session, params, operations, onNavigate } = props;
|
||||||
const serverId = params.serverId ?? "";
|
const serverId = params.serverId ?? "";
|
||||||
const [section, setSection] = useState<ServerDetailSection>("manage");
|
const [section, setSection] = useState<ServerDetailSection>("manage");
|
||||||
const [instance, setInstance] = useState<LoadState<ServerInstanceResponse>>({ status: "loading" });
|
const [instance, setInstance] = useState<LoadState<ServerInstanceResponse>>({ status: "loading" });
|
||||||
const [metrics, setMetrics] = useState<ServerMetricsResponse | null>(null);
|
|
||||||
const [plugins, setPlugins] = useState<GamePluginResponse[]>([]);
|
const [plugins, setPlugins] = useState<GamePluginResponse[]>([]);
|
||||||
const [jobs, setJobs] = useState<JobResponse[]>([]);
|
const [jobs, setJobs] = useState<JobResponse[]>([]);
|
||||||
const [runEndpoint, setRunEndpoint] = useState<RunEndpointResponse | undefined>();
|
const [runEndpoint, setRunEndpoint] = useState<RunEndpointResponse | undefined>();
|
||||||
@@ -84,12 +80,6 @@ export function ServerDetailPage(props: PageComponentProps) {
|
|||||||
setRunEndpoint(undefined);
|
setRunEndpoint(undefined);
|
||||||
setDeployment({ status: "error", reason: "部署定义加载失败" });
|
setDeployment({ status: "error", reason: "部署定义加载失败" });
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
const metricsResponse = await platformApiClient.listServerMetrics();
|
|
||||||
setMetrics(metricsResponse.items.find((item) => item.serverInstanceId === serverId) ?? null);
|
|
||||||
} catch {
|
|
||||||
setMetrics(null);
|
|
||||||
}
|
|
||||||
}, [serverId]);
|
}, [serverId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -99,18 +89,16 @@ export function ServerDetailPage(props: PageComponentProps) {
|
|||||||
const refreshOperationalState = useCallback(async () => {
|
const refreshOperationalState = useCallback(async () => {
|
||||||
if (!serverId) return;
|
if (!serverId) return;
|
||||||
try {
|
try {
|
||||||
const [detail, jobResponse, metricsResponse, endpointResponse] = await Promise.all([
|
const [detail, jobResponse, endpointResponse] = await Promise.all([
|
||||||
platformApiClient.getServerInstance(serverId),
|
platformApiClient.getServerInstance(serverId),
|
||||||
platformApiClient.listJobs(serverId),
|
platformApiClient.listJobs(serverId),
|
||||||
platformApiClient.listServerMetrics(),
|
|
||||||
platformApiClient.listRunEndpoints()
|
platformApiClient.listRunEndpoints()
|
||||||
]);
|
]);
|
||||||
setInstance({ status: "ready", data: detail });
|
setInstance({ status: "ready", data: detail });
|
||||||
setJobs(jobResponse.items);
|
setJobs(jobResponse.items);
|
||||||
setMetrics(metricsResponse.items.find((item) => item.serverInstanceId === serverId) ?? null);
|
|
||||||
setRunEndpoint(endpointResponse.items.find((endpoint) => endpoint.id === detail.runEndpointId));
|
setRunEndpoint(endpointResponse.items.find((endpoint) => endpoint.id === detail.runEndpointId));
|
||||||
} catch {
|
} catch {
|
||||||
setMetrics(null);
|
setRunEndpoint(undefined);
|
||||||
}
|
}
|
||||||
}, [serverId]);
|
}, [serverId]);
|
||||||
|
|
||||||
@@ -124,7 +112,6 @@ export function ServerDetailPage(props: PageComponentProps) {
|
|||||||
const detailFreshness = instance.status === "ready" ? runtimeObservationFreshness(instance.data, runEndpoint) : "unverified";
|
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 detailStateText = instance.status === "ready" && detailFreshness === "fresh" ? stateLabel(instance.data.state) : instance.status === "ready" ? `最后观测:${stateLabel(instance.data.state)}(Run 未验证)` : "未验证";
|
||||||
const detailSections = useMemo(() => serverDetailSectionEntries(readyPlugin), [readyPlugin]);
|
const detailSections = useMemo(() => serverDetailSectionEntries(readyPlugin), [readyPlugin]);
|
||||||
const metricsWaiting = metrics?.source === "run-metrics-pending";
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!params.routeKey || !readyPlugin?.pages.some((page) => page.key === params.routeKey)) return;
|
if (!params.routeKey || !readyPlugin?.pages.some((page) => page.key === params.routeKey)) return;
|
||||||
@@ -247,18 +234,6 @@ export function ServerDetailPage(props: PageComponentProps) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="server-detail-stat-strip">
|
|
||||||
<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` : "--"} />
|
|
||||||
<HeaderStat label="指标" value={metricFreshnessLabel(metrics)} />
|
|
||||||
</div>
|
|
||||||
<div className="server-detail-meter-strip">
|
|
||||||
<UsageMeter label="CPU" percent={metrics?.cpuPercent} pending={metricsWaiting} />
|
|
||||||
<UsageMeter label="内存" percent={metrics?.memoryPercent} pending={metricsWaiting} />
|
|
||||||
<UsageMeter label="磁盘" percent={metrics?.diskPercent} pending={metricsWaiting} />
|
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<nav className="section-tabs" aria-label="server sections">
|
<nav className="section-tabs" aria-label="server sections">
|
||||||
@@ -886,22 +861,6 @@ function formatDateTime(value?: string): string {
|
|||||||
return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleString();
|
return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleString();
|
||||||
}
|
}
|
||||||
|
|
||||||
function HeaderStat({ label, value }: { label: string; value: string }) {
|
|
||||||
return (
|
|
||||||
<span className="server-card-stat">
|
|
||||||
<span>{label}</span>
|
|
||||||
<strong>{value}</strong>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function metricFreshnessLabel(metrics: ServerMetricsResponse | null): string {
|
|
||||||
if (!metrics || metrics.source === "run-metrics-pending") return "等待上报";
|
|
||||||
const collectedAt = new Date(metrics.collectedAt).getTime();
|
|
||||||
if (Number.isFinite(collectedAt) && Date.now() - collectedAt > serverMetricFreshMs) return "指标过期";
|
|
||||||
return new Date(metrics.collectedAt).toLocaleTimeString();
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LlmSectionProps {
|
interface LlmSectionProps {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
instance: ServerInstanceResponse;
|
instance: ServerInstanceResponse;
|
||||||
|
|||||||
@@ -489,8 +489,6 @@ to{transform:translate(-50%,-50%) rotate(calc(var(--construct-drift) + 360deg))}
|
|||||||
.server-detail-title-row{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}
|
.server-detail-title-row{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}
|
||||||
.server-detail-title-row>div:first-child{min-width:0}
|
.server-detail-title-row>div:first-child{min-width:0}
|
||||||
.server-detail-title-row h1{margin:0;font-size:24px;color:var(--ink);overflow-wrap:anywhere}
|
.server-detail-title-row h1{margin:0;font-size:24px;color:var(--ink);overflow-wrap:anywhere}
|
||||||
.server-detail-stat-strip{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:8px}
|
|
||||||
.server-detail-meter-strip{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}
|
|
||||||
.section-tabs{display:flex;gap:6px;flex-wrap:wrap;padding:8px;border:1px solid var(--line);border-radius:8px;background:var(--frosted-surface),color-mix(in srgb,var(--surface) 72%,transparent);box-shadow:inset 0 1px 0 var(--crystal-rim)}
|
.section-tabs{display:flex;gap:6px;flex-wrap:wrap;padding:8px;border:1px solid var(--line);border-radius:8px;background:var(--frosted-surface),color-mix(in srgb,var(--surface) 72%,transparent);box-shadow:inset 0 1px 0 var(--crystal-rim)}
|
||||||
.section-tab{display:inline-flex;align-items:center;gap:6px;min-height:38px;padding:0 14px;border:1px solid var(--line-strong);border-radius:999px;background:var(--control-surface);color:var(--ink-soft);cursor:pointer;white-space:nowrap}
|
.section-tab{display:inline-flex;align-items:center;gap:6px;min-height:38px;padding:0 14px;border:1px solid var(--line-strong);border-radius:999px;background:var(--control-surface);color:var(--ink-soft);cursor:pointer;white-space:nowrap}
|
||||||
.section-tab:focus-visible,.section-tab:hover{border-color:var(--accent);outline:0}
|
.section-tab:focus-visible,.section-tab:hover{border-color:var(--accent);outline:0}
|
||||||
@@ -722,8 +720,7 @@ to{transform:translate(-50%,-50%) rotate(calc(var(--construct-drift) + 360deg))}
|
|||||||
.page-title{font-size:24px}
|
.page-title{font-size:24px}
|
||||||
.catalog-grid,.console-grid,.metric-grid,.overview-two-col,.resource-list-item,.server-card-grid{grid-template-columns:1fr}
|
.catalog-grid,.console-grid,.metric-grid,.overview-two-col,.resource-list-item,.server-card-grid{grid-template-columns:1fr}
|
||||||
.form-grid,.server-metrics,.server-workspace{grid-template-columns:1fr}
|
.form-grid,.server-metrics,.server-workspace{grid-template-columns:1fr}
|
||||||
.server-detail-stat-strip{grid-template-columns:repeat(2,minmax(0,1fr))}
|
.terminal-command-form{grid-template-columns:1fr}
|
||||||
.server-detail-meter-strip,.terminal-command-form{grid-template-columns:1fr}
|
|
||||||
.client-manager-command-grid,.client-manager-version-grid{grid-template-columns:1fr}
|
.client-manager-command-grid,.client-manager-version-grid{grid-template-columns:1fr}
|
||||||
.server-card-stats{grid-template-columns:repeat(2,minmax(0,1fr))}
|
.server-card-stats{grid-template-columns:repeat(2,minmax(0,1fr))}
|
||||||
.section-tabs{overflow-x:auto;flex-wrap:nowrap;padding-bottom:4px}
|
.section-tabs{overflow-x:auto;flex-wrap:nowrap;padding-bottom:4px}
|
||||||
|
|||||||
Reference in New Issue
Block a user