Files
browser/platform_web/pages/ServersPage.tsx
T
2026-07-15 19:43:06 +08:00

725 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { CakeSlice, Candy, Search, Sparkles } from "lucide-react";
import { type CSSProperties, type ChangeEvent, type FormEvent, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { platformApiClient } from "../api/client";
import type { GamePluginResponse, JobResponse, RunEndpointResponse, ServerInstanceResponse, ServerMetricsResponse } from "../api/types";
import {
RuntimeTaskProgressDialog,
type RuntimeTaskDialogAction,
runtimeBuildStages,
runtimeDependencyStages,
runtimeDownloadStages,
runtimeLogStages,
runtimeUpdateStages,
useRuntimeTaskController
} from "../components/RuntimeTaskProgress";
import { UsageMeter } from "../components/OperationControls";
import { EmptyState, ErrorState, LoadingState, ResultBadge } from "../components/StateViews";
import type { PageComponentProps } from "../contracts/page";
import {
defaultServerCreateForm,
endpointLabel,
pendingJobsForServer,
pluginLabel,
type ServerCreateFormState
} from "../contracts/serverManagement";
import { filterServerCards, serverIsOnline, type ServerCardView, type ServerStatusFilter } from "../contracts/workspace";
import {
clientManagerBuildRequest,
dependencyJobRequest,
logBackfillRequest,
runDistributionGenerateRequest,
runUpdateRequest,
serverCreateRequestFromForm
} from "../schemas/serverManagement";
import { isPlatformAdmin } from "../contracts/workspace";
import { downloadArtifactReference, safeArtifactFilename } from "../utils/artifactTransfer";
import { cx } from "../utils/classes";
type ListState = "loading" | "ready" | "error";
const statusFilters: Array<{ id: ServerStatusFilter; label: string }> = [
{ id: "all", label: "全部" },
{ id: "online", label: "在线" },
{ id: "offline", label: "离线" },
{ id: "attention", label: "需关注" }
];
export function ServersPage({ session, operations, onNavigate }: PageComponentProps) {
const [listState, setListState] = useState<ListState>("loading");
const [listError, setListError] = useState<string>("");
const [plugins, setPlugins] = useState<GamePluginResponse[]>([]);
const [endpoints, setEndpoints] = useState<RunEndpointResponse[]>([]);
const [instances, setInstances] = useState<ServerInstanceResponse[]>([]);
const [jobs, setJobs] = useState<JobResponse[]>([]);
const [metrics, setMetrics] = useState<Map<string, ServerMetricsResponse>>(new Map());
const [metricsPending, setMetricsPending] = useState(true);
const [keyword, setKeyword] = useState("");
const [statusFilter, setStatusFilter] = useState<ServerStatusFilter>("all");
const [form, setForm] = useState<ServerCreateFormState>(() => defaultServerCreateForm([], []));
const [showCreate, setShowCreate] = useState(false);
const runtimeTask = useRuntimeTaskController();
const [runtimeTaskActions, setRuntimeTaskActions] = useState<RuntimeTaskDialogAction[]>([]);
const refresh = useCallback(async () => {
setListState("loading");
try {
const [pluginResponse, endpointResponse, instanceResponse, jobResponse] = await Promise.all([
platformApiClient.listGamePlugins(),
platformApiClient.listRunEndpoints(),
platformApiClient.listServerInstances(),
platformApiClient.listJobs()
]);
setPlugins(pluginResponse.items);
setEndpoints(endpointResponse.items);
setInstances(instanceResponse.items);
setJobs(jobResponse.items);
setForm((current) => ({
...current,
pluginId: pluginResponse.items.some((plugin) => plugin.id === current.pluginId) ? current.pluginId : pluginResponse.items[0]?.id || "",
runEndpointId: endpointResponse.items.some((endpoint) => endpoint.id === current.runEndpointId)
? current.runEndpointId
: endpointResponse.items[0]?.id || ""
}));
setListState("ready");
setListError("");
} catch (error) {
setListState("error");
setListError(error instanceof Error ? error.message : "加载失败");
}
setMetricsPending(true);
try {
const metricsResponse = await platformApiClient.listServerMetrics();
setMetrics(new Map(metricsResponse.items.map((item) => [item.serverInstanceId, item])));
} catch {
setMetrics(new Map());
} finally {
setMetricsPending(false);
}
}, []);
useEffect(() => {
void refresh();
}, [refresh]);
const cards = useMemo<ServerCardView[]>(
() =>
instances.map((instance) => ({
instance,
metrics: metrics.get(instance.id),
pendingJobs: pendingJobsForServer(jobs, instance.id).length
})),
[instances, jobs, metrics]
);
const visibleCards = useMemo(() => filterServerCards(cards, keyword, statusFilter), [cards, keyword, statusFilter]);
const createPending = operations.isPending("platform", "创建服务器");
function updateForm(event: ChangeEvent<HTMLInputElement | HTMLSelectElement>) {
const { name, value } = event.target;
setForm((current) => ({ ...current, [name]: value }));
}
async function handleCreate(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const operationId = operations.begin({ intent: "创建服务器", targetKind: "server", targetId: "platform", requester: session.displayName });
try {
const result = await platformApiClient.createServerWorkflow(serverCreateRequestFromForm(form));
operations.succeed(operationId, `已创建实例 ${result.instance.id},安装任务 ${result.job.id} 已派发`, result.job);
setForm(defaultServerCreateForm(plugins, endpoints));
setShowCreate(false);
await refresh();
} catch (error) {
operations.fail(operationId, error instanceof Error ? error.message : "创建失败", operationId);
}
}
async function handleQuickRuntimeAction(instance: ServerInstanceResponse, action: ServerQuickRuntimeAction) {
const defaults = quickRuntimeDefaultsForPlugin(instance.pluginId);
const intent = quickRuntimeActionLabel(action);
const operationId = operations.begin({ intent, targetKind: "server", targetId: `${instance.id}:${action}`, requester: session.displayName });
setRuntimeTaskActions([]);
let generatedRunArtifact: { artifactId: string; checksum?: string } | null = null;
let generatedClientProfile: string | null = null;
try {
let message: string;
if (action === "generate-run") {
const distribution = await runtimeTask.runTrackedTask({
title: intent,
description: quickRuntimeTaskDescription(instance, action),
stages: runtimeBuildStages,
start: async () => {
const distribution = await platformApiClient.generateRunDistribution(instance.id, runDistributionGenerateRequest(instance.id, defaults.runOs, "amd64"));
return { value: distribution, jobId: distribution.buildJobId };
},
poll: (jobId) => platformApiClient.getJob(jobId)
});
generatedRunArtifact = { artifactId: distribution.artifactId };
message = `run 二进制已构建并上传,artifact ${distribution.artifactId}`;
} else if (action === "generate-client-manager") {
const distribution = await runtimeTask.runTrackedTask({
title: intent,
description: quickRuntimeTaskDescription(instance, action),
stages: runtimeBuildStages,
start: async () => {
const distribution = await platformApiClient.generateClientManager(
instance.id,
clientManagerBuildRequest({
serverInstanceId: instance.id,
profileKey: defaults.clientProfileKey,
targetOs: defaults.clientOs,
targetArch: "amd64",
repositoryUrl: defaults.repositoryUrl,
sourceRevision: "main"
})
);
return { value: distribution, jobId: distribution.buildJobId };
},
poll: (jobId) => platformApiClient.getJob(jobId)
});
generatedClientProfile = defaults.clientProfileKey;
message = `客户端二进制已构建并上传,artifact ${distribution.artifactId}`;
} else {
message = await runtimeTask.runTask({
title: intent,
description: quickRuntimeTaskDescription(instance, action),
stages: quickRuntimeStages(action),
executeStageIndex: quickRuntimeExecuteStageIndex(action),
execute: async () => {
if (action === "download-run") {
const reference = await platformApiClient.downloadLatestRunDistribution(instance.id);
await downloadArtifactReference(reference, (artifactId, offset, limit) => platformApiClient.readArtifactContent(artifactId, offset, limit));
return `run 下载已开始,artifact ${reference.artifactId},文件 ${safeArtifactFilename(reference.filename)}`;
}
if (action === "push-run-update") {
const reference = await platformApiClient.downloadLatestRunDistribution(instance.id);
const update = await platformApiClient.pushRunUpdate(instance.id, runUpdateRequest(instance.id, reference.artifactId, reference.checksum));
return `run 更新任务已排队,job ${update.jobId ?? update.id}`;
}
if (action === "dependencies-check") {
const job = await platformApiClient.checkDependencies(instance.id, dependencyJobRequest(instance.id, defaults.probeKey));
return `依赖检查任务已排队,job ${job.id}`;
}
if (action === "dependencies-install") {
const job = await platformApiClient.installDependencies(instance.id, dependencyJobRequest(instance.id, defaults.probeKey, defaults.installPlanKey));
return `依赖安装任务已排队,job ${job.id}`;
}
if (action === "live-logs") {
onNavigate("serverDetail", { serverId: instance.id });
return "已打开服务器详情,可切换到日志页查看实时日志";
}
const job = await platformApiClient.requestLogBackfill(instance.id, logBackfillRequest(instance.id, defaults.logSourceKey));
return `历史日志回填任务已排队,job ${job.id}`;
}
});
}
operations.succeed(operationId, message);
runtimeTask.succeedTask(message);
if (generatedRunArtifact) {
const artifact = generatedRunArtifact;
setRuntimeTaskActions([
{
label: "下载 run",
kind: "primary",
onClick: () => void downloadGeneratedRun(instance, artifact)
},
{
label: "推送更新",
onClick: () => void pushGeneratedRunUpdate(instance, artifact)
}
]);
} else if (generatedClientProfile) {
const clientProfile = generatedClientProfile;
setRuntimeTaskActions([
{
label: "下载客户端",
kind: "primary",
onClick: () => void downloadGeneratedClient(instance, clientProfile)
}
]);
}
await refresh();
} catch (error) {
const message = error instanceof Error ? error.message : "运行操作失败";
operations.fail(operationId, message, operationId);
runtimeTask.failTask(message);
}
}
async function downloadGeneratedRun(instance: ServerInstanceResponse, artifact: { artifactId: string; checksum?: string }) {
setRuntimeTaskActions([]);
try {
const message = await runtimeTask.runTask({
title: "下载 run",
description: `${instance.name} 的 run 包已生成,正在打开 artifact ${artifact.artifactId}。`,
stages: runtimeDownloadStages,
executeStageIndex: 1,
execute: async () => {
const reference = await platformApiClient.openArtifactDownload(artifact.artifactId);
await downloadArtifactReference(reference, (artifactId, offset, limit) => platformApiClient.readArtifactContent(artifactId, offset, limit));
return `run 下载已开始,文件 ${safeArtifactFilename(reference.filename)}`;
}
});
runtimeTask.succeedTask(message);
} catch (error) {
runtimeTask.failTask(error instanceof Error ? error.message : "run 下载失败");
}
}
async function pushGeneratedRunUpdate(instance: ServerInstanceResponse, artifact: { artifactId: string; checksum?: string }) {
setRuntimeTaskActions([]);
const operationId = operations.begin({ intent: "推送 run 更新", targetKind: "server", targetId: `${instance.id}:push-run-update`, requester: session.displayName });
try {
const message = await runtimeTask.runTask({
title: "推送 run 更新",
description: `${instance.name} 将使用刚生成的 artifact ${artifact.artifactId} 派发 run 自更新任务。`,
stages: runtimeUpdateStages,
executeStageIndex: 2,
execute: async () => {
const update = await platformApiClient.pushRunUpdate(instance.id, runUpdateRequest(instance.id, artifact.artifactId, artifact.checksum));
return `run 更新任务已排队,job ${update.jobId ?? update.id}`;
}
});
operations.succeed(operationId, message);
runtimeTask.succeedTask(message);
await refresh();
} catch (error) {
const message = error instanceof Error ? error.message : "推送 run 更新失败";
operations.fail(operationId, message, operationId);
runtimeTask.failTask(message);
}
}
async function downloadGeneratedClient(instance: ServerInstanceResponse, profileKey: string) {
setRuntimeTaskActions([]);
try {
const message = await runtimeTask.runTask({
title: "下载客户端",
description: `${instance.name} 的客户端管理器已生成,正在创建下载引用。`,
stages: runtimeDownloadStages,
executeStageIndex: 1,
execute: async () => {
const reference = await platformApiClient.downloadLatestClientManager(instance.id, { profileKey });
await downloadArtifactReference(reference, (artifactId, offset, limit) => platformApiClient.readArtifactContent(artifactId, offset, limit));
return `客户端下载已开始,文件 ${safeArtifactFilename(reference.filename)}`;
}
});
runtimeTask.succeedTask(message);
} catch (error) {
runtimeTask.failTask(error instanceof Error ? error.message : "客户端下载失败");
}
}
const latestCreate = operations.operations.find((operation) => operation.intent === "创建服务器");
return (
<section className="servers-page" aria-labelledby="server-page-title">
<header className="page-header">
<div>
<p className="page-kicker">{isPlatformAdmin(session) ? "平台管理员" : "我的服务器"}</p>
<h1 id="server-page-title" className="page-title">
服务器管理
</h1>
</div>
<div className="action-strip">
<button type="button" className="icon-command" title="刷新服务器状态" onClick={() => void refresh()}>
<Sparkles size={16} />
<span>刷新</span>
</button>
<button type="button" className="icon-command" title="创建服务器" onClick={() => setShowCreate((current) => !current)}>
<Candy size={16} />
<span>创建服务器</span>
</button>
</div>
</header>
{latestCreate && (
<div className="inline-result-strip" aria-live="polite">
<ResultBadge
status={latestCreate.status}
label={
latestCreate.status === "pending"
? "创建中…"
: latestCreate.status === "succeeded"
? (latestCreate.message ?? "创建成功")
: `创建失败:${latestCreate.errorReason ?? "未知原因"}(诊断 ${latestCreate.diagnosticId ?? latestCreate.id}`
}
/>
</div>
)}
{showCreate && (
<form className="provider-form" onSubmit={(event) => void handleCreate(event)} aria-label="创建服务器">
<div className="form-header">
<h2>创建服务器</h2>
</div>
<div className="form-grid">
<label>
实例 ID
<input name="id" value={form.id} onChange={updateForm} placeholder="server-example-3" required />
</label>
<label>
名称
<input name="name" value={form.name} onChange={updateForm} placeholder="Example Survival #3" required />
</label>
<label>
插件
<select name="pluginId" value={form.pluginId} onChange={updateForm} required>
{plugins.map((plugin) => (
<option key={plugin.id} value={plugin.id}>
{pluginLabel(plugin, plugin.id)}
</option>
))}
</select>
</label>
<label>
运行节点
<select name="runEndpointId" value={form.runEndpointId} onChange={updateForm} required>
{endpoints.map((endpoint) => (
<option key={endpoint.id} value={endpoint.id}>
{endpointLabel(endpoint, endpoint.id)}
</option>
))}
</select>
</label>
</div>
<button type="submit" className="primary-command" disabled={createPending} title="创建服务器">
<Sparkles size={16} />
<span>{createPending ? "创建中…" : "创建并安装"}</span>
</button>
</form>
)}
<div className="server-toolbar" role="search">
<Search size={16} aria-hidden="true" />
<input
type="search"
value={keyword}
placeholder="搜索服务器名称、ID 或插件"
aria-label="搜索服务器"
onChange={(event) => setKeyword(event.target.value)}
/>
{statusFilters.map((filter) => (
<button
key={filter.id}
type="button"
className={cx("segmented-button", statusFilter === filter.id && "segmented-button-active")}
onClick={() => setStatusFilter(filter.id)}
>
{filter.label}
</button>
))}
</div>
{listState === "loading" && <LoadingState label="正在加载服务器列表…" />}
{listState === "error" && <ErrorState title="服务器列表加载失败" reason={listError} diagnosticId="server-list" onRetry={() => void refresh()} />}
{listState === "ready" && cards.length === 0 && (
<EmptyState
icon={<CakeSlice size={26} />}
title="暂无可管理的服务器"
description={
isPlatformAdmin(session)
? "平台还没有服务器实例。点击上方“创建服务器”开始,或检查运行节点状态。"
: "当前账号名下没有可管理的服务器。如果这不符合预期,请联系平台管理员为你分配服务器,或点击刷新重试。"
}
actionLabel="刷新"
onAction={() => void refresh()}
/>
)}
{listState === "ready" && cards.length > 0 && visibleCards.length === 0 && (
<EmptyState title="没有匹配的服务器" description="调整搜索关键字或状态筛选后再试。" actionLabel="清除筛选" onAction={() => {
setKeyword("");
setStatusFilter("all");
}} />
)}
{visibleCards.length > 0 && (
<div className="server-card-grid" aria-label="server list">
{visibleCards.map((card) => (
<ServerCard
key={card.instance.id}
card={card}
metricsPending={metricsPending}
onOpen={() => onNavigate("serverDetail", { serverId: card.instance.id })}
onQuickAction={(action) => void handleQuickRuntimeAction(card.instance, action)}
/>
))}
</div>
)}
<RuntimeTaskProgressDialog task={runtimeTask.task} onClose={runtimeTask.closeTask} actions={runtimeTaskActions} />
</section>
);
}
type ServerQuickRuntimeAction =
| "generate-run"
| "download-run"
| "push-run-update"
| "generate-client-manager"
| "dependencies-check"
| "dependencies-install"
| "live-logs"
| "historical-logs";
interface ServerCardProps {
card: ServerCardView;
metricsPending: boolean;
onOpen: () => void;
onQuickAction: (action: ServerQuickRuntimeAction) => void;
}
function ServerCard({ card, metricsPending, onOpen, onQuickAction }: ServerCardProps) {
const { instance, metrics, pendingJobs } = card;
const online = serverIsOnline(instance.state);
const menuButtonRef = useRef<HTMLButtonElement>(null);
const menuPanelRef = useRef<HTMLDivElement>(null);
const [menuOpen, setMenuOpen] = useState(false);
const [menuStyle, setMenuStyle] = useState<CSSProperties>({});
const closeMenu = useCallback(() => setMenuOpen(false), []);
const openMenu = useCallback(() => {
const trigger = menuButtonRef.current;
if (!trigger) {
setMenuOpen(true);
return;
}
const rect = trigger.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const menuWidth = Math.min(320, Math.max(220, viewportWidth - 24));
const estimatedMenuHeight = 232;
const left = Math.min(Math.max(12, rect.right - menuWidth), Math.max(12, viewportWidth - menuWidth - 12));
const belowTop = rect.bottom + 8;
const top = belowTop + estimatedMenuHeight <= viewportHeight - 12 ? belowTop : Math.max(12, rect.top - estimatedMenuHeight - 8);
setMenuStyle({ left, top, width: menuWidth });
setMenuOpen(true);
}, []);
const toggleMenu = useCallback(() => {
if (menuOpen) {
closeMenu();
return;
}
openMenu();
}, [closeMenu, menuOpen, openMenu]);
useEffect(() => {
if (!menuOpen) {
return undefined;
}
const handlePointerDown = (event: PointerEvent) => {
const target = event.target;
if (!(target instanceof Node)) {
return;
}
if (menuButtonRef.current?.contains(target) || menuPanelRef.current?.contains(target)) {
return;
}
closeMenu();
};
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
closeMenu();
menuButtonRef.current?.focus();
}
};
document.addEventListener("pointerdown", handlePointerDown, true);
document.addEventListener("keydown", handleKeyDown);
window.addEventListener("resize", closeMenu);
window.addEventListener("scroll", closeMenu, true);
return () => {
document.removeEventListener("pointerdown", handlePointerDown, true);
document.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("resize", closeMenu);
window.removeEventListener("scroll", closeMenu, true);
};
}, [closeMenu, menuOpen]);
const chooseQuickAction = (action: ServerQuickRuntimeAction) => {
closeMenu();
onQuickAction(action);
};
return (
<article className="server-card" aria-label={`${instance.name} 服务器卡片`}>
<div className="server-card-head">
<span>
<strong>{instance.name}</strong>
<span className="provider-id">{instance.id}</span>
</span>
<span className={cx("status-pill", statusClass(instance.state))}>{stateLabel(instance.state)}</span>
</div>
<div className="server-card-stats">
<span className="server-card-stat">
<span>玩家</span>
<strong>{formatStat(metrics?.playerCount, metricsPending, (value) => (metrics?.maxPlayers ? `${value}/${metrics.maxPlayers}` : `${value}`))}</strong>
</span>
<span className="server-card-stat">
<span>TPS</span>
<strong>{formatStat(metrics?.tps, metricsPending, (value) => value.toFixed(1))}</strong>
</span>
<span className="server-card-stat">
<span>延迟</span>
<strong>{formatStat(metrics?.latencyMs, metricsPending, (value) => `${Math.round(value)}ms`)}</strong>
</span>
<span className="server-card-stat">
<span>任务</span>
<strong>{pendingJobs > 0 ? `${pendingJobs} 进行中` : online ? "空闲" : "--"}</strong>
</span>
</div>
<div className="server-card-meters">
<UsageMeter label="CPU" percent={metrics?.cpuPercent} />
<UsageMeter label="内存" percent={metrics?.memoryPercent} />
<UsageMeter label="磁盘" percent={metrics?.diskPercent} />
</div>
<div className="action-strip" style={{ justifyContent: "space-between" }}>
<button type="button" className="icon-command" onClick={onOpen}>
<Sparkles size={14} />
<span>详情</span>
</button>
<button ref={menuButtonRef} type="button" className="icon-command" aria-haspopup="menu" aria-expanded={menuOpen} onClick={toggleMenu}>
<span>运行操作</span>
</button>
</div>
{menuOpen &&
typeof document !== "undefined" &&
createPortal(
<div ref={menuPanelRef} className="runtime-action-popover" style={menuStyle} role="menu" aria-label={`${instance.name} 运行操作`}>
{serverQuickActionGroups.map((group) => (
<section key={group.label} className="runtime-action-group" aria-label={group.label}>
<span className="runtime-action-group-label">{group.label}</span>
<div className="runtime-action-grid">
{group.actions.map((action) => (
<button key={action} type="button" className="runtime-action-item" role="menuitem" onClick={() => chooseQuickAction(action)}>
<span>{quickRuntimeActionLabel(action)}</span>
</button>
))}
</div>
</section>
))}
</div>,
document.body
)}
</article>
);
}
const serverQuickActionGroups: Array<{ label: string; actions: ServerQuickRuntimeAction[] }> = [
{
label: "运行分发",
actions: ["generate-run", "download-run", "push-run-update", "generate-client-manager"]
},
{
label: "诊断维护",
actions: ["dependencies-check", "dependencies-install", "live-logs", "historical-logs"]
}
];
function quickRuntimeActionLabel(action: ServerQuickRuntimeAction): string {
switch (action) {
case "generate-run":
return "生成 run";
case "download-run":
return "下载 run";
case "push-run-update":
return "推送更新";
case "generate-client-manager":
return "生成客户端";
case "dependencies-check":
return "依赖检查";
case "dependencies-install":
return "依赖安装";
case "live-logs":
return "实时日志";
case "historical-logs":
return "历史日志";
}
}
function quickRuntimeStages(action: ServerQuickRuntimeAction) {
if (action === "generate-run" || action === "generate-client-manager") {
return runtimeBuildStages;
}
if (action === "download-run") {
return runtimeDownloadStages;
}
if (action === "push-run-update") {
return runtimeUpdateStages;
}
if (action === "dependencies-check" || action === "dependencies-install") {
return runtimeDependencyStages;
}
return runtimeLogStages;
}
function quickRuntimeExecuteStageIndex(action: ServerQuickRuntimeAction): number {
if (action === "generate-run" || action === "generate-client-manager") {
return 3;
}
if (action === "push-run-update") {
return 2;
}
return 1;
}
function quickRuntimeTaskDescription(instance: ServerInstanceResponse, action: ServerQuickRuntimeAction): string {
const label = quickRuntimeActionLabel(action);
return `${instance.name}${instance.id}${label},通过平台 API 派发并保留可追踪进度。`;
}
function quickRuntimeDefaultsForPlugin(pluginId: string) {
const isScum = pluginId.toLowerCase().includes("scum");
return {
runOs: isScum ? "windows" : "linux",
clientOs: "windows",
clientProfileKey: isScum ? "scum-client-manager" : "client-manager",
repositoryUrl: isScum ? "https://github.com/F88888/scum_client.git" : "https://github.com/example/client-manager.git",
probeKey: isScum ? "steamcmd" : "java-21",
installPlanKey: isScum ? "install-steamcmd-linux" : "install-java-linux",
logSourceKey: isScum ? "server-log" : "latest-log"
};
}
function formatStat(value: number | undefined, pending: boolean, format: (value: number) => string): string {
if (typeof value === "number" && Number.isFinite(value)) {
return format(value);
}
return pending ? "…" : "--";
}
export function stateLabel(state: ServerInstanceResponse["state"]): string {
switch (state) {
case "installing":
return "安装中";
case "ready":
return "就绪";
case "running":
return "运行中";
case "stopped":
return "已停止";
case "failed":
return "异常";
case "draft":
return "草稿";
case "deleted":
return "已删除";
}
}
export function statusClass(state: ServerInstanceResponse["state"]): string {
if (state === "running" || state === "ready") {
return "status-active";
}
if (state === "failed" || state === "deleted") {
return "status-error";
}
return "status-disabled";
}