Skip to content

1.2.3 - #73

Merged
WillTDA merged 13 commits into
masterfrom
1.2.3
Sep 29, 2026
Merged

WillTDA merged 13 commits into
masterfrom
1.2.3

Conversation

@WillTDA

@WillTDA WillTDA commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

A patch release on top of 1.2.2, fixing what widening CI (#72) turned up: five bugs and API mismatches, four more problems found along the way, and the licence missing from the Docker image. Not versioned yet; the "Promote release to 1.2.3" commit comes last, once this is reviewed.

Fixed

  • A server killed by a signal was recorded as a clean stop. A JVM ended by a signal exits with no code, and only a non-zero code counted as a crash, so there was no crashed state, no crashed event and no auto-restart. That's how the kernel's OOM killer ends a server in a memory-limited container. It's now a crash with crashReason: "signal"; Kill and the stop timeout still count as clean stops.
  • Forge 1.12.2 couldn't be created. Craftbox insisted on a .sha256 checksum beside the installer, and Forge's Maven publishes only .sha1 and .md5 for many older ones (1.12.2, 1.14.4, 1.15.2). Forge and NeoForge now take the strongest one published, and still refuse an installer with none.
  • Game mode and difficulty did nothing before Minecraft 1.14. Those versions read them only as numbers, so a name made the server play Survival on Easy whatever was chosen. Checked on the real jars: 1.8.9, 1.12.2 and 1.13.2 fall back that way; 1.14, 1.20.4 and 26.3 read names and numbers. Craftbox now writes numbers to server.properties for those versions and keeps names everywhere else. A server that already ran with the fallback now shows the difficulty it actually played, which a save corrects.
  • A crash was forgotten when the panel restarted. Startup and shutdown reset every server to stopped, so the crash banner, reason and exit code went, and a half-finished duplicate or import looked whole. crashed now survives a restart.
  • Duplicating without the world could copy it anyway. Only the world* folders were left out, so a world under another level-name came along. Those folders are now left out too, with a guard so a level-name like .. can't point outside the copy.
  • Two servers on one port. Starting a server on a port another running server held let it crash with a BindException. See below.
  • The Docker image had no licence text. LICENSE is now at /app/LICENSE, as the AGPL asks for every copy distributed. The image's licence label, which came from GitHub's detection as AGPL-3.0, is now set to AGPL-3.0-only to match package.json; that label only takes effect on the next release's publish.

API changes

  • POST /servers/:id/start and /restart answer 409 while another server's process holds the port, naming it: Port 25565 is in use by "Survival". Stop that server, or change this one's port. Restart checks before stopping, and so does restart behind a backup. server-ip is respected, so servers bound to different specific addresses can share a port.
  • Creates (/servers, /from-modpack, /from-mrpack), /duplicate, /import, and /edit and /properties when the port changes, now return a warnings array, empty when there's nothing to say: Port 25565 is also used by "Survival". Only one of them can run at a time. Sharing a port is still allowed. /import already warned; its wording now matches.
  • POST /servers/:id/backup-schedule and /backup-retention answer 400 for a value out of range or not a whole number ('12abc', 1.5), and then save nothing. They used to drop the value and answer 200.
  • GET /servers/:id/check-upgrade on vanilla says "No build tracking for this server type.", as documented, instead of "No builds published for this version."
  • A failed download's operation message now carries payload: {token, label} beside error, like every other download message.
  • POST /login counts only failed sign-ins towards its 5 per 15 minutes; signing in five times in a row no longer locks the address out.
  • crashReason gains signal (with a null exitCode), and the reference now lists every reason and what a panel restart does to each state.

Panel changes

  • The Port field on the create page, and the Port and Duplicate fields on Settings, show a warning underneath as you type a port another server uses. Warnings from a save show as toasts on the next page.
  • A refused Start or Restart shows the 409 message in the usual error toast, and the server stays as it was.
  • The crash banner and dashboard card say when the process was killed; the banner no longer shows an empty "Exit code:", and it now shows an import failure's reason.
  • The Backups page shows the error for an out-of-range value instead of "Backup schedule saved."
  • With two downloads in flight, a failure now settles the right one, and only the page that started it shows the error.
  • Properties shows Game Mode and Difficulty as names for pre-1.14 servers too.

CI

  • The signal check (operations) and the Forge 1.12.2 start (weekly) no longer run as known issues.
  • New checks: the port warnings, refused start and refused restart with a second server (operations lifecycle); the port hints in Chrome (browser); a crash kept across a panel restart and the server page's banner after it; the backup 400s; the vanilla reason; the failed download's token (transfers); a duplicate with a named world (transfers); six sign-ins in a row (web); every start test now creates its server as Creative on Hard and checks the server kept both; unit tests for the checksum fallback and the numeric modes.
  • The start test's list check didn't know the 1.13 to 1.15 wording of the reply ("0 of a max 20").

Checked locally

Against this branch's image: API 31/31, WebSocket 14/14, Transfers 16/16, Web 23/23, Browser 10/10, Operations lifecycle 13/13 and backups 7/7, unit 48/48, and starts of vanilla, Paper, Fabric and Forge at their latest versions plus vanilla 1.12.2 and Forge 1.12.2, 1.14.4 and 1.15.2 on Java 8. Before its fix, the same duplicate steps copied all three world folders, and the new 1.12.2 start check fails on the published 1.2.2 image (gamemode=0, difficulty=1).

Notes

  • NeoForge's Maven listed only 26.3.0.33-beta on 2026-09-28, which broke every NeoForge create. It was republished whole at 10:57 UTC today, so the NeoForge jobs should pass; nothing in Craftbox changed for it.
  • The panel's scripts are cached for a day, so after upgrading, a browser may run the old ones (without the new port hints) until it fetches them again. Nothing breaks in the meantime.

A process ended by a signal exits with no code at all, and _handleClose
only counted a non-zero code as a crash, so a JVM killed from outside
was recorded as a clean stop: no crashed state or event, and no
auto-restart. That is exactly how the kernel's OOM killer ends a server
in a memory-limited container.

An exit by signal that nobody asked for is now a crash with crashReason
"signal" and a null exitCode. Kill and the stop timeout still read as
clean stops, since both mark the stop as requested before signalling.
The crash banner and the dashboard card say the process was killed,
and the banner no longer shows an empty exit code when there is none.
The API reference now lists the crash reasons.

The operations test's signal check no longer runs as a known issue.
Forge and NeoForge installers are checked against a checksum published
beside them on Maven before they run, and Craftbox insisted on the
.sha256 one. Forge's Maven has only .sha1 and .md5 for many older
installers, so 1.12.2 (14.23.5.2864), 1.14.4 and 1.15.2 servers could
not be created at all.

Both now take the strongest sidecar published: .sha256, then .sha1,
then .md5, moving on only when one is missing. Any other failure, or no
sidecar at all, still refuses the installer. The sidecar comes from the
same host as the installer either way, so what it guards against,
corruption on the way, is the same.

Unit tests cover the order and the refusals, and the weekly Forge
1.12.2 start no longer runs as a known issue.
The backup schedule and retention endpoints dropped a value they didn't
accept and still answered 200, so an API client was told a bad value
had been saved, and the Backups page said "Backup schedule saved." when
the warning countdown was set to 50 and nothing changed. parseInt also
read '12abc' as 12 and 1.5 as 1.

Both now check every field before saving any of them and answer 400
with the allowed range, which the Backups page shows as its error
toast. A whole number, as a number or a string of digits, is required;
a field left out or null still keeps its value, as the page sends null
for an empty box. The schedule's `enabled` also has to be true or
false.

The operations test's backup checks now expect the 400s, including a
request with one good and one bad value saving neither.
The upgrade check answered a vanilla server with "No builds published
for this version.", which reads as though Mojang had withdrawn the
release. Vanilla's provider has a getBuilds() that always returns null,
since a release is a single jar, so the check went looking for builds
and found none. It now answers vanilla as it does custom, with "No
build tracking for this server type.", which is what the API reference
already said. The operations test checks the reason, not just that
there is one.
A download started from the panel is matched to its outcome by the dl
token on its URL, and the API reference says every message about it
carries that token. A failure didn't: it went out through
broadcastOperation, which sent only an error string for `failed`. The
panel made do by settling the failure against the one download it had
in flight, so with two in flight a failure left both spinning, and
every open page on the server showed the error toast, not just the one
that started the download.

broadcastOperation can now send a payload beside a failure's error,
and a failed download's report carries {token, label} like the others.
The panel settles a failure by its token and ignores reports for
downloads it didn't start. The transfers test checks the token.
The login limiter allowed five attempts per 15 minutes per address and
counted every one, so signing in five times in a row, from a few
browsers or a script, locked the address out with the right password.
The API reference described the limit as five attempts, which is how
it behaved, but a limit on guessing has no reason to count sign-ins
that succeed.

Only failed ones count now. Every attempt is still counted as it
arrives, so parallel guesses are capped as before, and a successful one
is taken back off once it has answered. A success and a failure are
both redirects, so the login route marks a success for the limiter to
read rather than it going by status code.

The web test signs in six times in a row and checks none of them used
up an attempt, before the existing check that five failures lock the
address out.
Starting a server on a port another running server held let the second
JVM start, fail to bind, and die with a BindException, which Craftbox
recorded as a crash. Nothing warned when a server was created or edited
onto a port another one used either; only an import did.

Start and restart are now refused with a 409 naming the server that
holds the port. Restart checks before stopping, so a port changed while
the server ran can't leave it down, and so does restart behind a backup.
A running server's port and address are taken when its JVM is spawned,
and server-ip is respected, so two servers bound to different specific
addresses can still share a port.

Sharing a port is still allowed, as only one of them can run at a time.
Creating, duplicating or importing a server onto a port another server
uses, or moving one onto it through Settings or Properties, answers
with a `warnings` array saying so. The panel shows those as toasts
after saving, and the Port fields on the create page and on the
Settings page (Port and Duplicate) say so as the port is typed.

The operations test creates a second server on the lifecycle server's
port and checks the warnings, the refused start and the refused
restart. The browser test checks the hint beside each Port field.
Duplicating without the world removed the world, world_nether and
world_the_end folders from the copy, whatever the server called its
world. A server whose level-name had been changed kept its world in
other folders, so the copy took the whole world with it.

The copy now also leaves out the folders level-name names, with their
_nether and _the_end siblings. level-name is free text, so a name that
would point at the copy itself or outside it is skipped rather than
deleted. The API reference says what is left out.

The transfers test duplicates a server whose world is under its own
level-name, split into three folders, and checks none of them came
along. Before this change all three did.
At startup, and again at shutdown, Craftbox reset every server that
wasn't stopped to stopped, so a crash was forgotten whenever the panel
restarted: the crash banner went, and the reason and exit code no
longer showed. The reset is there for states a restart makes untrue,
like running, and crashed only fell into it because stopped was the
one state it skipped. It was worse for a duplicate or import that had
failed half way, which came back looking like a whole server that
could be started.

A crashed server now stays crashed across a restart. Auto-start still
starts one that has it on, as it did when the crash had been turned
into a stop. The crash banner also shows an import failure's reason,
as it does a failed duplicate's, and the API reference says what a
panel restart does to each state.

The operations test crashes the lifecycle server before its panel
restart and checks the crash, and the server page's banner, survive.
Before 1.14 Minecraft reads gamemode and difficulty only as numbers.
Craftbox always wrote the names, which such a server can't parse, so it
quietly played Survival on Easy whatever was chosen, and rewrote the
file with those numbers. Probed on the real server jars: 1.8.9, 1.12.2
and 1.13.2 fall back that way, while 1.14, 1.20.4 and 26.3 read the
names, and read numbers as well.

Craftbox still keeps the names everywhere else, in the server record,
the forms and the API, and writes the numbers to server.properties for
a server before 1.14: on create, a modpack install, Settings and
Properties saves. Reading the file back turns a number into its name,
so the record and the Properties page's selects show names too. A
server that already ran with the fallback now shows the difficulty it
actually played, which a save through Settings or Properties corrects.

Unit tests cover which versions get numbers and the reading back. The
start test now creates each server as Creative on Hard and checks the
server kept both, which the weekly 1.12.2 starts would have failed.
The start test checks a server answers `list`, and knew two wordings of
the reply: "0/20" before 1.13 and "0 of a max of 20" from 1.16. Minecraft
1.13 to 1.15 say "0 of a max 20", so a start of any of those failed at
that step although the server had answered. No CI run covers those
versions, which is why it hadn't shown; starting Forge 1.14.4 and 1.15.2
by hand did.
The image carried the app, its dependencies and package.json, but not
LICENSE, and the AGPL asks for its text to go with every copy
distributed, which a published image is. It now sits at /app/LICENSE.

The image's licence label came from GitHub's licence detection, which
gives AGPL-3.0. The publish workflow now sets it to AGPL-3.0-only, as
package.json declares.
@WillTDA
WillTDA merged commit 93c1355 into master Sep 29, 2026
6 checks passed
@WillTDA
WillTDA deleted the 1.2.3 branch September 29, 2026 12:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant