feat(servers): 新建服务器默认空闲 10 分钟自动停服,面板可调,converge 回填旧服,系统服不休眠

This commit is contained in:
Lemon-miaow committed 2026-09-24 16:12:53 +08:00
1 parent bd909d5858
commit c1796bea17
23 files changed
+453 -18

No files matched your search

+55 -3
View File
@@ -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.",
+9 -2
View File
@@ -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": "路径不合法——请使用世界目录内的相对路径。",
+9 -2
View File
@@ -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}} 小时"
}
+4
View File
@@ -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.
+4
View File
@@ -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
+2
View File
@@ -286,6 +286,8 @@ export function ServerConsole() {
currentMemory={data.javaMemory}
currentStorage={data.storageSize}
currentCpu={data.cpu}
currentIdleStopSeconds={data.idleStopSeconds}
playerCountUnknown={data.playerCountUnknown}
onUpdated={reload}
/>
)}