Skip to content

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 11 Aug 00:13
6d15f6f

安全

  • 修改密码接口现在也走限速和并发派生上限。 它校验的是同一个面板密码,代价同样是 0.1 秒单核
    CPU,之前却因为「在登录之后」而完全没有节流。结果是:拿到一个会话的人在正门被限流之后,可以拿
    这个接口当密码探测器无限次试;任何一个已登录的客户端也能靠它把 CPU 从 Minecraft 服务端手里抢走。
    现在和登录、配对共用同一份额度 —— 理由和配对接口当初共用时一样,把一扇门敲满之后换另一扇继续
    就行了。输错的当前密码会记进凭据记录,输对则清空该地址的记录。

  • 同一个客户端对同一个实例最多开 4 条控制台连接。 一条连接会在实例的日志分发上挂一个订阅、
    占两个 goroutine,并且要一直占到关闭为止 —— 按速率限制拦不住这种「持有型」开销,一个不停重连
    又不关旧连接的客户端可以慢慢把它们堆到一个不能被打扰的服务端进程上。超过上限返回 409,
    和本机终端的会话上限是同一套做法。按「客户端 + 实例」计数,所以开满一个实例的控制台不影响看
    另一台服务器。

  • 设备令牌被拒绝时会记进凭据记录(设备令牌无效)。 猜一个 32 字节随机令牌没有意义,这条
    记录真正要抓的是另一种情况:某个 app 还在拿已经被解除配对的令牌请求。从外面看这和有人在探测
    面板一模一样,而在此之前它是完全静默的。连续重复会折叠成一行并计数。

  • 登录和配对接口加了限速,密码校验加了并发上限。 校验一次密码是 210k 轮 PBKDF2,约 0.1 秒的
    单核 CPU;而 /api/auth/login 和 /api/auth/devices 都是公开的。原先两者都不限次数,于是同一台
    机器上既能无限次在线猜密码,也能靠几十个并发请求把 CPU 从 Minecraft 服务端手里抢走 —— 后者甚至
    不需要猜对密码。

    现在按客户端地址计数:连续 5 次失败后限流,每 30 秒回一次机会,返回 429 并带 Retry-After。
    只有失败扣次数,登录成功会清空该地址的记录,所以连打几个错字再输对不会被罚;被限流的请求
    约 4ms 返回,不再跑 PBKDF2。配对接口和登录共用同一份额度 —— 分开的话,把一扇门敲满之后换另一扇
    继续就行了。

    另有一道并发派生上限(约 1/4 核数,下限 2),排不上返回 503。这一道才是真正兜住 CPU 的:
    按地址限速挡不住换源地址的攻击,也挡不住所有人都从加速器同几个回源地址进来的情况。

  • 登录成功现在会记日志。 之前每一次失败的猜测都写进了日志,唯独猜对的那次什么也没留下 ——
    面板被拿下之后没有任何痕迹可查。日志同时记 remote(TCP 对端)和 client(面板判定的真实
    客户端),后者在反代或加速器后面才有区别。

  • 新增 trustedProxies 配置,用来在反代/加速器后面认出真实客户端。 限速默认只认 TCP 对端地址:
    X-Forwarded-For 是任何客户端都能写的头,无条件相信等于让攻击者每个请求换一个计数桶。但在
    Nginx、CDN 或阿里云 GA 后面,所有请求都从少数几个回源地址进来,全算一个客户端的话,一个攻击者
    就能把所有人一起锁在门外。

    在 panel.json 里列出可信代理(支持 CIDR 和单个 IP)之后,只有对端命中列表时才读
    X-Forwarded-For,并且取最右侧那个不属于可信代理的地址 —— 每一跳都往右追加自己看到的地址,
    取左边就等于把计数桶交给攻击者挑。条目写错只会在启动日志里告警并跳过,不会拦住面板启动。

