feat(servers): 新建服务器默认空闲 10 分钟自动停服,面板可调,converge 回填旧服,系统服不休眠
This commit is contained in:
23 files changed
+453
-18
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Settings, ChevronRight } from "lucide-react";
|
||||
import { Settings, ChevronRight, AlertTriangle } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ConfirmFooter } from "@/components/ConfirmFooter";
|
||||
import {
|
||||
@@ -25,6 +25,17 @@ import type { AutostartPolicy } from "@/lib/types";
|
||||
|
||||
const MEMORY_OPTIONS = ["2Gi", "4Gi", "6Gi", "8Gi"];
|
||||
|
||||
/** Idle auto-stop presets in seconds; "0" is Never. The server default is 600. */
|
||||
const IDLE_OPTIONS = ["0", "300", "600", "900", "1800", "3600", "7200"];
|
||||
|
||||
/** idleLabel renders an idle-stop duration the way a person says it. */
|
||||
function idleLabel(t: (key: string, opts?: Record<string, unknown>) => string, seconds: number): string {
|
||||
if (seconds <= 0) return t("idle_stop_never");
|
||||
if (seconds % 3600 === 0) return t("idle_stop_hours", { count: seconds / 3600 });
|
||||
if (seconds % 60 === 0) return t("idle_stop_minutes", { count: seconds / 60 });
|
||||
return t("idle_stop_seconds", { count: seconds });
|
||||
}
|
||||
|
||||
function policyOptions(t: (key: string) => string): { value: AutostartPolicy; label: string }[] {
|
||||
return [
|
||||
{ value: "ownerOnly", label: t("create_server_policy_owner") },
|
||||
@@ -39,6 +50,8 @@ interface EditServerForm {
|
||||
image: string;
|
||||
memory: string;
|
||||
cpu: string;
|
||||
/** Idle auto-stop seconds as a Select value; "0" is Never. */
|
||||
idleStop: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -49,6 +62,9 @@ interface Props {
|
||||
currentMemory?: string;
|
||||
currentStorage?: string;
|
||||
currentCpu?: string;
|
||||
currentIdleStopSeconds?: number;
|
||||
/** The operator cannot read the player count, so idle stop is paused. */
|
||||
playerCountUnknown?: boolean;
|
||||
onUpdated: () => void;
|
||||
}
|
||||
|
||||
@@ -60,8 +76,11 @@ export function EditServerDialog({
|
||||
currentMemory = "",
|
||||
currentStorage = "",
|
||||
currentCpu = "",
|
||||
currentIdleStopSeconds = 0,
|
||||
playerCountUnknown = false,
|
||||
onUpdated,
|
||||
}: Props) {
|
||||
const currentIdleStop = String(currentIdleStopSeconds);
|
||||
const { t } = useTranslation("servers");
|
||||
const [open, setOpen] = useState(false);
|
||||
const images = useAsync(() => api.listImages(), []);
|
||||
@@ -72,6 +91,7 @@ export function EditServerDialog({
|
||||
image: currentImage,
|
||||
memory: currentMemory,
|
||||
cpu: currentCpu,
|
||||
idleStop: currentIdleStop,
|
||||
});
|
||||
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -86,10 +106,11 @@ export function EditServerDialog({
|
||||
image: currentImage,
|
||||
memory: currentMemory,
|
||||
cpu: currentCpu,
|
||||
idleStop: currentIdleStop,
|
||||
});
|
||||
setError(null);
|
||||
}
|
||||
}, [open, currentDisplayName, currentPolicy, currentImage, currentMemory, currentCpu]);
|
||||
}, [open, currentDisplayName, currentPolicy, currentImage, currentMemory, currentCpu, currentIdleStop]);
|
||||
|
||||
function set<K extends keyof EditServerForm>(k: K, v: EditServerForm[K]) {
|
||||
setForm((f) => ({ ...f, [k]: v }));
|
||||
@@ -103,7 +124,8 @@ export function EditServerDialog({
|
||||
form.autostartPolicy !== currentPolicy ||
|
||||
form.image !== currentImage ||
|
||||
form.memory !== currentMemory ||
|
||||
form.cpu !== currentCpu;
|
||||
form.cpu !== currentCpu ||
|
||||
form.idleStop !== currentIdleStop;
|
||||
|
||||
const canSubmit = hasChanges && !submitting;
|
||||
|
||||
@@ -130,6 +152,9 @@ export function EditServerDialog({
|
||||
cpu: form.cpu.trim(),
|
||||
};
|
||||
}
|
||||
if (form.idleStop !== currentIdleStop) {
|
||||
payload.idleStopSeconds = Number(form.idleStop);
|
||||
}
|
||||
|
||||
await api.patchServer(serverName, payload);
|
||||
setOpen(false);
|
||||
@@ -265,6 +290,33 @@ export function EditServerDialog({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>{t("edit_server_idle")}</Label>
|
||||
<Select value={form.idleStop} onValueChange={(v) => set("idleStop", v)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{/* A duration set outside the presets (kubectl, converge) stays selectable. */}
|
||||
{!IDLE_OPTIONS.includes(currentIdleStop) && (
|
||||
<SelectItem value={currentIdleStop}>{idleLabel(t, currentIdleStopSeconds)}</SelectItem>
|
||||
)}
|
||||
{IDLE_OPTIONS.map((s) => (
|
||||
<SelectItem key={s} value={s}>
|
||||
{idleLabel(t, Number(s))}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">{t("edit_server_idle_hint")}</p>
|
||||
{playerCountUnknown && form.idleStop !== "0" && (
|
||||
<p className="flex items-start gap-1.5 text-xs text-amber-600 dark:text-amber-400">
|
||||
<AlertTriangle className="mt-px h-3.5 w-3.5 shrink-0" />
|
||||
{t("edit_server_idle_unknown")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"bad_subdomain": "That subdomain is invalid — use 3–32 lowercase letters, digits or dashes, and avoid reserved names.",
|
||||
"at_capacity": "The cluster is at its running-server cap — try again once a server stops.",
|
||||
"storage_immutable": "Storage size can't be changed online.",
|
||||
"bad_idle_stop": "Idle stop must be between 1 minute and 24 hours, or Never.",
|
||||
"email_taken": "That email is already verified on another account — sign in with it or use another address.",
|
||||
"mail_undeliverable": "The verification email could not be delivered — try again later, or ask the operator to check the mail relay.",
|
||||
"bad_path": "That path is invalid — use a relative path inside the world directory.",
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
"create_server_cancel": "Cancel",
|
||||
"create_server_submit": "Create",
|
||||
"edit_server_title": "Edit Server Config",
|
||||
"edit_server_desc": "Configure display name, autostart policy, image, memory, and CPU",
|
||||
"edit_server_desc": "Configure display name, autostart policy, image, memory, CPU, and idle stop",
|
||||
"edit_server_desc_long": "Updating server spec. Fields left unchanged will retain their current values.",
|
||||
"edit_server_submit": "Save Config",
|
||||
"luckperms_group_name": "Group Name",
|
||||
@@ -167,5 +167,12 @@
|
||||
"luckperms_clear_history": "Clear history",
|
||||
"edit_server_cpu": "CPU Limit",
|
||||
"edit_server_cpu_placeholder": "e.g. 1, 2, 500m",
|
||||
"owned_filter_mine": "me"
|
||||
"owned_filter_mine": "me",
|
||||
"edit_server_idle": "Idle auto-stop",
|
||||
"edit_server_idle_hint": "Stops the server after this long with nobody online, freeing memory and CPU. The next player to join wakes it.",
|
||||
"edit_server_idle_unknown": "The player count can’t be read right now, so idle stop is paused until it can.",
|
||||
"idle_stop_never": "Never",
|
||||
"idle_stop_seconds": "{{count}} s",
|
||||
"idle_stop_minutes": "{{count}} min",
|
||||
"idle_stop_hours": "{{count}} h"
|
||||
}
|
||||
@@ -35,6 +35,7 @@
|
||||
"bad_subdomain": "子域名不合法:需为 3–32 位小写字母、数字或连字符,且不能使用保留名。",
|
||||
"at_capacity": "集群的在线服务器已达上限——等一台服务器停止后再试。",
|
||||
"storage_immutable": "存储容量不支持在线调整。",
|
||||
"bad_idle_stop": "空闲停服时长须在 1 分钟到 24 小时之间,或选择“从不”。",
|
||||
"email_taken": "该邮箱已在其他账户上完成验证;请直接用该邮箱登录,或换一个地址。",
|
||||
"mail_undeliverable": "验证码邮件发送失败——请稍后重试,或联系管理员检查邮件服务。",
|
||||
"bad_path": "路径不合法——请使用世界目录内的相对路径。",
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
"create_server_cancel": "取消",
|
||||
"create_server_submit": "创建",
|
||||
"edit_server_title": "编辑服务器配置",
|
||||
"edit_server_desc": "配置显示名、自启策略、镜像、内存与CPU",
|
||||
"edit_server_desc": "配置显示名、自启策略、镜像、内存、CPU 与空闲停服",
|
||||
"edit_server_desc_long": "正在修改服务器的 spec 配置。未修改的项将保持原样。",
|
||||
"edit_server_submit": "保存配置",
|
||||
"luckperms_group_name": "用户组名称",
|
||||
@@ -167,5 +167,12 @@
|
||||
"luckperms_clear_history": "清除历史记录",
|
||||
"edit_server_cpu": "CPU 限制",
|
||||
"edit_server_cpu_placeholder": "例如 1, 2, 500m",
|
||||
"owned_filter_mine": "我"
|
||||
"owned_filter_mine": "我",
|
||||
"edit_server_idle": "空闲自动停服",
|
||||
"edit_server_idle_hint": "没人在线达到这个时长就自动停服,省下内存和 CPU;玩家下次进服时自动唤醒。",
|
||||
"edit_server_idle_unknown": "现在读不到在线人数,空闲停服暂停中,直到能读到为止。",
|
||||
"idle_stop_never": "从不",
|
||||
"idle_stop_seconds": "{{count}} 秒",
|
||||
"idle_stop_minutes": "{{count}} 分钟",
|
||||
"idle_stop_hours": "{{count}} 小时"
|
||||
}
|
||||
@@ -323,6 +323,8 @@ export const api = {
|
||||
autostartPolicy?: AutostartPolicy;
|
||||
image?: string;
|
||||
memory?: string;
|
||||
/** Idle auto-stop: 0 turns it off, else seconds empty before the stop (60–86400). */
|
||||
idleStopSeconds?: number;
|
||||
resources?: {
|
||||
cpu?: string;
|
||||
cpuRequest?: string;
|
||||
@@ -738,6 +740,8 @@ export function humanizeError(e: unknown): string {
|
||||
return t("at_capacity");
|
||||
case "storage_immutable":
|
||||
return t("storage_immutable");
|
||||
case "bad_idle_stop":
|
||||
return t("bad_idle_stop");
|
||||
// Email identity: the verified-email uniqueness index (migration 0020) plus
|
||||
// VerifyEmailOTP's guard make a second verified holder impossible; the OTP
|
||||
// relay can also refuse to deliver at all.
|
||||
|
||||
@@ -36,6 +36,10 @@ export interface ServerInfo {
|
||||
javaMemory?: string;
|
||||
storageSize?: string;
|
||||
cpu?: string;
|
||||
/** Seconds empty before idle auto-stop; 0 when the server never idles out. */
|
||||
idleStopSeconds?: number;
|
||||
/** True while the operator cannot read the player count; idle stop waits. */
|
||||
playerCountUnknown?: boolean;
|
||||
}
|
||||
|
||||
/** ServerStatus is GET /servers/{name}/status. It never carries `owned` or
|
||||
|
||||
@@ -286,6 +286,8 @@ export function ServerConsole() {
|
||||
currentMemory={data.javaMemory}
|
||||
currentStorage={data.storageSize}
|
||||
currentCpu={data.cpu}
|
||||
currentIdleStopSeconds={data.idleStopSeconds}
|
||||
playerCountUnknown={data.playerCountUnknown}
|
||||
onUpdated={reload}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user