Remove pre-1.0 audit and protected request scaffolding
This commit is contained in:
@@ -103,15 +103,13 @@ describe("first-party console pages", () => {
|
||||
},
|
||||
metrics: { status: "error", reason: "metrics unavailable", diagnosticId: "metrics-1" },
|
||||
usage: { status: "error", reason: "usage unavailable", diagnosticId: "usage-1" },
|
||||
providers: { status: "error", reason: "providers unavailable", diagnosticId: "providers-1" },
|
||||
signals: { status: "error", reason: "audit unavailable", diagnosticId: "audit-1" }
|
||||
providers: { status: "error", reason: "providers unavailable", diagnosticId: "providers-1" }
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(html).toContain("4 个模块不可用");
|
||||
expect(html).toContain("3 个模块不可用");
|
||||
expect(html).toContain("资源指标不可用");
|
||||
expect(html).toContain("审计信号不可用");
|
||||
expect(html).toContain("AI 提供商信号不可用");
|
||||
expect(html).toContain("server.lifecycle.start");
|
||||
expect(html).not.toContain("暂无异常信号");
|
||||
@@ -131,10 +129,9 @@ describe("first-party console pages", () => {
|
||||
core: { status: "ready", data: { instances: [], endpoints: [], jobs: [] }, refreshedAt: "2026-07-18T10:00:00Z" },
|
||||
metrics: { status: "ready", data: [], refreshedAt: "2026-07-18T10:00:00Z" },
|
||||
usage: { status: "error", reason: "unavailable", diagnosticId: "usage" },
|
||||
providers: { status: "ready", data: [], refreshedAt: "2026-07-18T10:00:00Z" },
|
||||
signals: { status: "ready", data: [], refreshedAt: "2026-07-18T10:00:00Z" }
|
||||
}}
|
||||
/>
|
||||
providers: { status: "ready", data: [], refreshedAt: "2026-07-18T10:00:00Z" }
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(html).toContain("没有创建服务器的权限");
|
||||
@@ -196,7 +193,7 @@ describe("first-party console pages", () => {
|
||||
expect(serverDeploymentWorkflowSource).toContain("不会重装或覆盖现有游戏配置");
|
||||
expect(serverDeploymentWorkflowSource).toContain("接管已有服务器执行流程");
|
||||
expect(serverDeploymentWorkflowSource).toContain("不需要填写 SteamCMD 目录");
|
||||
expect(serverDeploymentWorkflowSource).toContain("当前平台尚未提供 SCUM 服务端的受控升级任务");
|
||||
expect(serverDeploymentWorkflowSource).toContain("当前平台尚未提供 SCUM 服务端的自动升级任务");
|
||||
expect(serverDeploymentWorkflowSource).toContain("Run 会按心跳自动识别服务器");
|
||||
expect(serversPageSource).toContain('onNavigate("serverDetail", { serverId: result.instance.id })');
|
||||
expect(serverDetailPageSource).not.toContain("运行配置绑定");
|
||||
@@ -229,7 +226,7 @@ describe("first-party console pages", () => {
|
||||
expect(serverDetailPageSource).not.toContain("操作历史");
|
||||
expect(serverDetailPageSource).toContain("<span>打开终端</span>");
|
||||
expect(serverManagementTerminalSource).toContain("openServerLogEvents");
|
||||
expect(serverManagementTerminalSource).toContain("queueGameClientBridgeCommand");
|
||||
expect(serverManagementTerminalSource).toContain("dispatchSourceRCONCommand");
|
||||
expect(serverManagementTerminalSource).toContain("terminalQuickCommandCatalog");
|
||||
expect(serverManagementTerminalSource).not.toContain("password");
|
||||
expect(serverManagementTerminalSource).not.toContain("direct socket");
|
||||
@@ -342,7 +339,7 @@ describe("first-party console pages", () => {
|
||||
expect(serverDetailPageSource).toContain("打开终端");
|
||||
expect(serverManagementTerminalSource).toContain("terminalQuickCommandCatalog");
|
||||
expect(serverDetailPageSource).not.toContain("scumManagementRCONCommandRequest");
|
||||
expect(serverManagementTerminalSource).toContain("queueGameClientBridgeCommand");
|
||||
expect(serverManagementTerminalSource).toContain("dispatchSourceRCONCommand");
|
||||
expect(serverDetailPageSource).not.toContain("commandHistory");
|
||||
});
|
||||
|
||||
@@ -379,7 +376,7 @@ describe("first-party console pages", () => {
|
||||
expect(html).toContain("维护排障入口");
|
||||
expect(html).toContain("节点详情");
|
||||
expect(html).toContain("最近失败任务");
|
||||
expect(html).toContain("审计异常");
|
||||
expect(html).toContain("最近失败任务");
|
||||
});
|
||||
|
||||
it("renders profile settings as a full page", () => {
|
||||
|
||||
@@ -17,7 +17,6 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { platformApiClient } from "../api/client";
|
||||
import type {
|
||||
AiProviderResponse,
|
||||
AuditEventResponse,
|
||||
JobResponse,
|
||||
PlatformResourceUsageResponse,
|
||||
RunEndpointResponse,
|
||||
@@ -25,7 +24,7 @@ import type {
|
||||
ServerMetricsResponse
|
||||
} from "../api/types";
|
||||
import { UsageMeter } from "../components/OperationControls";
|
||||
import { ProductionGovernancePanel } from "../components/ProductionGovernancePanel";
|
||||
import { ProductionOperationsPanel } from "../components/ProductionOperationsPanel";
|
||||
import { EmptyState, ErrorState, LoadingState } from "../components/StateViews";
|
||||
import { jobBuckets, moduleFreshnessLabel, summarizeEndpointOperations, type OperationsModuleState } from "../contracts/operationsConsole";
|
||||
import { jobCapabilityLabel } from "../contracts/jobPresentation";
|
||||
@@ -45,7 +44,6 @@ export interface HomePageInitialState {
|
||||
metrics?: OperationsModuleState<ServerMetricsResponse[]>;
|
||||
usage?: OperationsModuleState<PlatformResourceUsageResponse>;
|
||||
providers?: OperationsModuleState<AiProviderResponse[]>;
|
||||
signals?: OperationsModuleState<AuditEventResponse[]>;
|
||||
}
|
||||
|
||||
interface HomePageProps extends PageComponentProps {
|
||||
@@ -57,7 +55,6 @@ export function HomePage({ session, onNavigate, initialState }: HomePageProps) {
|
||||
const [metrics, setMetrics] = useState<OperationsModuleState<ServerMetricsResponse[]>>(initialState?.metrics ?? { status: "loading" });
|
||||
const [usage, setUsage] = useState<OperationsModuleState<PlatformResourceUsageResponse>>(initialState?.usage ?? { status: "loading" });
|
||||
const [providers, setProviders] = useState<OperationsModuleState<AiProviderResponse[]>>(initialState?.providers ?? { status: "loading" });
|
||||
const [signals, setSignals] = useState<OperationsModuleState<AuditEventResponse[]>>(initialState?.signals ?? { status: "loading" });
|
||||
|
||||
const refreshCore = useCallback(async () => {
|
||||
setCore({ status: "loading" });
|
||||
@@ -103,23 +100,12 @@ export function HomePage({ session, onNavigate, initialState }: HomePageProps) {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const refreshSignals = useCallback(async () => {
|
||||
setSignals({ status: "loading" });
|
||||
try {
|
||||
const response = await platformApiClient.listAuditEvents();
|
||||
setSignals({ status: "ready", data: response.items, refreshedAt: refreshedNow() });
|
||||
} catch (error) {
|
||||
setSignals({ status: "error", reason: errorMessage(error, "审计事件加载失败"), diagnosticId: "overview-audit-events" });
|
||||
}
|
||||
}, []);
|
||||
|
||||
const refreshAll = useCallback(() => {
|
||||
void refreshCore();
|
||||
void refreshMetrics();
|
||||
void refreshUsage();
|
||||
void refreshProviders();
|
||||
void refreshSignals();
|
||||
}, [refreshCore, refreshMetrics, refreshProviders, refreshSignals, refreshUsage]);
|
||||
}, [refreshCore, refreshMetrics, refreshProviders, refreshUsage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialState) {
|
||||
@@ -139,14 +125,14 @@ export function HomePage({ session, onNavigate, initialState }: HomePageProps) {
|
||||
return [...counts.entries()].map(([serverType, count]) => ({ serverType, label: serverType, count }));
|
||||
}, [core]);
|
||||
|
||||
const overviewSignals = useMemo<PlatformOverviewSignal[]>(() => buildOverviewSignals(core, providers, signals), [core, providers, signals]);
|
||||
const overviewSignals = useMemo<PlatformOverviewSignal[]>(() => buildOverviewSignals(core, providers), [core, providers]);
|
||||
const jobs = core.status === "ready" ? jobBuckets(core.data.jobs) : null;
|
||||
const endpointSummary = core.status === "ready" ? summarizeEndpointOperations(core.data.endpoints) : null;
|
||||
const onlineCount = core.status === "ready" ? core.data.instances.filter((item) => serverIsOnline(item.state)).length : 0;
|
||||
const offlineCount = core.status === "ready" ? core.data.instances.length - onlineCount : 0;
|
||||
const activeProviders = providers.status === "ready" ? providers.data.filter((item) => item.status === "active").length : 0;
|
||||
const errorProviders = providers.status === "ready" ? providers.data.filter((item) => item.status === "error").length : 0;
|
||||
const moduleFailureCount = [core, metrics, usage, providers, signals].filter((module) => module.status === "error").length;
|
||||
const moduleFailureCount = [core, metrics, usage, providers].filter((module) => module.status === "error").length;
|
||||
const canManageServers = session.capabilities.includes("servers.manage");
|
||||
|
||||
const metricAverages = useMemo(() => {
|
||||
@@ -293,7 +279,7 @@ export function HomePage({ session, onNavigate, initialState }: HomePageProps) {
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<ProductionGovernancePanel compact title="生产容量与告警" />
|
||||
<ProductionOperationsPanel compact title="生产容量与告警" />
|
||||
|
||||
<section className="overview-two-col">
|
||||
<article className="console-panel" aria-label="resource usage">
|
||||
@@ -342,15 +328,14 @@ export function HomePage({ session, onNavigate, initialState }: HomePageProps) {
|
||||
<div className="panel-header">
|
||||
<h2><MoonStar size={16} /> 最近运营信号</h2>
|
||||
<div className="panel-header-actions">
|
||||
<span className="page-status">{moduleFreshnessLabel(signals.status === "ready" ? signals.refreshedAt : signals.refreshedAt)}</span>
|
||||
<button type="button" className="icon-command" onClick={() => void refreshSignals()}><Sparkles size={14} /> 刷新审计</button>
|
||||
<span className="page-status">{moduleFreshnessLabel(core.status === "ready" ? core.refreshedAt : providers.status === "ready" ? providers.refreshedAt : undefined)}</span>
|
||||
<button type="button" className="icon-command" onClick={() => { void refreshCore(); void refreshProviders(); }}><Sparkles size={14} /> 刷新信号</button>
|
||||
</div>
|
||||
</div>
|
||||
{signals.status === "loading" && core.status === "loading" && <LoadingState label="正在收集信号…" compact />}
|
||||
{signals.status === "error" && <ErrorState title="审计信号不可用" reason={signals.reason} diagnosticId={signals.diagnosticId} onRetry={() => void refreshSignals()} compact />}
|
||||
{core.status === "loading" && providers.status === "loading" && <LoadingState label="正在收集信号…" compact />}
|
||||
{providers.status === "error" && <ErrorState title="AI 提供商信号不可用" reason={providers.reason} diagnosticId={providers.diagnosticId} onRetry={() => void refreshProviders()} compact />}
|
||||
{overviewSignals.length === 0 && signals.status === "ready" && core.status === "ready" && providers.status === "ready" ? (
|
||||
<EmptyState title="暂无异常信号" description="最近没有故障、失败任务或需要关注的审计事件。" />
|
||||
{overviewSignals.length === 0 && core.status === "ready" && providers.status === "ready" ? (
|
||||
<EmptyState title="暂无异常信号" description="最近没有故障、失败任务或 AI 提供商异常。" />
|
||||
) : (
|
||||
<div className="signal-list">
|
||||
{overviewSignals.map((signal) => (
|
||||
@@ -374,8 +359,7 @@ export function HomePage({ session, onNavigate, initialState }: HomePageProps) {
|
||||
|
||||
function buildOverviewSignals(
|
||||
core: OperationsModuleState<OverviewData>,
|
||||
providers: OperationsModuleState<AiProviderResponse[]>,
|
||||
signals: OperationsModuleState<AuditEventResponse[]>
|
||||
providers: OperationsModuleState<AiProviderResponse[]>
|
||||
): PlatformOverviewSignal[] {
|
||||
const collected: PlatformOverviewSignal[] = [];
|
||||
if (core.status === "ready") {
|
||||
@@ -391,11 +375,6 @@ function buildOverviewSignals(
|
||||
collected.push({ id: `ai-${provider.id}`, kind: "aiProvider", summary: `AI 提供商 ${provider.name} 连接异常`, detail: "LLM 辅助暂不可用,请在提供商管理中检查已保存配置。", targetPage: "aiProviders", targetId: provider.id, tone: "warning", at: "" });
|
||||
}
|
||||
}
|
||||
if (signals.status === "ready") {
|
||||
for (const event of signals.data.slice(0, 5)) {
|
||||
collected.push({ id: `audit-${event.id}`, kind: "log", summary: event.summary || `${event.action} ${event.resourceKind}`, detail: `${event.resourceKind}/${event.resourceId}:${event.result}`, targetPage: event.resourceKind === "server-instance" ? "servers" : "maintenance", targetId: event.resourceId, tone: event.result === "failure" ? "warning" : "info", at: event.createdAt });
|
||||
}
|
||||
}
|
||||
return collected.sort((left, right) => Date.parse(right.at || "") - Date.parse(left.at || "")).slice(0, 8);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Activity, ListChecks, RotateCcw, ServerCog, Sparkles, WandSparkles } from "lucide-react";
|
||||
import { ListChecks, RotateCcw, ServerCog, Sparkles, WandSparkles } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { platformApiClient } from "../api/client";
|
||||
import type { AuditEventResponse, JobResponse, RunEndpointResponse, ServerInstanceResponse } from "../api/types";
|
||||
import type { JobResponse, RunEndpointResponse, ServerInstanceResponse } from "../api/types";
|
||||
import { EmptyState, ErrorState, LoadingState, ResultBadge } from "../components/StateViews";
|
||||
import { ProductionGovernancePanel } from "../components/ProductionGovernancePanel";
|
||||
import { ProductionOperationsPanel } from "../components/ProductionOperationsPanel";
|
||||
import { jobCapabilityLabel } from "../contracts/jobPresentation";
|
||||
import type { PageComponentProps } from "../contracts/page";
|
||||
import { cx } from "../utils/classes";
|
||||
@@ -13,7 +13,6 @@ type ModuleState<T> = { status: "loading" } | { status: "error"; reason: string
|
||||
|
||||
export function MaintenancePage({ session, operations, onNavigate }: PageComponentProps) {
|
||||
const [endpoints, setEndpoints] = useState<ModuleState<RunEndpointResponse[]>>({ status: "loading" });
|
||||
const [events, setEvents] = useState<ModuleState<AuditEventResponse[]>>({ status: "loading" });
|
||||
const [jobs, setJobs] = useState<ModuleState<JobResponse[]>>({ status: "loading" });
|
||||
const [servers, setServers] = useState<ModuleState<ServerInstanceResponse[]>>({ status: "loading" });
|
||||
const [triageResult, setTriageResult] = useState<{ status: "pending" | "succeeded" | "failed"; label: string } | null>(null);
|
||||
@@ -28,16 +27,6 @@ export function MaintenancePage({ session, operations, onNavigate }: PageCompone
|
||||
}
|
||||
}, []);
|
||||
|
||||
const refreshEvents = useCallback(async () => {
|
||||
setEvents({ status: "loading" });
|
||||
try {
|
||||
const response = await platformApiClient.listAuditEvents();
|
||||
setEvents({ status: "ready", data: response.items });
|
||||
} catch (error) {
|
||||
setEvents({ status: "error", reason: error instanceof Error ? error.message : "加载失败" });
|
||||
}
|
||||
}, []);
|
||||
|
||||
const refreshJobs = useCallback(async () => {
|
||||
setJobs({ status: "loading" });
|
||||
try {
|
||||
@@ -60,23 +49,20 @@ export function MaintenancePage({ session, operations, onNavigate }: PageCompone
|
||||
|
||||
const refreshAll = useCallback(() => {
|
||||
void refreshEndpoints();
|
||||
void refreshEvents();
|
||||
void refreshJobs();
|
||||
void refreshServers();
|
||||
}, [refreshEndpoints, refreshEvents, refreshJobs, refreshServers]);
|
||||
}, [refreshEndpoints, refreshJobs, refreshServers]);
|
||||
|
||||
useEffect(() => {
|
||||
refreshAll();
|
||||
}, [refreshAll]);
|
||||
|
||||
const endpointItems = endpoints.status === "ready" ? endpoints.data : [];
|
||||
const eventItems = events.status === "ready" ? events.data : [];
|
||||
const jobItems = jobs.status === "ready" ? jobs.data : [];
|
||||
const serverItems = servers.status === "ready" ? servers.data : [];
|
||||
const failedJobs = useMemo(() => jobItems.filter((job) => job.state === "failed").slice(0, 8), [jobItems]);
|
||||
const serverById = useMemo(() => new Map(serverItems.map((server) => [server.id, server])), [serverItems]);
|
||||
const endpointById = useMemo(() => new Map(endpointItems.map((endpoint) => [endpoint.id, endpoint])), [endpointItems]);
|
||||
const failedAuditCount = eventItems.filter((event) => event.result !== "success").length;
|
||||
const unhealthyEndpointCount = endpointItems.filter((endpoint) => endpoint.status !== "online" || heartbeatAgeMinutes(endpoint.lastHeartbeatAt) > 5).length;
|
||||
|
||||
async function retryJob(job: JobResponse) {
|
||||
@@ -121,7 +107,7 @@ export function MaintenancePage({ session, operations, onNavigate }: PageCompone
|
||||
|
||||
<div className="form-guidance maintenance-triage-intro">
|
||||
<strong>维护排障入口</strong>
|
||||
<span>从节点详情、最近失败任务和审计异常进入重试、查看相关服务器、查看日志链路,不需要直接接触 run 端。</span>
|
||||
<span>从节点详情和最近失败任务进入重试、查看相关服务器、查看日志链路,不需要直接接触 run 端。</span>
|
||||
</div>
|
||||
|
||||
<section className="maintenance-triage-grid" aria-label="维护排障入口">
|
||||
@@ -137,17 +123,11 @@ export function MaintenancePage({ session, operations, onNavigate }: PageCompone
|
||||
<strong>{jobs.status === "ready" ? `${failedJobs.length} 个失败` : "加载中"}</strong>
|
||||
<small>从失败任务进入重试、查看相关服务器和查看日志链路。</small>
|
||||
</button>
|
||||
<button type="button" className="triage-card" onClick={() => void refreshEvents()}>
|
||||
<Activity size={18} />
|
||||
<span>审计异常</span>
|
||||
<strong>{events.status === "ready" ? `${failedAuditCount} 条异常` : "加载中"}</strong>
|
||||
<small>按资源定位失败操作和平台拒绝原因。</small>
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{triageResult && <ResultBadge status={triageResult.status} label={triageResult.label} />}
|
||||
|
||||
<ProductionGovernancePanel title="容量治理与告警闭环" />
|
||||
<ProductionOperationsPanel title="容量与告警闭环" />
|
||||
|
||||
<section className="console-panel" aria-label="run endpoints">
|
||||
<div className="panel-header">
|
||||
@@ -262,53 +242,6 @@ export function MaintenancePage({ session, operations, onNavigate }: PageCompone
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="console-panel" aria-label="audit events">
|
||||
<div className="panel-header">
|
||||
<h2>审计事件</h2>
|
||||
</div>
|
||||
{events.status === "loading" && <LoadingState label="正在加载审计事件…" compact />}
|
||||
{events.status === "error" && (
|
||||
<ErrorState title="审计事件加载失败" reason={events.reason} diagnosticId="maintenance-audit" onRetry={() => void refreshEvents()} compact />
|
||||
)}
|
||||
{events.status === "ready" && events.data.length === 0 && (
|
||||
<EmptyState title="暂无审计事件" description="平台还没有记录任何审计事件。" actionLabel="刷新" onAction={() => void refreshEvents()} />
|
||||
)}
|
||||
{events.status === "ready" && events.data.length > 0 && (
|
||||
<div className="console-record-list">
|
||||
{events.data.slice(0, 30).map((event) => (
|
||||
<div key={event.id} className="console-record">
|
||||
<div className="console-record-head">
|
||||
<strong>{event.summary || `${event.action} ${event.resourceKind}`}</strong>
|
||||
<span className={cx("status-pill", auditResultClass(event))}>{event.result}</span>
|
||||
</div>
|
||||
<div className="console-record-meta">
|
||||
<span>
|
||||
事件 <code>{event.id}</code>
|
||||
</span>
|
||||
<span>操作者 {event.actorId}</span>
|
||||
<span>
|
||||
资源 {event.resourceKind}/{event.resourceId}
|
||||
</span>
|
||||
<span>{formatTimestamp(event.createdAt)}</span>
|
||||
</div>
|
||||
<div className="console-row-actions">
|
||||
<button type="button" className="theme-upload" onClick={() => void refreshEvents()}>
|
||||
查看审计链路
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="theme-upload"
|
||||
disabled={event.resourceKind !== "server-instance"}
|
||||
onClick={() => event.resourceKind === "server-instance" && onNavigate("serverDetail", { serverId: event.resourceId })}
|
||||
>
|
||||
查看相关服务器
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -355,10 +288,6 @@ function heartbeatAgeMinutes(value: string): number {
|
||||
return (Date.now() - timestamp) / 60000;
|
||||
}
|
||||
|
||||
function auditResultClass(event: AuditEventResponse): string {
|
||||
return event.result === "success" ? "status-active" : "status-error";
|
||||
}
|
||||
|
||||
function jobStateLabel(state: JobResponse["state"]): string {
|
||||
switch (state) {
|
||||
case "queued":
|
||||
|
||||
@@ -71,11 +71,11 @@ const plugin: GamePluginResponse = {
|
||||
aiPurposes: [],
|
||||
productionLifecycle: { operations: ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"], dependencyPolicy: "required", approvalRequired: ["disable", "rollback", "retire"] },
|
||||
gameClientBridge: {
|
||||
commands: [{ type: "announcement.send", title: "Send announcement", permission: "server.game-client.command", approvalLevel: "operator", payloadSchemaRef: "schemas/bridge/announcement.json", timeoutSeconds: 30, maxPayloadBytes: 4096 }],
|
||||
commands: [{ type: "diagnostic.ping", title: "Diagnostic ping", permission: "server.game-client.command", approvalLevel: "operator", payloadSchemaRef: "schemas/bridge/diagnostic-ping.json", timeoutSeconds: 30, maxPayloadBytes: 4096 }],
|
||||
snapshots: [{ type: "companion.health", schemaVersion: "1", schemaRef: "schemas/bridge/health.json", keepForSeconds: 3600, maxRecords: 24 }],
|
||||
commandRetentionSeconds: 86400,
|
||||
maxCommands: 1000,
|
||||
pages: [{ pageKey: "players", commandTypes: ["announcement.send"], snapshotTypes: ["companion.health"] }]
|
||||
pages: [{ pageKey: "players", commandTypes: ["diagnostic.ping"], snapshotTypes: ["companion.health"] }]
|
||||
},
|
||||
status: "installed"
|
||||
};
|
||||
|
||||
@@ -98,7 +98,7 @@ describe("ServerDetailPage config write approval", () => {
|
||||
|
||||
it("uses the historical terminal drawer without restoring separate raw command panels", () => {
|
||||
expect(serverDetailPageSource).not.toContain("SourceRCONCommandPanel");
|
||||
expect(serverManagementTerminalSource).toContain("queueGameClientBridgeCommand");
|
||||
expect(serverManagementTerminalSource).toContain("dispatchSourceRCONCommand");
|
||||
expect(serverDetailPageSource).not.toContain("scumManagementRCONCommandRequest");
|
||||
expect(serverDetailPageSource).toContain("<span>打开终端</span>");
|
||||
expect(serverDetailPageSource).toContain("ServerManagementTerminalDrawer");
|
||||
|
||||
@@ -398,7 +398,7 @@ function ServerDeploymentSection({ instance, deployment }: ServerDeploymentSecti
|
||||
<div className="panel-header"><h2><PackageOpen size={16} style={{ verticalAlign: "-2px" }} /> 部署定义</h2><span className="page-status">{view.mode || "未配置"} · 修订 {view.revision}</span></div>
|
||||
<p className="section-copy">服务器目录是主目录;执行目录只用于高级自定义启动,留空时继承服务器目录。路径和命令均为受保护输入,不会回显。</p>
|
||||
<div className="console-row-list"><div className="console-row"><span>服务器目录</span><strong>{view.serverRootConfigured ? "已配置" : "未配置"}</strong></div><div className="console-row"><span>高级执行目录</span><strong>{view.workingDirectoryConfigured ? "已配置" : "使用服务器目录"}</strong></div><div className="console-row"><span>启动设置</span><strong>{view.startCommandConfigured ? "已配置" : view.mode === "custom-command" ? "未配置" : "插件引导"}</strong></div>{view.latestDispatch && <div className="console-row"><span>最近 Run 调度</span><strong>{view.latestDispatch.deploymentDefinitionIncluded ? `部署定义已随任务发送 · r${view.latestDispatch.deploymentRevision} · ${view.latestDispatch.jobState}` : "未携带部署定义"}</strong></div>}{view.latestDispatch?.runConfirmed && <div className="console-row"><span>Run 执行确认</span><strong>已按 r{view.latestDispatch.deploymentRevision} 确认执行</strong></div>}</div>
|
||||
{isScumTemplate && <div className="console-row-list" style={{ marginTop: 12 }}><div className="console-row"><span>SCUM 受控模板</span><strong>{projection?.templateVersion ? `${projection.templateKey ?? "已选择"} · v${projection.templateVersion}` : "等待 Run 预检"}</strong></div><div className="console-row"><span>预检 / 扫描</span><strong>{deploymentProjectionLabel(projection?.preflightState)} / {deploymentProjectionLabel(projection?.discoveryState)}</strong></div><div className="console-row"><span>配置映射 / 健康验证</span><strong>{deploymentProjectionLabel(projection?.mappingState)} / {deploymentProjectionLabel(projection?.verificationState)}</strong></div>{projection?.failureCode && <div className="console-row"><span>失败原因</span><strong>{projection.failureCode}</strong></div>}</div>}
|
||||
{isScumTemplate && <div className="console-row-list" style={{ marginTop: 12 }}><div className="console-row"><span>SCUM 部署模板</span><strong>{projection?.templateVersion ? `${projection.templateKey ?? "已选择"} · v${projection.templateVersion}` : "等待 Run 预检"}</strong></div><div className="console-row"><span>预检 / 扫描</span><strong>{deploymentProjectionLabel(projection?.preflightState)} / {deploymentProjectionLabel(projection?.discoveryState)}</strong></div><div className="console-row"><span>配置映射 / 健康验证</span><strong>{deploymentProjectionLabel(projection?.mappingState)} / {deploymentProjectionLabel(projection?.verificationState)}</strong></div>{projection?.failureCode && <div className="console-row"><span>失败原因</span><strong>{projection.failureCode}</strong></div>}</div>}
|
||||
</article>;
|
||||
}
|
||||
|
||||
|
||||
@@ -477,7 +477,7 @@ export function UsersPage({ session, operations, initialState }: UsersPageProps)
|
||||
<ConfirmDialog
|
||||
open={confirmRemoval !== null}
|
||||
title="停用用户"
|
||||
description={`确认停用 ${confirmRemoval?.user.displayName ?? ""}?该账号将无法登录,已有审计记录会保留。`}
|
||||
description={`确认停用 ${confirmRemoval?.user.displayName ?? ""}?该账号将无法登录。`}
|
||||
confirmLabel="确认停用"
|
||||
danger
|
||||
busy={confirmBusy}
|
||||
|
||||
Reference in New Issue
Block a user