A gzip.open() handle passed as a subprocess stream hands the child the
raw fd, so mysqldump/pg_dump wrote plain SQL inside a gzip header and
restores piped compressed bytes into mysql/psql. Backups, policies and
pre-deploy snapshots all produced files that could not be restored.
run_to_gzip/run_from_gzip pump the bytes through Python instead, with
stderr in a temp file so a noisy child can't deadlock the pipe. The two
hand-rolled db_sync dump pipes move onto them too.
Creating a terminal, regenerating the pairing token and moving a server
checked the role but not workspace membership, so a developer outside a
workspace could get a shell on its servers or re-pair them to their own
agent. REST now uses the rule the Socket.IO rooms already applied, in
one helper. The Default workspace counts as shared: servers created
without a context land there and nobody is a member of it.
PROTECTED_ROOTS covered only the running /opt/serverkit-<x>, so the
previous version's .env (JWT key) in the sibling slot was readable by
any role with files.read, including the default viewer.
The sidebar and top bar moved to --surface; the status bar stayed on
--bg-sidebar, breaking the one-background contract the settings UI
browser regression checks.
Each list page restyled DataTable on top of the shared look: fourteen
re-skin classes, filled grey headers, padded cards inside padded pages
and nth-child column widths (which put Docker's Name column 300px in
once the checkbox column existed). The table wrap is now the only
frame, the header is unfilled, column widths come from the column
definition, and raw tables and ui Table uses render through DataTable.
scripts/check-tables.mjs holds it in lint.
Page bodies drop the 1400px reading column (and the per-page caps on
Domains, Deployments, Dashboard, detail pages and Recipes) for one
20px gutter everywhere; Settings caps only its form column. The top
bar gets the sidebar's surface and edge so the chrome reads apart from
the canvas, and the bulk-selection pill clears the status bar.
Also removes decorative chrome: gradient letter avatars and tinted
stat icons become neutral, Domains rows lose the icon tile and the
"Registrar -" sub-line, and destructive row actions are quiet ghost
buttons that turn red on hover (.btn-ghost.is-danger).
The 404 description and the database tuner confirms were built from
three or four t() fragments glued with +, which dropped spaces
("itbelongs", "CheckExtensions") and left translators guessing
(German rendered a short interruption as a short circuit). Each is
now one key with a proper translation in every locale. Also fixes the
German nav term for Dashboard and drops the retired sidebarView key.
Picking a built-in view on any ResourceListPage list showed Unsaved
at once: the live page bag carries pageSize and an unset search that
built-in views never declare. Compare only the page keys both sides
set, treating an empty search as unset.
On PageLayout the studio body sat in a block-level fill region, so its
flex:1 collapsed it to its content height. The tree also repeated
"Not installed" next to the Install button, truncating MySQL to MyS.
The light sidebar sat ~2% off the page and its border vanished, so the
nav blended into the content. It now uses the surface tone with a
visible hairline. The account menu loses its sidebar presets; the
Customize sidebar page already owns that.
A failed contributions request (the backend restarting under an update)
fell back to the baked plugins manifest and cached that for the whole
session. When the baked manifest is empty, every extension vanished
from the nav until a full reload. Retry with backoff instead.
The select sweep replaced native selects with Radix, so selectOption()
failed in the settings UI test. SelectItem now exposes its value as
data-value and the test opens the trigger and clicks that option.
A clickable card was role=button around its own action button, giving
assistive tech two conflicting controls. The title is now the real
button; the rest of the card stays a mouse-only shortcut.
Attach flows create a new domain row, and POST /domains rejects any
name ServerKit already has, so suggesting existing domains there was
guaranteed to fail on submit. rejectExisting drops the suggestions and
reports a taken name as an error. Other DomainField uses (SSL,
tunnels, canonical domain) still suggest existing domains.
Reveal state is keyed by row index. Deleting a revealed row moved the
next row's secret into that index and showed it unmasked. Delete now
shifts the revealed set with the rows and a pasted .env clears it.
Machine translation turned product names into ordinary words
(Compose, Discord, Slack, Telegram, Traefik, Let's Encrypt and
others). They now appear in their original spelling.
Machine translation turned product names into ordinary words
(Compose, Discord, Slack, Telegram, Traefik, Let's Encrypt and
others). They now appear in their original spelling.
Machine translation turned product names into ordinary words
(Compose, Discord, Slack, Telegram, Traefik, Let's Encrypt and
others). They now appear in their original spelling.
Machine translation turned product names into ordinary words
(Compose, Discord, Slack, Telegram, Traefik, Let's Encrypt and
others). They now appear in their original spelling.
Machine translation turned product names into ordinary words
(Compose, Discord, Slack, Telegram, Traefik, Let's Encrypt and
others). They now appear in their original spelling.
Machine translation turned product names into ordinary words
(Compose, Discord, Slack, Telegram, Traefik, Let's Encrypt and
others). They now appear in their original spelling.
Machine translation turned product names into ordinary words
(Compose, Discord, Slack, Telegram, Traefik, Let's Encrypt and
others). They now appear in their original spelling.
Machine translation turned product names into ordinary words
(Compose, Discord, Slack, Telegram, Traefik, Let's Encrypt and
others). They now appear in their original spelling.
Machine translation turned product names into ordinary words
(Compose, Discord, Slack, Telegram, Traefik, Let's Encrypt and
others). They now appear in their original spelling.
Machine translation turned product names into ordinary words
(Compose, Discord, Slack, Telegram, Traefik, Let's Encrypt and
others). They now appear in their original spelling.
Machine translation turned product names into ordinary words
(Compose, Discord, Slack, Telegram, Traefik, Let's Encrypt and
others). They now appear in their original spelling.
Machine translation turned product names into ordinary words
(Compose, Discord, Slack, Telegram, Traefik, Let's Encrypt and
others). They now appear in their original spelling.
Machine translation turned product names into ordinary words
(Compose, Discord, Slack, Telegram, Traefik, Let's Encrypt and
others). They now appear in their original spelling.
Machine translation turned product names into ordinary words
(Compose, Discord, Slack, Telegram, Traefik, Let's Encrypt and
others). They now appear in their original spelling.
Machine translation turned product names into ordinary words
(Compose, Discord, Slack, Telegram, Traefik, Let's Encrypt and
others). They now appear in their original spelling.
Machine translation turned product names into ordinary words
(Compose, Discord, Slack, Telegram, Traefik, Let's Encrypt and
others). They now appear in their original spelling.