新增

  • 插件的版本锁定现在在版本列表里就能点。 「锁在某一版、不要再提示更新」这个能力一直都有 ——
    插件抽屉的「设置」页有个「版本锁定」下拉框,锁上之后这个插件不再报「有更新」,批量升级也会
    跳过它。问题是没人找得到它:想停在某一版这个念头是看着版本列表时产生的,而当时唯一能做的
    事是下载和删除。现在每个已经在库里的版本展开后都有「锁定在这个版本 / 解除版本锁定」,锁着的
    那个版本行上照旧挂「已锁定」徽章。两个入口写的是同一个字段,改哪边都一样,更新策略的其余几项
    不会被顺手改掉。

  • 新建实例改成向导式页面:一步问一件事,缺什么就在那一步补上。 原来的新建是一个对话框,
    五个问题一次问完,而且默认你要的东西都已经有了 —— 核心下拉框里是核心库现有的,jar 输入框
    里填的是目录里已有的,Java 压根不问。开第十个服时这样最快,开第一个服时不是:核心库是空的,
    下拉框里一个选项都没有,唯一的出路是「去核心库下载一个」,一点,对话框关掉,刚填的全没了。

    现在是 /instances/new 上的一个页面,五步,顺序就是这些东西互相依赖的顺序:

    1. 服务端核心 —— 从核心库挑一个,或者就在这一步下载一个新的(项目、版本、构建信息
      和下载进度都在页面里,下完自动选上),或者「先不放核心」自己传 jar。
    2. Java 环境 —— 按上一步选的核心算出它要哪个 Java,逐个标「可用 / 版本过低 / 可能太新」,
      并且就在这一步装。默认选的是满足要求里最低的那个:1.16.5 挑 Java 8 而不是 21,
      因为老服务端在新 Java 上一样起不来,这个方向以前没人提醒过。
    3. 名称与位置 —— 名字按核心预填(Paper 1.21.4),目录、内存(对着本机内存给档位,超过
      八成会警告)、开机自启和崩溃重启。
    4. 服务器设置 —— 端口、MOTD、最大玩家数、难度、游戏模式、存档名和世界种子,以及
      EULA。种子和存档名放在这里是有原因的:世界一旦生成就改不动了,而它们过去只能在实例建好
      之后去「服务器配置」里赶在第一次启动前改。代理端(Velocity)没有世界也没有 EULA,这一步
      整个不出现。
    5. 确认创建 —— 把每个答案读一遍,然后按顺序做四件事:建实例、复制核心、写 EULA、写
      server.properties,一行一个状态。第一件事之后实例就真的存在了,所以后面哪一步失败都会
      单独报出来而不是回滚 —— 建好一个缺核心的实例,也好过面板悄悄把刚建的服务器删掉。

    只写你改过的 server.properties 项,其余的仍旧由服务端第一次启动时自己生成。整个过程在按下
    「创建实例」之前不碰磁盘,每一步都能往回改。

  • 新增「数据库环境」,可以像装 Java 一样把 MySQL / PostgreSQL / MongoDB 装起来并跑起来。
    半数常用插件都要数据库 —— LuckPerms 存权限、CoreProtect 存日志、Plan 存统计 —— 而「装个 MySQL」
    在此之前意味着包管理器、service 文件、root 密码和一条 GRANT 语句,全都不是 Minecraft 知识,
    却挡在勾选插件配置的前面。

    侧栏「资源库 → 数据库环境」分三页:我的数据库(建库、启停、复制连接串)、已装引擎
    (引擎是数据库程序本身,一次安装可供多个库共用)、安装引擎(下载)。整套东西归守护进程所有,
    关掉网页不影响下载,也不影响正在跑的数据库;勾了「面板启动时自动开」的库会跟着面板一起起来,
    并且排在服务端之前启动 —— 反过来的话,开机自启的服务端会连不上库,好几个插件会就此关闭自己。

    建库时端口、账号和密码都有默认值(端口自动错开,密码自动生成),建完直接给出可以粘进插件配置的
    连接串和 JDBC 地址。默认只监听回环地址:同一台机器上的服务端照常连,外面连不进来。

    几点值得先知道的:

    • 二进制都从官方或权威渠道取,能校验的都校验:MongoDB 用官方发布清单里的 SHA-256,PostgreSQL 用
      Maven 中央仓库的 SHA-1,MySQL 用官方的 MD5。后两者的强度不足以防替换,所以这一块不提供镜像
      —— 对这两个引擎来说,到发布方的 HTTPS 本身就是保证,加一层镜像等于把它去掉。
    • PostgreSQL 官方不为 Linux 提供免安装包(EDB 的二进制包如今只剩 Windows 和 macOS),这里用的是
      zonky.io 发布在 Maven 中央仓库的便携构建 —— embedded-postgres 用的就是它。
    • MySQL 在 Linux 上装的是官方精简包(60 MB 左右);ARM 机器官方没出精简包,只能下完整包(近 1 GB)。
      macOS 上的 MySQL 请用 Homebrew,官方压缩包的文件名带 macOS 版本号,面板没法可靠地拼出来。
    • MySQL 和 MongoDB 的官方二进制依赖系统里的共享库(libaio、libnuma 等)。装完面板会先跑一次
      引擎自检
      ,跑不起来就在「已装引擎」页把原始报错和对应的安装命令一并写出来 —— 包括 Ubuntu 24.04
      上装完 libaio1t64 还要补一个 libaio.so.1 软链接这种坑。跑不起来的引擎不允许建库。
    • MongoDB 官方压缩包里不带 mongosh,面板没法给它建账号密码,所以 MongoDB 的库只能监听回环地址。
    • 面板以 root 运行时,数据库会降权到一个普通系统账号再启动(PostgreSQL 根本拒绝以 root 运行)。
      优先用该引擎自己的惯例账号(mysql / postgres / mongodb),没有就退而求其次。
    • 删除数据库时,数据目录默认保留,要连数据一起删得在确认框里单独勾选。
  • 服务器上的 jar 现在能直接导入插件库了,不用先下载到本地再传回去。 插件库页的「库外来源」
    一直有个「收编进库」按钮,但它只对校验和跟库里某个版本一模一样的 jar 有用;剩下的(自己编译
    的、朋友发的、从备份还原来的)点下去只会得到一句「先在 XX 的插件页上把它导入插件库」—— 而那个
    按钮并不存在:面板里的「导入 jar」是从你自己电脑上传,可文件明明已经在服务器上了。

    现在这条路补上了:面板直接在实例目录里把 jar 读出来、算校验和、作为一个版本存进插件库。实例
    目录里的文件一动不动,还是原来那一份在跑。进库之后它和下载来的插件一样 —— 能装到别的服、能在
    跨实例总览里比版本、能回滚;唯一的区别是它没有上游,所以永远不会有更新提示。

    两个入口:插件库页的「收编进库」现在两种 jar 都能收(认得出的记账,认不出的导入);实例插件页
    每一行的 ⋯ 里,「接管」和「导入插件库」按情况二选一出现。

  • 实例插件页现在读得懂每个 jar 里的 plugin.yml,点插件名打开抽屉看全部信息。 面板过去只从
    没装过的 jar 里挖名字和版本,剩下的字段读完就扔;面板自己装的那些干脆不读——记录里有版本号,
    看起来就够了。可记录里没有的恰恰是最要命的一条:这个插件要哪些前置。一个插件明明躺在
    plugins/ 里却没跑起来,十次里有八次是前置没装,而这件事页面上一个字都没有。

    现在每个 jar(自己传的和面板装的一视同仁)都会读一遍描述文件:插件名、版本、简介、作者、
    API 版本、前置依赖、软依赖。简介和依赖列表是散文和列表,塞进表格只会被截断或者把行高撑坏,
    所以放进右侧抽屉——点行首的插件名打开。抽屉里的依赖会跟这台服现有的插件比一遍,逐条标
    已装 / 没装,不用自己回去数。plugin.yml 里 description: | 和 > 这两种折行写法也认,
    以前这种写法等于没有简介。

    支持 Bukkit / Spigot / Paper 的 plugin.yml、BungeeCord 的 bungee.yml、Velocity 和 Fabric
    的 JSON。Forge 的 TOML 还是不读——猜错了比不说更糟。读出来的结果按文件的大小和修改时间缓存,
    轮询这一页不会反复去开压缩包。

  • 两个 jar 声明了同一个插件名时会标出来。 插件叫什么由 jar 里的 plugin.yml 说了算,跟文件名
    毫无关系,所以「手动传了一份新构建,忘了删旧的」在文件列表里看不出任何异常:两个文件名不一样、
    大小不一样、都好端端待在 plugins/ 里。而服务端只会加载其中一个、按目录顺序挑,另一个直接拒绝,
    于是页面上显示的版本号是那个没在跑的。

    现在这样的行会整行标黄、带一个「重名 N」标记,页面顶上有一条汇总,筛选栏多一个「重名」档,
    抽屉里会把撞上的文件路径逐个列出来。只算启用着的 jar——.disabled 的那个服务端根本不加载,
    停用本来就是解决重名的办法之一。

  • 主机监控新增网络流量:上行、下行两条曲线,外加自面板启动以来的累计收发。 一台服卡,先看的三个数
    一直只有两个 —— CPU 和内存。而 Minecraft 是个每秒往每个在线玩家推几十 KB 的东西,带宽先到顶在人
    多的服上比 CPU 先到顶常见得多,表现出来就是所有人一起橡皮筋、方块拉丝,而面板上 CPU 和内存都好端端的,
    于是排查方向从一开始就是错的。

    采样和 CPU 走同一个五秒的节拍,存在同一个环形缓冲里,所以关掉网页也在采,重新打开就是完整的
    三十分钟。只统计真正出得去的网卡:回环(本机上服务端和数据库之间的往来,它从没离开过这台机器)
    不算,docker / veth / 网桥 / 隧道 / 流量镜像设备也不算 —— 这些设备上的字节最终还会在物理网卡上再数
    一遍,全加起来会得到两三倍于真相的数字。到底在数哪几块网卡,就写在图表标题旁边。

    累计量从面板启动算起,不是从开机算起:内核的计数器是自开机以来的总数,一台跑了半年的机器上,
    那个数回答的不是任何人问过的问题。

  • 实例列表新增「导入现有目录」:把机器上已经有的服务端目录直接接管过来。 面板原先只会新建 —— 目录
    是它挑的,核心是它复制的。可是相当一部分人是先有服务器、后有面板:手动跑了一年的生存服,或者从别的
    面板搬过来的一堆目录。这些人过去只能新建一个实例、把目录字段改成已有路径、再去启动设置里手填 jar
    名,而这条路上没有任何一步会告诉你「这个目录里到底有什么」——填错了要等到启动失败才知道。

    现在选一个目录,面板就地读一遍并把看到的东西摆出来:哪个 jar 像是服务端(按名字认 Paper / Purpur /
    Fabric / Forge / Velocity 这些,认不出就取最大的那个,所以插件 jar 不会被当成核心)、有哪些世界存档、
    插件和模组各多少个、server.properties 里的端口和 MOTD、EULA 同意了没有。名字默认取目录名,确认之后
    面板只往自己的实例表里写一条记录 —— 目录里的世界、插件、配置一个字节都不动,不复制核心,也不改写
    配置。

    唯一会被拒绝的是已经有实例指着的目录:两个实例指向同一个世界,一起开服会把存档写坏。这一条在
    对话框里就说清楚,创建接口本身也拦(它原先不拦,手填路径就能撞上)。⌘K 里也能直接叫出这个对话框。

  • 实例控制台改成跑在伪终端上,并且是默认。 原先服务端的 stdout 是一根管道,这带来两个躲不开的
    限制:服务端能看出没有终端,于是 JLine 不提供补全、TerminalConsoleAppender 不上色;而按行扫描
    意味着没有换行的输出根本不会出现——区块预生成、Forge / NeoForge 安装器画的进度条,还有
    Continue? [y/N] 这类提示,在面板上都是一片空白,看着像服务器卡死了。

    现在默认给服务端开一个 pty,输出以原始字节送到 xterm.js,键盘直接喂给服务端自己的行编辑器。
    于是 Tab 补全由正在运行的那个服务端回答——插件命令、命令方块参数、真实在线玩家名、版本
    差异全部自动正确,不再是面板手写的那份命令表;进度条来多少显示多少;颜色不用再强制;折行按
    你浏览器窗口的真实宽度(多个浏览器同看一个服务器时取最窄的那个)。

    代价是终端只有一条流,stderr 不再单独标红(内容仍然完整记录,只是无法区分)。需要这个区分
    的实例可以在「启动设置 → 控制台」里关掉终端模式,连同原来的命令输入框、面板自带的补全和 ↑↓
    历史一起回来。Windows 没有 ConPTY 实现,所有实例仍以管道模式运行,开关是灰的;Linux / macOS
    上万一开不出 pty,会回落到管道并在控制台里说明,而不是拒绝启动服务器。

    行视图并没有被丢掉:同一份字节会再抽成行进环形缓冲,所以 /logs?since=、Done (x.xxs)! 就绪
    识别和日志导出都照旧,\r 重绘在日志里只保留最后一次的结果而不是每一个百分比。
    终端模式下 -Dterminal.jline=false -Dterminal.ansi=true 不会被加上——前者恰好会关掉终端模式
    最想要的那个补全。

  • 「设置 → 登录记录」 —— 面板收到的地址,以及最近的登录、配对和限流事件,不用再 SSH 上去翻
    journald。上半页是当前这次连接的客户端地址和 TCP 对端;配了 trustedProxies 时两者会不同,
    页面会说清哪个是代理回源地址(防火墙要放行的那个)、哪个是真实客户端。

    下半页是面板启动以来的事件表:登录成功/失败、设备配对/解除、修改密码、被限流。连续重复的同类
    事件折叠成一行并标出次数 —— 否则一次限流就能把其他记录全挤出去,而那恰恰是最需要看清历史的时候。
    失败记录里的用户名是对方输入的,页面上明说了它不代表这个账号存在。

    只存在内存里,面板一重启(包括自动更新)就清空,和会话一样。它是方便查看的视图,不是审计
    底账:需要长期留存的记录仍然在系统日志里,那边一条都没少。这样也不会多出一份需要保护的凭据轨迹
    文件,或者悄悄把磁盘写满。

    用加速器的话请注意:这一页显示的是已经连过的地址,而回源节点通常有一批。它适合用来验证
    防火墙白名单对不对,不适合用来生成白名单。

  • 面板可以自己跑 HTTPS 了:-tls-cert / -tls-key。 原先面板只会说明文 HTTP,加密这件事必须
    由反代代劳。反代仍然是有域名时的最佳选择(证书能自动续期),但它覆盖不了几种情况:手上已经有
    证书(acme.sh 签的、公司内部 CA 发的、云厂商送的)、内网自建 CA、或者机器只能用 IP 访问因而
    永远签不出公网证书。

    两个参数必须成对出现,只给一个直接报错退出 —— 半套配置多半是打错了,静悄悄退回明文 HTTP 比报错
    糟糕得多。证书在绑端口之前加载,路径写错会立刻失败并指出是哪个文件,不会等服务器都起来了才发现。
    最低 TLS 1.2。证书续期后需要重启面板重新加载。

  • 插件管理 —— 侧栏多了一个「插件库」页,实例里多了一个「插件」标签。分工是刻意的:
    插件的添加、下载来源、版本和更新全部归全局的插件库管,实例只能拿来用、换版本、停用。
    如果每个服都能自己下载,同一个插件很快就会有六份说不清区别的副本,而最想回滚到的那个旧版
    永远是被就地覆盖掉的那个。

    全局:定义来源、拉版本、看更新。 填一个 GitHub 仓库(EssentialsX/Essentials,或者
    直接把浏览器地址栏里的仓库地址粘进去),面板就从它的 Release 里拉 jar,走的和面板自己更新
    同一条路,镜像源也跟着「设置 → 面板更新」里的那个走(默认 https://ghfast.top/,镜像不通
    自动回落 GitHub 直连)。一个 Release 挂了好几个 jar 时,默认跳过 -sources、-javadoc、
    -dev 这类附带包,在剩下的里挑最大的那个 —— 装错成 sources 包的后果是服务器正常启动、
    插件却完全不工作,这种问题最难查。不满意可以填个文件名通配(EssentialsX-*.jar)自己指定。
    预发布版默认不列。

    每个版本单独存一份,放在 data/plugins/<插件>/<版本>/。很多插件每次发布都叫同一个
    Foo.jar,按文件名存新版会直接盖掉旧版,回滚时就没东西可回了。检查更新是手动点的
    (整体或者单个),结果缓存在 registry.json 里,卡片上写清上次检查的时间 —— 匿名 GitHub API
    一小时只有 60 次配额,页面自动去刷会把它花在没人看的地方。

    实例:只管用哪个。 从库里挑插件和版本装进去,拿到的是自己的副本,所以删掉库里的插件
    不会动到已经装上的服。换版本是一次操作,新 jar 落地后旧的才删,不会出现同一个插件两个版本
    同时躺在 plugins/ 里害得服务器起不来;本来是停用状态的插件换完版本还是停用的,不会被
    悄悄打开。停用就是把 jar 改名成 .jar.disabled(Bukkit / Spigot / Paper / Velocity /
    BungeeCord 都只加载 *.jar),插件自己的配置目录一动不动,重新启用就是改回来。
    增删和启停都要重启服务器才生效,正在跑的时候界面会明说。

    不是面板装的 jar 也会列出来(自己传的、从备份恢复的),标成「自行放入」,可以停用和删除,
    只是没有版本和来源信息 —— 装作没看见只会让服务器上多一个谁也说不清的插件。Fabric / Forge
    的模组把「安装目录」填成 mods 就行,同一套机制。

    下载归守护进程管,关掉网页也会继续,重开页面接得上进度;先写 .part 再改名,失败、取消或
    断网都不会留下一个看起来能用的半截 jar。删实例时它的插件记录一起清掉。

    页面分两层:侧栏的「插件库」是一张列表 —— 二十个插件里哪个有更新、哪个没人用、一共占多少
    磁盘,一眼扫完;点进去才是那个插件自己的页面,版本、上游发布、来源设置和删除都在那里。挑版本、
    看更新说明、决定回滚到哪一版,本来就是一次对着一个插件做的事,挤在一堆卡片里只会互相干扰。
    访问令牌和下载源不在这两个页面里,它们搬去了「设置 → 插件源」:那是配好一次就不再动的东西,
    没必要天天挡在插件列表上面。

    下载源可以挑。 和 Java 运行时一样给了一组镜像:ghfast.top、gh-proxy.com、
    github.moeyy.xyz、GitHub 直连,也可以填自己搭的代理前缀。默认是自动,按顺序挨个试;
    选定了某一个,它不通也会自动回落到 GitHub 直连 —— 代理挂掉该是重试一次,而不是装不上插件。
    下载完成后任务条会写明这一次实际是从哪儿下的。镜像只搬 jar:版本列表和更新检查始终直连
    api.github.com(这些代理不代理它),私有仓库的 jar 更是只走认证过的 API。

    实例里的插件会自动认出来。 不是面板装的 jar(自己传的、从备份恢复的、以前手动放的),
    现在会读它自己的描述文件 —— plugin.yml / paper-plugin.yml / bungee.yml / velocity-plugin.json /
    fabric.mod.json —— 按服务器启动时读到的那个名字和版本显示,而不是文件名。文件名和插件名对不上
    是常事(build-42-shaded.jar 是谁?)。要是这个 jar 和插件库里某个版本逐字节一致
    (按下载时记的 SHA-256 比对),还会多一个「接管」按钮:点了之后它就是一个普通的受管插件,
    能换版本、能跟更新,磁盘上什么都不动。只认内容不认文件名 —— 靠名字猜,等于让面板声称它知道
    这台服务器在跑哪个版本,而它并不知道。

    私有仓库也能用。 自己写的插件发在自己的私有仓库里,不必为了让面板读到就把仓库开放:
    在插件库页面填一个 GitHub 访问令牌(fine-grained 令牌给目标仓库 Contents: Read-only,
    classic 令牌勾 repo 即可),剩下的不用管 —— 哪个仓库是私有的由面板自己问 GitHub,
    检查更新和下载前都会核一次并按实际情况纠正,检查和下载都会带上认证走 GitHub API。
    (来源上那个「私有仓库」的勾还在,但它是显示和兜底用的:没配令牌时问不出结果,答案只能由人来填。)
    私有仓库的 jar 只能从 API 取(公开下载链接对它无效),因此不走下载镜像 ——
    镜像是第三方,既没有你的令牌,也不该知道这个仓库的存在。令牌只发给 api.github.com,
    存在 panel.json(0600,和密码哈希同一个文件),并且只进不出:接口只回答「配没配」
    和末尾四位,任何地方都读不回完整令牌,填错了就用新的覆盖掉。公开仓库配了令牌也有好处 ——
    匿名 API 每小时 60 次的配额会变成 5000 次。

    信任边界说清楚:GitHub Release 没有上游发布的校验和可以对,所以这里只校验体积、记录
    下载到的 SHA-256,信任链落在 GitHub 的 TLS 和你填的那个仓库的写权限上。这一点和 Java 运行时
    (Adoptium 给 SHA-256)、服务端核心(PaperMC 给 SHA-256)不一样 —— 那两个的镜像换不掉内容,
    这个不行。

变更

  • 侧栏「资源库」按搭建顺序排:Java 环境 → 服务端核心 → 插件库。 原来是核心排在最前面,
    而一台服真正的装配顺序是反过来的:先有 Java 才跑得起核心,先有核心才谈得上插件。第一次建服
    的人顺着这一组从上往下点一遍,正好就是该做的事的顺序;核心排第一则会让人在没有 Java 的机器上
    先下载一个跑不起来的 jar。命令面板里这三项的顺序读的是同一份定义,一起跟着变了。

  • 插件市场里「下哪个平台的构建」挪到了它所属的版本下面。 一个版本号常常对应好几个 jar ——
    LuckPerms 的一次发布同时有 Paper、Velocity、Fabric、Forge 四个构建,装错了根本加载不起来。
    选择器一直都在,只是单独放在版本列表下面:列表列 12 个版本,选择器就被推到了屏幕之外,
    于是版本行上写着「3 个平台构建」,而能对这句话做点什么的地方在没人滚到的位置。现在点开哪个
    版本,构建就展开在哪一行下面 —— 挑版本和挑构建本来就是同一个决定的两半。

  • 抽屉变宽了,并且跟着屏幕走。 插件详情抽屉原先固定 520px、插件库抽屉固定 740px,都是
    「刚好塞得下」的宽度:版本行要放版本号、日期、体积、游戏版本列表和构建数,构建卡片要放平台名
    和兼容性徽章,在 520px 下每一样都在换行。现在两者都以原尺寸为下限随视口增长(分别到 700px 和
    1040px 封顶,免得抽屉盖住它本来要让你看着的那个列表),手机上仍然是整屏。

  • 实例插件页的操作列换成了真正的按钮,版本列挪到了插件名旁边。 操作列过去是五个下划线文字
    链接,哪几个出现取决于这一行的状态,于是每行的按钮起止位置都不一样,整列对不齐,而且看着也
    不像能点的东西。现在固定是两个按钮加一个 ⋯:常用的两个(配置、停用/启用,出问题的行则是
    安装依赖、查看日志)在外面,接管、回滚、移除收进菜单——移除是不该放在顺手位置上的那种操作。
    操作列宽度固定,所以每一行的 ⋯ 落在同一个像素上。

    版本列同时从状态列右边挪到了插件名右边。插件名和版本本来是一件事——这个目录里装的是什么的哪个
    构建——中间隔着状态列,等于把一句话拆成两半分开读。

  • 核心库和 Java 环境的清单从卡片栅格改成整行的列表。 这两页放的都是「面板下载来的东西」,
    一台机器上通常只有两三个:两个核心、一个 Java。而卡片的列宽是按「一张卡最窄还能读」定的
    320px,于是在 1440px 宽的页面上,这两三样东西被排成四列里的头两格 —— 每张卡里塞着名称、
    平台、构建号、体积、加入时间、文件名、哪些实例在用它和一个删除按钮,右边却空着一大半。
    拥挤和空旷是同一件事的两面。

    现在一行一个:左边是名称和文件名(或 Java 的发行方和路径),中间构建 / 体积 / 加入于各占
    一列且逐行对齐,右边是使用中的实例和删除。宽度被真正用上,长文件名和长路径也终于有地方放。
    窄一点的屏幕(侧栏还在,行只剩七百来像素)上,三个数字合成一组一起走,把宽度还给名称;
    进入抽屉宽度(≤1024px)之后每行折回竖排——每个值都自带标签,所以折下来不丢任何信息。

  • 文件页重做:多选批量操作、当前目录筛选、按列排序,改名和删除换成面板自己的对话框。 这一页
    过去只做到「能用」:一屏行,每行三个文字链接,重命名和删除靠浏览器自带的 prompt 和 confirm。
    而那两个原生框恰恰在这一页最该说清楚的地方什么也说不出 —— 删一个文件夹时它列不出里面有什么,
    重名要等请求被打回 409 才知道,手机上弹出来的还是一个顶着浏览器域名的系统框。

    现在每行前面有勾选框,选中后顶上出现一条批量栏(下载 / 删除 / 取消选择),删除前会把要删的名字
    一个个列出来;名称、大小、修改时间三列可以点着排序,文件夹在两个方向上都排在文件前面;工具栏右端
    是一个只过滤当前目录的查找框。新建文件夹、新建文件和重命名都走面板自己的对话框,重名和斜杠在按钮
    按下去之前就会讲明白,重命名默认只选中扩展名前面那一半 —— 改的通常是那一半。新建文件建完直接进
    编辑器:建一个空文件本来就是为了往里写东西。

    每行左边多了一个按类型上色的图标(目录、jar、配置、图片各一种),改名、下载、删除收成三个等宽的
    图标按钮,所以每一行的右端都在同一个位置上;修改时间写成「3 分钟前」,精确时间在指上去时给;
    底部一行合计。png / jpg 这类图片点开就地预览,不必先下载到本地再打开 —— server-icon.png 是这里
    最常被看一眼的文件,为此下载接口多了一个 inline=1,只对光栅图生效,并且带 nosniff。

  • 文本编辑器加了行号、Ctrl / ⌘ + S 保存和「撤销修改」,有未保存的改动时关标签页会拦一次。
    把人送到这个框里来的,通常是控制台上一句以「at line 42」结尾的报错。

  • 版本号退回 0.x.x,快照号改成两位加后缀。 面板还在开发阶段,功能和配置格式都没有稳到
    能拿 1.0 去承诺兼容性的程度,这个号先还回去:1.0.0 → 0.0.0、1.1.0 → 0.1.0、
    1.2.0 → 0.2.0、1.3.0 → 0.3.0,更新日志和文档里的版本号一并改了。

    快照的口径也跟着换了。以前快照叫「下一个补丁版」(1.3.0 之后是 1.3.1-snapshot.86),
    现在叫「下一个次版本」,并且只写两位 —— 0.3.0 之后的快照是 0.4-snapshot.86。0.x 阶段
    发版走的是次版本号,补丁位基本用不上,0.3.1-snapshot.* 等于替一个根本不打算发的版本占位。
    少写一位不影响排序:缺的那一位按 0 算,0.4-snapshot.86 仍然稳稳落在 0.3.x 之后、0.4.0
    之前,正式版通道看不见它,快照通道在 0.4.0 发布时自动更新过去。

    后缀里的分隔符仍然是点(-snapshot.86,不是 -snapshot-86)。语义化版本只把点分开的纯数字段
    按数值比大小,写成 -snapshot-86 就并成了一整段字母数字,只能按字典序比 —— snapshot-9 会
    排到 snapshot-86 后面,提交数一过两位,快照通道就再也更新不动了。

  • 收起侧栏的按钮挪进了侧栏,顶栏左上角那个位置改成真正的「返回上一级」。 那个位置上原来是一个指向
    左边的尖角 —— 浏览器、手机、文件管理器都在同一个角上用同一个形状表示「回去」。于是它被当成返回键点,
    点完侧栏收起来了,页面一动没动。现在折叠控制在它所折叠的那根柱子的底部(图标条状态下变成展开箭头,
    [ 快捷键照旧),空出来的角上是返回:一次上一级,跟着面包屑走。

    从实例的某一页返回先回到该实例的控制台,再按一次才离开这个实例 —— 两次而不是一次,是故意的:
    另一种做法是同一个按钮有时移动一页、有时把你所在的整个上下文丢掉,而屏幕上没有任何东西说明这次
    是哪一种。离开一个作用域时播的是进入时那段动画的倒放,和侧栏里的「返回上级」完全一样,因为它就是
    同一件事。抽屉布局(窄屏)下侧栏本身在屏幕外,那个角仍然是汉堡按钮。

  • 实例里切换分区时,主内容区有过渡了。 分区页一旦打开就不会被卸载(这样来回切不用重新拉一遍文件
    列表、不丢滚动位置),代价是它们的出场动画只在第一次创建时跑过一次;而「切过去」这个动作原先是把
    上一页 hidden 掉、把下一页显示出来,同一帧完成,中间没有任何东西。更早的一版试图补一个入场动画,
    但那个类是在浏览器画完一帧之后才挂上的 —— 于是新页面先以最终形态闪了一下,动画才从头开始,
    看起来更像卡了一下而不是切了一页。现在离场的那一页会盖在新页上淡出九十毫秒,新页从下面升上来,
    入场标记在这一帧之内就位。

  • 面板更新改成两步:下载和停服同时进行,服务器全部停稳之后才替换二进制。 原先是串行的 —— 先把
    release 下完校验完,再去停服务器,最后重启。这个顺序保证了「下载失败不影响正在运行的服务器」,代价
    是两段等待被排成一队:一个几十兆的包在慢线路上要一两分钟,一个大世界存盘也要一两分钟,而这两件事
    之间没有任何依赖。

    现在第一步把它们并起来跑:请求一发出,下载开始的同时每个服务器收到 stop 并开始存档。第二步在两件
    事都完成之后才开始,一步都不能少 —— 面板是这些进程的父进程,替换二进制之后的 exec 不会继承任何一个,
    所以「服务器还活着就换文件」正是原来的顺序要避免的事。任何一步失败都不替换文件,并且把这次停掉的
    服务器重新拉起来
    :更新没发生却把服务器留在停止状态,那不是失败的更新,那是一次停机。更新期间
    启动服务器的请求会被拒绝(返回 409 并说明原因),否则它会被算成「已停止」而漏进恢复列表,几秒后被
    重启连坐。

    更新页现在把两件事分开报:进度条走下载,下面一行走服务器 —— 3/5 已停止 · 正在等待 生存服 存档退出,
    下载先跑完时进度条会改成「下载完成,正在等服务器停好」。

  • 实例的六个分区页铺满宽度。 除控制台外,其余五个页面(监控、文件、已装插件、服务器配置、实例设置)
    都被限制在 880px 的阅读宽度里 —— 那个宽度对一段说明文字是对的,对一张图表、一份文件列表、一列插件行
    就只是把两侧的空间白白空掉。现在它们和控制台一样吃满面板宽度(上限仍是 1440px,超宽屏不会拉出一行
    三千像素的配置项)。

    同时,一屏更宽不等于把输入框拉更长:设置类面板改成按列排布,server.properties 的其余配置项按
    320px 一列自动分栏,长文本框和目录选择器仍占整行。原先要滚三屏的配置页现在一屏能看完。

  • 服务端核心、Java 环境、插件库各自拆出二级导航。 这三页原先都是一个页面里摞三件事:已有的东西、
    可下载的目录、以及管下载的设置。三件事的频率完全不同 —— 货架每周看、目录每月翻、镜像源一年选一次 ——
    而你最常要的那一件(已经有什么)被压在另外两件下面。现在它们是各自的页面,挂在侧栏对应条目下面的
    二级导航里:服务端核心分「核心库 / 下载核心」,Java 环境分「已安装 / 安装新版本 / 下载源」,插件库
    分「插件列表 / 插件源」。

    「面板设置 → 插件源」因此搬到了「插件库 → 插件源」,和它管的那些插件放在一起;旧链接会自动跳到
    新位置。

    这三项和实例、主机、面板设置一样是整根侧栏替换:点进去,被点的那一行飞到顶上变成作用域标题,
    它的几个页面从下面升上来,标题下面是「返回上级」。中途试过让它们在父条目下面展开成一条缩进的二级
    导航,那是这个面板里唯一一处「第二层」的画法 —— 同一件事有两种形状,而展开的那一种恰好是没有任何
    动作
    就出现的:你点了一下,侧栏里多出三行字,没有东西告诉你刚才发生了什么。一种形状、一段动画、
    一个出口。

  • 全面补齐过渡动效:所有能按的东西都有按下反馈,所有会出现的东西都会消失。 面板原先只有一套
    时长(140ms)和一条曲线,无论是指针下的一次变色、抽屉滑出还是整页切换都跑同一个数。更要命的是
    动画都是单向的:菜单、对话框、⌘K 面板都会优雅地升起来,然后在两帧之间凭空消失 —— 这读起来不像
    「关掉了」,像渲染出了故障。而绝大多数控件(列表行、卡片、标签页、版本按钮、快捷指令)连按下
    状态都没有,点下去到请求回来的几百毫秒里,界面什么也不说。

    现在动效有一套四级的时长阶梯(90 / 140 / 220 / 300ms),选哪一级取决于这个动作是什么,而
    不是它移动了多远;曲线分成进场、退场、数据驱动三条。在此之上:每个可按的控件都会在指针下沉一
    下再弹回;每个会入场的浮层都有对应的出场(useDismiss 让组件比「决定关闭」多活一次退场动画);
    列表按 26ms 的步长依次落位;状态点、状态胶囊、徽章改颜色时是渐变而不是跳变;<details> 展开时
    高度会滚动展开,而不是把下面的内容瞬间顶下去半屏。侧栏折叠成图标条时,宽度是动画收拢的,文字
    先淡出再离开文档流。

    另外三处需要 JavaScript 才能做的:主题明暗切换走 View Transition 整页交叉溶解(没有这个 API 或
    用户要求减少动效时就退回瞬间切换);实例的六个分区面板因为从不卸载,改由 React 在切换时打一次
    入场标记;侧栏作用域切换见下条。所有动效都尊重 prefers-reduced-motion —— CSS 那半由样式表
    统一关掉,JavaScript 那半自己读这个偏好,因为定时器和 WAAPI 动画不受样式表管辖。

  • 点进某台服或主机时,被点的那一行会飞到侧栏顶部变成作用域标题。 侧栏在三个作用域之间整体替换
    (见下条),这个结构是对的,但切换是一刀切的:上一帧还是一列服务器,下一帧就是控制台/文件/监控,
    中间没有任何东西把「你点的那一行」和「它变成的那个标题」连起来。于是每进一次服,你都要重读一遍
    侧栏才知道自己在哪 —— 而重读导航正是「卡顿感」的定义。

    现在被点的行会飞上去落成标题,它上方的内容整体上移淡出、下方的内容整体下移淡出,让开它要去的
    位置,二级导航再从下面依次浮起。返回时完全反向播放同一段动画:标题缩回它原来那一行。两端靠共享
    的 data-nav-key 配对,所以进和出是同一份代码,谁都不需要知道自己是哪个方向。抽屉形态下不播
    (整根侧栏本来就要滑出屏幕),要求减少动效时也不播。

  • 面板设置的四个子 Tab 移进侧栏,成为它自己的作用域。 「已配对设备 / 登录记录 / 插件源 /
    面板更新」原先是页面顶部的一条标签栏,这让面板设置成了唯一一处无法从侧栏直达的目的地:你得
    先导航到一个页面,再在页面里导航一次,而落在四个里的哪一个取决于你上次停在哪。四个条目的第二套
    导航,压在一套本来就有相应形状的导航底下。现在它和实例、主机一样是一个作用域,四个分区是侧栏
    条目,每一个都有自己的地址。(Tabs 组件随之没有了调用方,一并删掉。)

  • 侧栏里的「插件 / 模组」改叫「插件库」。 页面自己的标题一直就是「插件库」,导航却叫另一个
    名字。

  • 导航改成三个作用域,侧栏在它们之间整体替换。 面板里其实有三类东西,生命周期差着数量级:
    面板自己和它的共享库存、单台服务器、底下那台机器。原先它们挤在一张扁平的侧栏里,实例还整份
    列在上面 —— 三十台服的时候,导航被挤到折叠线以下,而你要找的那一台也不会正好在最上面。

    现在全局侧栏是 概览 / 实例(所有实例 + 最近开过的 3–5 个)/ 资源库 / 系统;进入某台服或
    主机之后,侧栏整个换成那个作用域的导航,顶部固定「← 返回上级」和当前实体(名字 + 状态点)。
    没有第三层嵌套,控制台也就不再是二级页面。资源库 和 系统 是两个独立分组,以后要对子用户
    隐藏,是两次判断而不是六次。

    代价是侧栏只放得下几台服,所以补了 ⌘K / Ctrl+K 命令面板:实例和页面在同一张列表里搜,
    ↑↓ 选、↵ 打开。实例作用域顶部的实体切换器点开的也是它。

  • 控制台从标签页升成实例首页。 打开一台服直接就是驾驶舱:身份与电源栏(状态、核心、Java、
    运行时长、连接地址带复制)、三张指标卡(CPU / 内存 / 运行时长,都带最近 5 分钟走势)、
    然后是日志,日志拿走剩下的全部高度,宽屏时右边配一列快捷命令。控制台要是被塞进二级页面,
    管理员会改用 SSH,面板就被绕过去了。

  • 电源按钮改成状态机,危险操作的确认文案写清楚后果。 停止时只显示「启动」,运行时是
    「重启 + 停止」,启动/停止过程中全部禁用并显示进度 —— 不再是两个按钮一半灰着让人自己判断。
    「强制结束」永远在 ⋯ 里。确认框不再问「确定要停止吗」,而是点名这台服、说明它已经运行多久、
    在线玩家会掉线;强制结束则写明「上一次自动保存之后的世界改动全部丢失」。

  • 首页重做,回答的是「有没有出事」和「进哪台服」。 顶部四个数(运行中 X/Y、本机内存、
    磁盘剩余、待处理告警),然后是告警区 —— 崩溃、磁盘不足、内存超分配、可更新,每条都带一个
    直达修复位置的入口,没有告警时整块不渲染。实例卡片按「异常 → 运行中 → 正常停止」排,
    运行中的显示内存条和 CPU,停下的显示核心、Java 和停止原因。原先按创建顺序排,崩掉的那台
    会埋在中间。

  • 新增「主机」作用域(单机版,所以没有节点列表,侧栏直接进这一台):

    • 内存给三个数 —— 物理总量、实际占用、已分配(运行中各实例 -Xmx 之和),用双层进度条
      画。能不能再开一台服看的是「已分配」,而这个数在单一百分比下完全不可见;超分配之后内核
      会挑一个进程杀掉,表现为服务器「无缘无故」消失。超了会在首页顶起告警。
    • 磁盘按告警级别处理,剩余低于 15% 在首页顶起红色告警,并给出直达的清理入口(各实例的
      文件管理器、核心库、插件库、Java 环境)。磁盘写满会让存档保存失败并损坏世界文件。
    • 实例分布:每台服占了多少额度,一眼看完。
    • 网络、每核 CPU 这类诊断指标只放在这里,不上首页。
  • SSH 终端划进主机作用域,并且和游戏控制台明显区分开。 侧栏里用分隔线隔开、配锁图标;
    页面顶部常驻 user@host 身份条,配色另起一套(冷色,面板其余部分是暖色)。两者越像,越容易
    把命令敲进错的那个 —— 而其中一个是整机 shell。开关也从「面板设置」搬到了「主机 → 节点配置」,
    因为它是这台机器的属性,不是面板的。

  • 插件的完整安装路径搬进实例页。 「已装插件」里现在可以直接添加一个新的 GitHub 来源、
    拉一个版本下来、装进这台服,不用先跳去全局资源库 —— 人产生安装念头的时候一定在某台服里。
    全局插件库还在,它的价值是共享缓存和批量升级,不是日常安装路径。

  • 列表页的筛选条件落进 URL(/instances?q=…&state=…),刷新保留、链接可以直接发给别人。
    表格行高 48px,1024px 以下转成卡片。

  • 手机断点从 720px 提到 1024px。 服主经常是在外面收到告警才打开面板,手机上优先保留
    状态、电源、控制台;指标卡横向滚动。

  • 磁盘占用率改成用「剩余/总量」算,不再直接用采集器给的 percent。两者不总是一致(文件系统
    有保留块),而同一个页面上写着「已用 21%」又写着「只剩 12%」,读者唯一能得到的信息是这页不可信。

  • 界面骨架重做:多了一条常驻的顶栏,侧栏可以收起,手机上侧栏改成抽屉。

    顶栏上写着你在哪(所有实例 / 生存服 / 控制台,实例那一级还带状态点)—— 每个页面都会把自己的标题
    滚出视野,翻到文件列表第三屏时,屏幕上原本已经没有任何东西还在回答「这是哪个服」。
    主题切换、修改密码、退出登录一并搬了上来:它们本来压在侧栏最底下,那既是最后才会看的地方,
    也是抽屉一关就没了的地方。

    侧栏可以折叠成一条 64px 的图标栏(点顶栏左边的按钮,或者按 [,状态记在本机)。
    240px 在小屏笔记本上接近两成宽度,而控制台和文件管理器恰恰是越宽越好用的两个页面。
    折叠后导航项只剩图标,实例只剩名字首字,状态点缩到首字的右上角,「有更新 / 下载中」的
    角标缩成一个不带字的圆点 —— 数字可以不看,但「有事情在等你」不能没了。

    窄屏(≤1024px)的侧栏不再是顶部的一条横幅,而是盖在页面上的抽屉,点遮罩、按 Esc、
    或者选中任何一项都会关上,焦点跟着抽屉进出。原来那条横幅最高能占到 45% 的视口,
    手机上打开控制台,还没看见第一行日志就已经用掉半个屏幕。同时页面高度改用 dvh,
    底部不会再被收起的地址栏吃掉一截,左右和底部都留出了刘海屏的安全区。

  • 实例列表按状态排序:崩溃的排最前(列表里唯一在要求你做点什么的东西),然后是活着的,
    最后是自己停掉的;同一组内保持创建顺序,不会有服务器在你瞄准的时候从指针底下挪走。
    这个顺序现在用在每一处列出服务器的地方 —— 首页卡片、「所有实例」、主机的实例分布。

  • 实例头部只留一组按钮,重启和强制结束的位置见上面的电源状态机。
    原来四个按钮并排,强制结束和启动一样大、隔着两个按钮的距离 —— 这是丢世界的一种方式。

  • 控制台在宽屏(≥1280px)旁边多一列:快捷命令和运行状态。「服务器卡了,是内存还是主线程」
    是一边读日志一边问的问题,原来要离开日志去「资源」页,等翻回来那几行已经滚没了。
    现在指标卡就在日志正上方,曲线还在一次点击之外。窄屏完全不渲染这一列。

  • 页面容器收敛:原来并存四种页面外壳(.page 880px、.page--wide 不限宽、
    .settings 820px,外加设置页自己另开的滚动容器),四种宽度和两种滚动归属散在 CSS 里,
    新写页面挑错了看起来还差不多对。现在只有一个 Page 组件和一个决定:文字,还是卡片。
    顺带给宽页面也封了顶(1440px)—— 2560 的屏幕上把图表拉满并不会得到一张更好的图表。

  • 可访问性:侧栏和面包屑改成真正的链接,⌘/Ctrl+点击可以在新标签页里开一个服,
    右键能复制链接,鼠标悬停时状态栏也会显示目标地址(普通左键仍然走前端路由,不会整页刷新)。
    标签页补上 role="tablist" 语义,左右方向键和 Home / End 可以切换,只有当前标签在 Tab
    焦点序列里 —— 不用连按六次才能到达内容。新增「跳到主内容」链接(Tab 第一站,聚焦才显示)。
    标签条被截断时按截断的那一侧渐隐:原来滚动条是隐藏的,「右边还有两个标签」和
    「一共就这四个标签」在屏幕上长得一模一样。

修复

  • 手机上「服务器配置」的每一个输入框都比装着它的卡片宽。 表单面板的栅格写的是
    minmax(340px, 1fr):340px 是「一个输入框最窄还能填」的下限,在宽屏上它决定要不要分栏,非常对;
    但在 390px 的手机上,卡片扣掉边框和内边距只剩 316px,一条下限 340px 的轨道比它自己的容器还宽
    24px,于是每个输入框的右边框都挂在卡片外面。改成 minmax(min(340px, 100%), 1fr) —— 有 680px
    就分两栏,没有就老老实实收到容器有多宽。server.properties 那张长表(minmax(320px, 1fr))
    是同一个毛病,一起改了。

  • 手机上「插件列表」看不见插件名。 这张表是逐级丢列的:1300px 丢「上游最新」和「部署」,
    820px 再丢「状态」。但最后剩下的四条轨道里有三条是固定宽度(勾选框、库内最新、两个按钮),
    在 390px 上它们加起来就已经吃掉了全部宽度,唯一可伸缩的插件名列被压到 2px —— 一列图标,
    没有一个名字。现在 560px 以下再丢一档「库内最新」,把宽度还给名字;版本在这一行自己的抽屉里,
    名字不在别处。

  • 1200px 宽的窗口下,「插件列表」最后一列挂在卡片外面。 断点写的是 1100px,但表格量的不是
    窗口宽度 —— 是窗口减掉 240px 的侧栏再减掉内容边距之后剩下的那 946px。七条轨道要 976px,
    于是 1100 和 1240 之间整段都在溢出。断点提到 1300px。

  • 文件页在手机上把空目录的那句提示裁掉了半句。 .data-table 的单元格是 white-space: nowrap
    —— 对文件名是对的,对「这个目录是空的。把服务端 jar 或插件拖进来就能开始。」就成了一句跑出
    屏幕的话。同一页脚上「所有操作都被限制在这个目录内」也被隔壁那条长路径的省略号整句吃掉了;
    路径和这句话现在是两个元素,只有路径会被截短。

  • 实例卡片上,名字一长,右边的状态就被挤成两行。 卡片头是一行 flex:圆点、名字、状态。
    名字没有 min-width: 0 就不肯换行,于是去挤旁边那个四个字的状态标签,「已停止」被折成
    「已停」「止」。名字现在可以收缩换行,状态不换行,并对齐名字的第一行。

  • 「下载核心」页顶上那枚写着存放路径的标签会横着冲出屏幕。 标签自己有 360px 的封顶,但装着
    它们的那一行没有,而封顶里的 100% 量的正是这一行 —— 一行按内容撑到 609px,封顶也就跟着
    失效。现在这一行本身不许超过页面宽度。

  • 手机上实例控制台的指标条一进页面就偏左 14px。 那条指标是刻意做成贴着屏幕两边横向滚动的
    (负外边距加同样大的内边距),但 scroll-snap 的对齐基准是内边距盒,第一块指标于是把自己
    对到了 x=0,整条带着 14px 的滚动量出现,比下面的控制台窄了一格。补上 scroll-padding 之后
    停在该停的地方。

  • 侧栏顶部「你正在哪个作用域」那一行的图标比下面的导航图标窄 2px,整列左边缘对不齐。 装图标
    的盒子写死 16px,而里面的图标是 18px —— 既挂在盒子外面,又把标题文字往左推了两像素。

  • 侧栏折叠成图标条时,三个分组标题被切成半截字漏在分隔线上。 折叠后的标题应该只剩一条横线,
    它的高度确实设成了 0,但 overflow: hidden 裁的是内边距边缘而不是内容边缘,于是上下各 6px
    的内边距把 12px 高的字留在了外面。内边距改成外边距之后,0 就真的是 0。

  • 插件抽屉里的「3 / 12」计数在手机上从斜杠处断成两行。

  • 插件版本里每个 jar 那一行在手机上挤成四列。 校验和 96px、平台徽章和体积 80px、一个「删除」
    —— 文件名只剩七十来像素。窄屏下改成堆叠,顺序不变。

  • 插件市场下载「一个版本发好几个平台 jar」的插件,拿到的永远是游戏服那个构建,页面也从不提还有别的。
    LuckPerms 一个版本同时发 Bukkit、Velocity、Fabric 好几个 jar,它们是同一个版本的不同文件,装到
    对不上的服务端根本不会被加载 —— 于是「我下了个 LuckPerms,Paper 能装,Velocity 装不了」。

    面板内部一直是按「一个 release 挂多个构建」建模的,安装、升级、插件库的补下别的平台构建都认这件事;
    缺的只有市场抽屉这一步:勾了参照服务器时会按它的核心挑,没勾就静默取排序第一个(游戏服的构建),
    而且全程没有一个字提到这里存在一个选择。旁边那个「仅下载到库」更糟 —— 它连勾好的参照服务器都不看,
    永远下第一个,偏偏它的措辞让人以为「先囤着,回头装哪都行」。

    现在版本行会写明「N 个平台构建」,选中的版本下面多一栏「构建」:一个平台一个按钮,各自标着大小、
    是否已经在库里,以及按这个 jar 自己的元数据判定的兼容徽章。两个下载按钮走的都是这个选择,下完
    的提示也会说清楚下的是哪个构建。默认仍然按参照服务器的核心预选,没有参照服务器时才回落到第一个,
    并在旁边直说这一点。

  • 多平台插件的兼容徽章判的是 release,不是你要下的那个 jar。 一个 release 的 loaders 字段是它所有
    构建的并集,所以「LuckPerms 兼容你的 Velocity 代理端」是对的,而「你正要下的这个 Bukkit 构建兼容」
    不是 —— union 一旦当成单个文件的说明书,绿色徽章就正好出现在最不该出现的地方。市场抽屉现在为每个
    带平台标记的构建单独算一份判定(jar 自己的声明 → 它的平台 → release,依次回落),下载按钮和它旁边
    的提示也跟着这份判定走,选中一个装不上的构建会直接说「上面挑一个对得上的构建」,而不是让人去版本
    列表里找一个并不存在的问题。

  • 打开「插件市场」不用再等几秒才看到精品栏 —— 那一栏现在是存下来的答案。 精品栏是一份手写的
    插件清单,内容由代码里那十一个 id 决定;面板过去每次开页面都拿着这十一个 id 现去 Modrinth 和
    SpigotMC 读一遍,读完才画得出来。而且是两遍:页面拿到服务器列表之后会自动勾上一台服作为
    兼容性的参照,勾上就等于换了一次筛选条件,于是同一次打开要问上游两轮。这一栏里没有一个数字是
    有人盯着看它变的(名字、图标、支持的版本、下载量),却让每一次打开都空等一次跨国请求。

    现在读一次存六小时。过期之后先把存着的那一份给你,新的在背后取 —— 所以除了第一个人,谁都
    不会再等上游;而面板启动时就先读了一次,所以那个第一个人通常也不存在。同时打开的几个请求合成
    一次上游调用,不再一人一轮。

    版本列表照旧不缓存:那是按下安装的那一刻现读的,也只有那一刻,一个旧答案会让面板装错文件。

    上游出问题不会把这一栏清空:新的一次读失败就继续显示旧的那一份,只是把重试的时间往后推一分钟。
    只读到一半(一个源答了、另一个没答)只存十分钟,缺的那几行会自己回来,不用等下次重启;一个都
    读不到就只记一分钟 —— 记一下是为了让读不到上游的面板不必在每次开页面时都空等一遍超时,一分钟
    又短到把网络修好之后,人还在这一页上就能看到它填上。

  • 顶栏面包屑左边那个返回按钮,现在回到你上一个页面,而不是路由树上的上一级。 它待在浏览器、
    手机和文件管理器都用来放「返回」的那个角上,所以它得是那个意思 —— 撤销你上一步 —— 而不是往
    层级上爬。两者多数时候一致,因为多数时候你就是从上一级过来的;不一致的那些恰恰是爬树爬错了:
    从某个实例的「插件」页进「插件市场」,按返回被送到「插件列表」——一个你根本没去过的页面。

    上一页记在浏览器自己的历史条目上(history.state),返回也交给浏览器执行,所以栈是变短
    而不是又压一层,前进键仍然有效,刷新一次也还认得来路。原来那套「上一级」留作兜底:粘贴进来的
    链接、新开的标签页、会话的第一个页面——这些前面本来就没有上一步可撤销,此时树是唯一知道出口
    的东西。按钮的提示文字也精确到页(「返回 <实例名> · 监控」),因为上一页现在常常是同级的另一
    个页面,只报一个实例名等于什么都没说。

    顺带:点击你已经在的那个页面(侧栏里高亮的那一行)不再往历史里压一条重复记录 —— 那会让紧接
    着的一次返回看起来像是坏了,按下去什么也不动。

  • 文件列表里「操作」那一列的分割线和其他列对不齐,整列看下来是一道锯齿。 那一列的单元格用了
    .actions 这个类名,而它在别处的意思是「面板底部那排按钮」,规则里写着 display: flex。一个
    <td> 一旦变成 flex 容器就不再是表格单元格了 —— 它按行内的匿名块排版,于是自己的下边框画在自己
    内容结束的地方,而不是整行的边上,每一行都差那么几像素。现在按钮排在单元格里面的容器上,另外
    补了一条规则,让 .data-table 里的单元格不会再被这个类名拽出表格。

  • 「选择目录」弹窗被裁掉了大半,标题、路径栏和两个按钮都看不见。 目录选择器是从「新建实例」和
    「导入现有目录」里叫出来的,也就是一个对话框里再开一个对话框。对话框是 position: fixed,
    而 fixed 只有在头顶没有任何一层带 transform、filter 或 backdrop-filter 的祖先时才相对视口定位 ——
    这里两条都占了:遮罩上有背景模糊,而做完入场动画的卡片会留下一个单位矩阵,这也算数。于是这个弹窗
    是相对着叫出它的那张卡片排的版,并被那张卡片的滚动框裁掉。现在所有对话框都渲染到 <body> 上,
    头顶没有东西能困住它们。

    顺带修掉了同一条路上的另一个问题:在目录选择器的路径框里按回车(「前往」)会连带提交它身后那张
    表单,而那张表单是「创建实例」。

  • 侧栏折叠成图标条时,目录选择器里的文件名全部消失。 折叠是靠关掉一组类名实现的,而其中一个
    类名被目录列表借去用了 —— 类名是全局的,于是收起侧栏也把屏幕另一头一个弹窗里的文字一起收了,
    剩下一列没有名字的文件夹图标。折叠规则现在限定在侧栏内部,列表也有了自己的类名。

文档

  • README 重写,长文拆进 docs/,并配上真实截图。 原先的 README 有 600 多行:部署步骤、命令行
    参数、数据目录、反代配置、限速原理、设备令牌、控制台编码、发布流程,全挤在一页里。这对写的人方便,
    对第一次点进仓库的人不方便 —— 他要的第一个答案是「这是什么、长什么样、怎么装」,而这三件事分别
    埋在第 1、没有、和第 31 行往后的位置。

    现在 README 只回答那三个问题:一句话定位、特性表、界面截图、四条命令的快速开始,然后是文档索引。
    正文按读者分成六篇:部署指南、上手指南、
    功能详解、控制台、安全与访问控制、
    开发指南。内容基本是搬过去的 —— 那些解释「为什么这么设计」的段落一句没删,
    只是不再挡在装机步骤前面。

    截图是真跑出来的:三台服务器(Paper 26.2 / Paper 1.21.11 / Velocity)配着 Java 25 和 21、
    三个从 Modrinth 装的插件,控制台里是真实的启动日志和命令回显。控制台、文件管理器、插件库、
    Java 环境、核心下载、主机监控、本机终端、新建实例向导各一张,另有浅色主题和手机端。

安装

下载对应平台的压缩包,解压后直接运行:

tar -xzf hypercraft-0.4.0-linux-amd64.tar.gz
cd hypercraft-0.4.0-linux-amd64
./hypercraft -data ./data

前端已经嵌在二进制里,不需要另外部署。开机自启参考包内的
hypercraft.service。

校验

sha256sum -c SHA256SUMS.txt --ignore-missing