Conversation
Bumps [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) from 13.0.1 to 13.0.2. - [Release notes](https://github.com/WiseLibs/better-sqlite3/releases) - [Commits](WiseLibs/better-sqlite3@v13.0.1...v13.0.2) --- updated-dependencies: - dependency-name: better-sqlite3 dependency-version: 13.0.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
Bumps [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) from 8.6.0 to 8.6.1. - [Release notes](https://github.com/express-rate-limit/express-rate-limit/releases) - [Commits](express-rate-limit/express-rate-limit@v8.6.0...v8.6.1) --- updated-dependencies: - dependency-name: express-rate-limit dependency-version: 8.6.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
Bumps [sharp](https://github.com/lovell/sharp) from 0.34.5 to 0.35.3. - [Release notes](https://github.com/lovell/sharp/releases) - [Commits](lovell/sharp@v0.34.5...v0.35.3) --- updated-dependencies: - dependency-name: sharp dependency-version: 0.35.3 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com>
…/express-rate-limit-8.6.1 Bump express-rate-limit from 8.6.0 to 8.6.1
…/better-sqlite3-13.0.2 Bump better-sqlite3 from 13.0.1 to 13.0.2
…/sharp-0.35.3 Bump sharp from 0.34.5 to 0.35.3
The `statusPagePublic` flag is only consulted by the `/status` index route; individual status pages, their JSON, and the mods zip have never checked it and are reachable by anyone holding the server UUID. The docs claimed otherwise, which made the behaviour look like an access-control bug. Correct the claim and add a note explaining that the UUID is the capability token by design, so share links keep working for players without a panel account, and that scanner reports flagging these routes are false positives.
The event log Time column showed only a relative age ("5m ago"), with the
absolute time hidden in a title tooltip and formatted differently from the
rest of the panel. It now renders the canonical formatDate() output followed
by the relative age in parentheses, matching the backups, files, plugins and
account pages.
timeAgo() existed as two byte-identical private copies in events.js and
status.js; hoist it into app.js next to formatDate() and drop both. The
status page keeps its relative-only display.
Also re-tick the event times every 30s so the relative half does not go
stale on a long-open tab, and widen the column to fit the longer string.
Both dashboard upload modals left their fields editable for the duration of the transfer. On the .mrpack modal that was actively misleading: name, port, memory and the EULA checkbox are snapshotted into the request when the upload starts, so any edit made afterwards was silently discarded. Add setControlsLocked(root, locked) to app.js and apply it to both flows. It skips [data-bs-dismiss="modal"] buttons, so Cancel and the header X stay live and the existing hide.bs.modal handlers can still abort the transfer and free the server-side DGUP session. It also marks forms [data-busy], because the shared data-validate-required handler re-enables the submit button on any input/change event and import.js dispatches those itself — without the guard the lock undid itself.
…toast (#52) Sessions are a 1-hour rolling idle timeout, so a tab left open is signed out with nothing on screen saying so. The next click produced a toast reading just "unauthorized" — the raw {error:'unauthorized'} body echoed by the ~60 call sites that surface data.error — and no way forward. Handle 401 centrally in apiFetch: explain what happened via flashToast, which survives the navigation, then redirect to /login, where ensureAuth's returnTo brings the user back to the page they were on. A latch keeps concurrent calls from queueing duplicate toasts and racing redirects. Migrate the 12 remaining raw fetch call sites to apiFetch so they inherit the handling and stop hand-rolling the CSRF header. The polled stats call on the console page now doubles as a passive heartbeat, so an idle tab detects expiry without the user clicking anything. Where a call site relied on res.json() throwing to reach a catch block, that is now an explicit res.ok check. Two supporting fixes: - csrfValidate rendered a misleading "Invalid or missing CSRF token" 403 for page form POSTs from a lapsed session, since the tab carries the old session's token. Redirect those to /login instead. - The four WebSocket reconnect loops retried forever, silently, 401ing the upgrade every 30s. Browsers hide the handshake status from JS, so after three consecutive failures spend one authenticated request to tell an expired session apart from an unreachable panel.
A server could be backed up mid-provision, which zipped a half-downloaded jar and a partly-extracted mods folder, moved the state to backing_up and then to stopped while provisioning was still running, and — because the Backups page sends startAfter=true for any non-stopped state — went on to start the half-built server. Three separate defects made that reachable: setOperationalState wrote any allowed target state without consulting canTransition, so every backup, restore and jar-upgrade flow bypassed the state machine. The table already forbade provisioning -> backing_up; nothing enforced it. It now honours the table, treating a same-state write as a no-op since several failure paths set stopped defensively. A rejected transition throws with status 409 so the routes report a state conflict rather than a 500 — this is what two racing operations look like, not a server fault. Preconditions were written as `proc && proc.state`, and processes are created lazily — only on start or on a WebSocket subscribe — so a server provisioning in the background has none and the guards silently passed. Add ServerManager.getState(server), which falls back to the persisted state, and use it for the backup, restore, upgrade-jar, restart and restore-point guards. The restore endpoint had no state guard at all. The views treated provisioning as "running", so the Backups page disarmed the one guard that existed by sending stopFirst. The provisioning check is now separate and stopFirst does not bypass it. The backup scheduler had the same hole from the other direction: with no process it took its "server not running, back up directly" branch. It now skips provisioning servers with a log line, matching how it already skips when another backup is in progress, rather than relying on the transition being refused underneath it. Also close the management pages while provisioning: blockWhileProvisioning redirects Settings, Properties, Plugins, Files and Backups to the Console with an explanatory flash, the nav links render disabled, and the page reloads itself once the server leaves provisioning. Console and Events stay open.
Servers created from a modpack or .mrpack, and servers imported from a .cbx, could not be offered a jar upgrade. Three separate defects, all funnelling through check-upgrade, whose result is the only thing that renders the Upgrade Jar button. A server with no recorded build was reported as having no upgrade available. That was a dead end: upgrading is the only thing that records a build, since upgrade-jar passes a null build to the provider, which installs the newest and writes it back. Imported and duplicated servers inherit whatever build the source had, including none, and nothing else ever repairs it. Such a server now reports an upgrade as available, with a reason explaining what will happen, so the button appears and the upgrade back-fills the build. Builds were compared with `>`, which is a string comparison for the providers that report dotted versions. "21.1.100" > "21.1.95" is false, so NeoForge and Forge upgrades were detected for some build numbers and not others — the reported "sometimes checking for upgrades fixes it, sometimes not". Add compareBuilds() alongside pickPreferredBuild, comparing numerically for Paper-family integer builds and segment-wise for dotted versions. Fabric returns null from getBuilds, so check-upgrade bailed out before comparing anything and every Fabric server reported itself up to date. Most Modrinth modpacks are Fabric, and the installer stores the pinned loader version as the build, so there was a real value being ignored. Add an optional getLatestBuild() provider method and implement it for Fabric. getBuilds stays null deliberately: making it return the loader list would surface a loader picker in the create/edit UI, which Fabric intentionally does not have.
The API could upload, delete and set the environment of a mod, but could not
read what was installed. The only way to get the list under bearer auth was to
download the public status-page mods zip, which omits server-only mods and
means transferring every jar to answer a question about names.
Add two reads under /api/v1:
GET /servers/:id/plugins -> {contentType, files[]}
GET /servers/:id/plugins/environment -> {environment}
Both reuse the composition the plugins page already performs — getContentType,
listModFiles and getModEnvMap — so the API and the UI derive `environment` the
same way and cannot disagree: 'both' is the absence of a map entry, and a
disabled jar on disk is how a client-only mod is represented.
Unlike the mutating routes in this file, reads do not require the server to be
stopped, and the listing does not create the content directory the way the
page render does — a GET should not have side effects on disk.
The API could write a file but never read one. POST /servers/:id/edit-file was write-only, and the two routes that did emit file contents — the .cbx export and the backup download — lived in the browser-facing router behind session auth, so a bearer caller was redirected to /login. With an API key alone, no file on a server was reachable. Add three reads under /api/v1: GET /servers/:id/files?path= directory listing GET /servers/:id/file?path= text file contents as JSON GET /servers/:id/download?path= raw stream, any file and move the two existing binary routes into /api/v1 so a key can reach them: GET /servers/:id/export GET /servers/:id/backups/:backupId/download Moving them is transparent to the browser: ensureApiAuth accepts an existing session before it looks for a bearer token, and GETs skip CSRF, so the panel's plain <a href> links keep working — they just point at the new paths. Where those routes previously flashed and redirected on error they now return JSON, which suits both callers. Path handling matches the rest of the panel: resolve against the server directory, reject anything outside it with isPathInside (symlinks resolved), 403 on traversal. /download keeps the stopped-server requirement and the EBUSY -> 409 handling, since a running server holds handles on jars and world data. Extract the file-browser helpers to utils/fileBrowser.js. isTextFile and its extension set existed as identical copies in the web and API routers, and this added a third caller; listDirectory is lifted out of the Files page handler so the page and the API describe a directory identically rather than drifting.
Bump package.json, package-lock.json and the README badge.
A restart routes through `stopped` on its way back up: the process exits, the state is broadcast, and two seconds later the replacement is spawned. For that gap every state check said the server was stopped, so the console re-enabled Start and Delete and the API accepted both. Starting in that window was the damaging case. startServer reaches _ensureProcess, which rebuilds the process because the cached one reads as stopped, and the old object's pending respawn timer was never cancelled — so two seconds later a second JVM launched in the same directory on the same port. Deleting had the same shape: the directory went away while a respawn was still queued. Track the gap explicitly. ServerProcess raises _restarting before the stopped transition, so that broadcast already carries restarting:true, and lowers it as soon as the server leaves stopped — not when it finishes booting, or Stop and Kill would stay disabled for the whole startup, which on a large modpack is minutes. The respawn timer is now held on the instance and cleared by destroy(). ServerManager refuses start/stop/restart/kill and any operational state change while the flag is up, tagged 409 like the other state conflicts, and the delete route checks it too. The console keeps every power button disabled for the duration and no longer fires an action while one is already in flight.
Controls that need the server stopped were gated once, server-side, when the page rendered. The page then received live state over the WebSocket and updated the state badge — but nothing else. Stop a server while sitting on the Mods tab and Upload stayed dead; start one and Delete stayed live. Only the console page re-derived anything, and only its own power buttons. Add a declarative gate: data-enable-when="stopped crashed" disables a control whenever the live state falls outside that list, and data-show-when / data-hide-when do the same for the explanatory alerts that accompany one. applyStateGates walks them on a new craftbox:state event, which both WebSocket owners now dispatch alongside the badge update — mirroring the craftbox:operation event that already existed. The live state is read from the data-state attribute both owners already maintain on the nav header. Several pages rendered two entirely different versions of a control depending on state — an anchor when stopped and an id-less disabled button when running — so no amount of live toggling could have reached them. Those are now rendered once and gated by attribute. Duplicate and Save Template no longer render as either a submit button or a modal trigger; there is one button, and the submit handler decides at click time whether to act directly or offer to stop the server first. Where a decision was snapshotted into the DOM at render time — the backup modal's stopFirst/startAfter hidden inputs, the export button's data-server-stopped, the restart modal's data-server-state — it is now read from the live state when the button is actually pressed, which removes the staleness rather than trying to keep a copy in sync.
The event log was a static render — new events only appeared on reload, even though the page already holds an open socket. The public status page has done this for a while; the panel's own log had not. Events were only broadcast from three hand-written call sites in ServerProcess, covering started/stopped/crashed/restarted. Everything else — backups, jar upgrades, player joins, user actions — was written to the database and never reached a socket at all. Broadcast from logEvent instead, so every event type is live by construction, and drop the three now-duplicate broadcasts. The sink is injected at boot rather than imported, since ServerProcess already depends on eventLogger and the reverse would be circular. The payload now carries initiatedBy and playerName so a live row can render the same "Initiated By" cell as a server-rendered one, and the event id so clients can identify rows. Public sockets are unaffected: broadcast() still filters them down to the started/stopped/crashed/restarted allowlist. On the page, rows are built from the same badge/icon map the view uses, handed to the client as JSON so the vocabulary stays defined once. New rows go on top and the oldest is dropped past 500, matching the server-side prune. An active type filter suppresses events that don't match it, rather than silently contradicting the dropdown. The table and the empty state are now both always rendered so the first live event can swap between them, and the Clear button appears with it.
Console output was reachable only over the WebSocket, which rejects bearer
keys. An API-key client could send a command via POST /servers/:id/command but
had no way to see the reply — the only workaround was reading
logs/craftbox-console.log through the file endpoint, unbounded and unparsed.
GET /servers/:id/console returns recent output oldest-first as
{timestamp, line} pairs, with limit 1-1000 (default 200) and a truncated flag.
Two sources, because they genuinely differ. The log file is durable,
timestamped and survives a panel restart, so it is the default; it is
append-only and never rotated, so reads tail a bounded window from the end
rather than loading the file. The in-memory buffer is shorter and untimestamped
and is discarded whenever the process object is rebuilt, but it holds the few
[Craftbox] lines emitted after the log stream closes on exit, which never reach
disk. `auto` prefers the file and falls back to memory for a server that has
never been started on this install.
This release adds new API surface — file read/download, mod and plugin listing, and console read — plus live state and event-log updates in the panel. That is new functionality rather than a patch, so it moves to a 1.2.0 minor and enters beta rather than shipping as 1.1.1.
The Files tab could browse, download and edit text, but nothing could be put into a server directory or taken out of one. Anything that is not a .jar for a mod loader — a datapack, a world, a plugin config, a resource pack — had to be placed by hand on the host filesystem, which is not an option for anyone running the panel in Docker or on a box they do not have a shell on. Four endpoints under /api/v1, grouped like the plugins routes rather than following the older edit-file shape: POST /servers/:id/files/upload multipart + `path`, or DGUP for large files POST /servers/:id/files/mkdir POST /servers/:id/files/rename POST /servers/:id/files/delete Creating is ungated and destroying is not. Upload and mkdir work in any state, matching edit-file, which has always written into a running server's directory; requiring a full stop to drop in a datapack would make the feature useless for the case people actually want it for. Rename and delete require the server stopped, since those are the destructive pair and a running server holds open handles — a write that loses to one of those handles comes back as a 409 with the same wording /download already uses, rather than a 500. Uploads overwrite an existing file and report how many they replaced, so the outcome is visible instead of silent. A name already taken by a folder is rejected per-file, leaving the rest of the batch alone. There is no extension allowlist. An authenticated caller can already edit any text file and download the whole directory, so filtering here would be theatre; names are still reduced to a single path segment, and rename/mkdir additionally refuse characters and reserved device names that would fail confusingly at the filesystem layer — on Windows now, or after an export and import later. The page itself follows the Mods tab: picker plus full-page drag and drop, progress in the shared overlay, confirm modals, flash toast and reload. Rename and delete carry data-enable-when so they track live state over the WebSocket without any bespoke JavaScript; upload does not, since it only needs a selection. Folders get rename and delete too, with sterner confirm copy given the delete is recursive. Also adds neoforge to the content-type map in files.ejs, which serverNav has had all along.
A client-only mod lives on disk as <name>.jar.disabled. Uploading that same jar again wrote <name>.jar beside it and left the disabled copy in place, so one mod became two files — and listModFiles emits a row per file, so the Mods page listed it twice under the same name. From there it got worse rather than settling. enableOnDisk and disableOnDisk both skip their rename when the other form already exists, so the environment dropdown silently did nothing for that mod. Delete removed only whichever form it found first, so the mod reappeared on the next load. reconcileModFiles heals a pair on the next start, but only for mods carrying a client/server tag — a mod on the default `both` has no map entry, so its pair was never repaired. Fix the cause: an upload removes a disabled twin and clears the mod's environment tag, so the file it just replaced does not linger. Uploading is an explicit "put this on the server", so the mod comes back as Client and Server rather than staying tagged client-only from its previous life. Then make the two consumers robust to a pair arriving some other way — a hand-managed mods folder, or, now, a file dropped in through the file manager. listModFiles collapses both forms onto the enabled one, and delete unlinks every form it finds. Uploads also report how many files they replaced, which the response never carried, so re-uploading a mod said "1 mod uploaded" and gave no hint that anything had been overwritten. The toast now reads "1 mod uploaded, 1 replaced."
The backend of a change can be exercised over HTTP, but the things that actually broke in this cycle — a control that fails to re-enable when the server stops, an event row that renders differently live than it does from a page render, a restart window where Start is briefly clickable — only show up in a browser with a real server behind it. Collects those into TODO.txt, covering both betas: the file manager and mods fixes from beta.2, and live state gating, the live event log, the restart race and the new read endpoints from beta.1.
Adds file management to the Files page — upload, rename, delete and new folder — which was the last thing in the panel that still needed shell access to the host. Also fixes a mod being listed twice, and silently duplicated on disk, when a jar was uploaded over a disabled copy of itself. Still a beta: the file manager is new surface that writes to and deletes from server directories, and wants a run against real servers before it ships as 1.2.0. TODO.txt has the checklist.
The rename and new-folder modals added in beta.2 shipped with their confirm button always enabled, unlike every other modal in the panel. Both now start disabled and enable only once the name would actually be accepted — the check mirrors safeEntryName/newNameError, so the button no longer lights up for a name the API is about to refuse. Also marks the required fields that were missing the asterisk: Version and the JAR URL on Settings (both already starred on Create), and the name field in the rename, new-folder and group modals. Neither Settings field takes a `required` attribute — they render prefilled and go disabled while the server runs, and an existing server with a blank one would leave the shared submit gate wedging Save with no way out.
Uploading over a file the running server holds open silently overwrote it. Only Windows fails that write with EBUSY, which the handler already reported; on Linux it succeeds and corrupts a live server — during testing it replaced a running server's jar, recovered from a backup. The handler now checks server state up front and refuses to replace the jar, or any existing file under the world folders, logs/ or mods/plugins, while the server is running. Per-file rejection, so the rest of a batch still lands, and new files are unaffected — nothing holds a handle on a name that isn't there yet. The Events "Clear" button never hid itself on an empty log. The live-update JS was right; the template was not. An escaping EJS tag emitted class="d-none", which the browser reads as a class literally named "d-none", quotes and all, so it never matched. The same mistake was in eleven other places — the state hints on Settings and Properties, where it meant both halves of a running/stopped pair rendered at once, and the disabled-state tooltips on Files, Mods and Plugins, which rendered as a truncated `"Stop` plus a handful of junk attributes. All now use the unescaped tag. GET /console?limit=0 fell back to the default because `parseInt(...) || 200` cannot tell a parsed 0 from an absent value. Only a missing or unparseable value takes the default now. GET /events?limit= had the same shape and no lower bound at all, so limit=-5 reached slice(0, -5) and quietly dropped the five newest events. TODO.txt records the results of the pass, and what still needs re-testing.
Fixes the three issues the beta.2 test pass turned up. The one that matters is the file manager: uploading over a file a running server held open silently overwrote it on Linux, which is enough to corrupt a live server's jar or world. That is now refused while the server runs. Also fixes a template mistake that had been quietly breaking state-dependent markup in twelve places — the Events "Clear" button on an empty log, both halves of the running/stopped hints on Settings and Properties rendering at once, and the disabled-state tooltips on Files, Mods and Plugins. Still a beta: none of the three fixes has been re-tested in a browser yet, and the beta.2 checklist still has items that need a real Minecraft client and a fresh install to cover. TODO.txt has both lists.
WillTDA
marked this pull request as draft
August 4, 2026 22:55
Deferring an imminent scheduled backup by re-saving the schedule pushed the due time out correctly, but a Craftbox restart before that new time ran the backup anyway, on the old timing. The deferral exists only as backupSchedule.nextBackupAt, and two things conspired to lose it. Graceful shutdown cancels every schedule through stopSchedule, which deleted that stored time on the way down — so the deferral was gone before the next boot could read it. The catch-up check then never consulted it in the first place: it decided a backup had been missed by comparing the age of the last scheduled backup against the interval, a measurement the deferral is invisible to. A countdown at five minutes means the last backup is an interval-minus-five old, so any downtime past those five minutes read as overdue and fired immediately on the way back up. stopSchedule now cancels timers and nothing else. Every path that ends a schedule for real already clears the stored time itself: the schedule endpoint clears it on every save, import strips a value carried in from another instance, duplicate builds a fresh schedule without one, and delete removes the record. The catch-up check now treats the stored time as the answer to when the next backup is due — still in the future means nothing was missed, however old the last backup is. Only a record without one falls back to the old measurement, for schedules predating the field or yet to complete a cycle. A catch-up that does run clears the time it satisfied, so the new cycle starts a full interval out instead of firing again on startSchedule's one-second grace. Both writers now share one awaited helper, rather than two fire-and-forget read-modify-writes that could land in either order.
Carries a single fix: a scheduled backup deferred by re-saving the schedule ran anyway, on its old timing, the first time Craftbox restarted before the new due time. The deferral was stored in one place and shutdown deleted it, so nothing survived to tell the next boot the backup had been pushed back. Still a beta: this and the beta.3 fixes want a run against real servers before 1.2.0 ships. TODO.txt has the checklist.
The Files tab could create folders but not files, so building a config from scratch still meant writing it somewhere else and uploading it — the one gap left between the panel's file manager and a working file explorer. New Text File sits beside New Folder and opens the same dialog against a new POST /servers/:id/files/mkfile, which writes an empty file. The input opens on ".txt" with the caret in front of it, so the common case is typing a base name and pressing Enter; the extension is still fully editable for anything else. mkfile is ungated like mkdir: a name that is not on disk yet cannot be one the running server is holding open. It writes with 'wx', so an existing file is never truncated even if it appears between the check and the write, and an empty server.properties or eula.txt created in the root re-syncs the mirrored database fields the same way uploading or deleting one does. No extension allowlist, matching upload — the listing already decides editable-vs-download by extension. Both create modals now go through one function in files.js rather than a second near-copy of the first, so the name gating, Enter-to-submit and failure handling cannot drift apart later.
Both create modals tell you where the new entry will land, but an unquoted path ran into the sentence around it — a folder name can contain spaces, so "Created in My Configs." reads as prose rather than as a location. The path now appears in double quotes, which also makes a trailing space or an odd character visible. The server root has no name to quote and stays as it was.
Every server page's back arrow went to the dashboard, even when the server had been opened from a group page — leaving the group and having to click back into it to reach the next server in it. It now points at /dashboard/groups/<group> whenever the server has one, and at the dashboard otherwise. A group only exists while a server is in it, so the destination is always a page that renders.
Assign Group sits alone on the last row of Advanced Options, so a half-width field pinned to the left edge read as though a second field was missing beside it. Centring the column puts it under the gap between Memory and JVM Arguments instead. No effect below the md breakpoint, where the field is full width.
The checklist had grown into a record of the beta.1 and beta.2 passes with the beta.3 fixes annotated onto it, and beta.4 never got a section at all — so what was left unticked no longer told you what still needed testing. It is now scoped to the build in hand: the New Text File dialog and its endpoint, the quoted destination line, the Assign Group position and the back button's group destination. Rewritten each beta from here on.
Finishes the Files tab: New Text File beside New Folder, so a config can be created and edited in the panel instead of being written elsewhere and uploaded. Both create dialogs now quote the folder they are creating into. Two smaller pieces of navigation and layout with it: a server's back arrow returns to its group page rather than always the dashboard, and Assign Group is centred on Settings. Still a beta: none of it has been through a browser yet, and the checklist has been rewritten around this build. TODO.txt has the list.
Assign Group was centred on Settings by a class written into the template, while the create form does the same thing at runtime — centerLoneRowItems, which centres any row down to one visible column. Two mechanisms for one behaviour, and the template one silently stops being right the moment a second column joins that row. centerLoneRowItems moves to app.js and takes the form to scope itself to. The create form calls it exactly where it did before, on every validation pass and when the custom-JAR toggle hides the version picker; the settings form calls it once at init, since which of its columns render is decided server-side and nothing hides one after load.
Moving centerLoneRowItems into app.js put the create form's centring on shared code, so the rows it has always centred — the port field once Custom or modpack mode hides the version picker — need re-checking, not just the settings row the change was made for.
The editor's list of readable extensions had no room for the file in hand: a .jsonl or .json5 was refused outright, and the only way to change one was to download it, edit it elsewhere and upload it back. Any extension a mod invents for its own config hit the same wall, as did a file with no extension at all. Editability is now read off the first 8 KB of the file: text if it decodes as UTF-8, holds no NUL byte, and is not mostly control characters. Nobody has to add an extension anywhere for it to open. Two shortcuts sit either side of that check — always-binary extensions are refused without a read, so listing a mods folder of several hundred jars still opens nothing, and a list of known text extensions stands in when there is nothing to read, either because the path does not exist yet or because the running server holds it locked. Reading the bytes also settles the reverse case, which the extension list could not see at all: a UTF-16 or latin-1 file wearing a .txt is now refused, since the panel reads and writes UTF-8 throughout and would have shown it as mojibake and mangled it on save. .nbt and .dat go the same way for the same reason — both are gzipped binary, and both were previously offered for editing. Whole-file reads are capped at 5 MB. Both paths used to read the file into a string with no limit, which an append-only log on a running server will eventually turn into an out-of-memory or ERR_STRING_TOO_LONG. Past the cap the API takes a byte window instead, with ?tail= or ?offset=&limit=, so a log can be read while the server writing it is still running — /download cannot do that, since it needs the server stopped. A window landing mid-character is trimmed back to a whole one so no replacement characters reach the caller. The editor UI takes no window: it posts the whole textarea back, so opening a partial file would truncate the rest away on save. It refuses an oversized file and points at the download.
beta.5 shipped without a browser pass and nothing in its checklist was ever ticked, so those sections carry forward rather than being rewritten away. The new section covers the detection change, the encoding cases the extension list could not catch, and the byte windows against a running server.
Text files are now recognised by reading them rather than by matching an extension against a list, so .jsonl, .json5, a mod's own invented config extension and a file with no extension at all all open in the editor. Two consequences worth knowing before upgrading. .nbt and .dat no longer offer an Edit button — they are gzipped binary and editing them corrupted world and player data — and a file over 5 MB is no longer opened in one piece by either the editor or the API, which instead reads it in windows with ?tail= or ?offset=&limit=. Still a beta, and still unproven in a browser: this build carries beta.5's untouched checklist as well as its own. TODO.txt has both.
The Docker build fails on npm ci: the lockfile lists sharp's 25 optional platform binaries as dependencies but only carries the package entry for @img/sharp-win32-x64, so an install on linux/amd64 has nothing to resolve @img/sharp-linux-x64 against and refuses with EUSAGE. The lockfile was last written by npm 9, which prunes the other platforms' optional dependencies when it rewrites, and the audit fix that went through it took 541 lines of them with it. Regenerated with npm 11, which keeps every platform, so the lockfile installs the same on a Windows workstation and in the linux container. Still no vulnerabilities and no dependency version changes — this only puts back entries that should never have left.
Creating a file or folder named "sub/notes.txt" returned 200 and quietly made "notes.txt" in the current directory instead. Same for "../notes.txt", and the same on all three of mkfile, mkdir and rename. Nothing escaped the server directory — isPathInside still had the last word, and a traversal attempt through the `path` field was always refused — but silently creating something other than what was asked for is its own bug, and both halves of the project already said it should be refused. The browser refused a slash before the request was ever sent (nameError, public/js/files.js), and the API docs said a name "must be a single path segment". The server was the one place that did not check. The check was unreachable rather than missing: newNameError ran on the output of safeEntryName, which has already reduced a name to its last segment, so no separator ever survived to be complained about. It now takes the name as typed and runs first, with the rest of the browser's rules alongside it — length, control characters, bare dots — so the two lists match check for check. Uploads keep the old behaviour deliberately. A browser sends a whole relative path as the filename when a folder is dropped in, so reducing that to a basename is right there, and safeEntryName is untouched for it. Two things the docs had wrong in the same paragraph are fixed with it: a trailing space is trimmed rather than rejected, and the upload/typed-name split was never written down at all.
52 of 55 ticked from a real pass against a clean install. Two findings, both written up in the testing notes at the foot of the file. The mkfile naming gap is fixed in code but stays unticked: the fix has only been checked against newNameError directly, not through the API or the modal, so it needs one re-run before the line is honestly ticked. The Custom-type centring line was the checklist being wrong rather than the page. Picking Custom swaps the version picker for the JAR URL field rather than removing it, so Server Port keeps a neighbour and the row should not centre. The line now says that, and is ticked on the behaviour that was actually observed.
Closes out the checklist at 55/55 and drops it. Both findings from the beta.6 pass are settled: the typed-name rejection landed in 81f422d, and the Custom-type centring line was the checklist being wrong about the page rather than the page being wrong. The naming fix is verified end to end rather than at the unit level, which is what the last build was waiting on. A slash or a traversal attempt in a typed name returns 400 through mkfile, mkdir and rename alike, including from a direct fetch that bypasses the modal's own client-side gate — so it is a real server check and not a duplicated client one. Reserved names, `path`-field traversal and the rest of the naming rules are unregressed, upload still reduces a dropped folder's relative path to its basename on purpose, and the New Text File gating suite re-ran clean at 24/24. TODO.txt goes with this commit rather than being carried forward. It is rewritten each beta and there is nothing left unticked in it; the history has every version of it, and the outcome is recorded here.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Supersedes #50, which GitHub closed automatically when the
1.1.1branch was renamed to1.2.0.Promoted from 1.1.1 to a 1.2.0 minor because this adds new API surface rather than only fixing things. Currently versioned
1.2.0-beta.5.Five prereleases have been cut from this branch, each published to Docker Hub as
willtda/craftbox:<version>: beta.1, beta.2, beta.3, beta.4, beta.5.New
POST /servers/:id/files/upload,/mkdir,/mkfile,/renameand/delete: creating is ungated (matching/edit-file, which has always written into a running server's directory), renaming and deleting require the server stopped.GET /servers/:id/consolereads console output over REST. Serves the durable on-disk log by default, so it works for servers that aren't running and survives a panel restart. The WebSocket rejects bearer keys, so this was the missing half of the existing command endpoint.GET /servers/:id/files,/fileand/download. Server export and backup downloads moved under/api/v1, so an API key can reach every file on a server; they previously redirected API clients to the login page. (API endpoint to read/download files #57)logEvent, so backups, jar upgrades and user actions are live for the first time; previously only start/stop/crash reached a socket, and only the public status page listened.Fixed
logs/ormods//plugins/, are refused while the server runs — per file, so the rest of the batch still lands. Windows refused that write on its own; Linux, which is every Docker install, did not, and overwriting a live jar or world is enough to corrupt a running server.class="d-none"in twelve places: both halves of the running/stopped hints on Settings and Properties showed at once, the Events Clear button stayed on an empty log, and the disabled-state tooltips on Files, Mods and Plugins came through cut off after their first word.setOperationalStatenever consulting the transition table. (Server can be backed up while still provisioning #56)21.1.100read as older than21.1.95. (Update Jar feature not working if server was imported or created from a modpack #60)stoppedwith the power buttons live. Starting in that window rebuilt the process while the old respawn timer was still armed, launching a second JVM in the same directory on the same port.GET /servers/:id/console?limit=0returned the default 200 lines instead of clamping to 1.GET /servers/:id/events?limit=had the same shape with no lower bound at all, where a negative limit silently dropped the newest events.mods/, which every other mod loader already did.unauthorizedtoast with no recovery. (Better unauthenticated error handling #52).mrpack/.cbxupload dialogs stayed editable during upload, silently discarding edits. (Input elements do not lock/disable while.mrpackor.cbxis uploading. #59)Navigation and layout
centerLoneRowItemsinapp.js— the create form on every validation pass, since its columns come and go with modpack mode; the settings form once at init.Verification
No test suite exists in the repo. Everything through beta.4 was verified with throwaway harnesses against an isolated copy of the app and then click-tested in a browser against a live Docker instance: the restart guard, view rendering in both server states, the event log contract and broadcast pipeline, the console endpoint (path traversal, auth, limit clamping), and the beta.2 file manager surface end to end. That pass found three defects — the busy-file upload, the Clear button, and
limit=0— all fixed in beta.3 and listed above.Still outstanding: the beta.3 fixes have not been re-tested in a browser, and nothing in beta.5 has been through one at all. A fresh install, an upgrade over a 1.1.x data directory, and a Docker build from source have not been exercised; neither has anything needing a real Minecraft client.
TODO.txtis the checklist and is rewritten each beta, so what is unticked in it is always what is outstanding for the build in hand.docs/API.mdtracks the shipped behaviour of every endpoint added here, including the busy-file rejection rules and the ungated-create/gated-destroy split.