Commit Graph
574 Commits
Author SHA1 Message Date
Juan Denis e7ba1c2a89 refactor: route file operations through capability-checked adapters 2026-10-24 04:31:28 -04:00
Juan Denis 540b86c445 Give each schedule picker its own field ids
The picker hardcoded ids like sp-hour, so two on one page (one per
schedule card) produced duplicate ids and every label pointed at the
first picker's field.
2026-10-10 22:35:53 -04:00
Juan Denis 73a93a7b36 Let labels name the resource and server pickers
The pickers dropped an id, so the five labels above them named
nothing. They now put it on the trigger button; clicking the label
opens the picker. Ratchet 46 -> 41.
2026-10-10 22:35:28 -04:00
Juan Denis c7a50510c7 Format build and backup durations through intl
Three hand-rolled copies printed untranslated "3m 0s" and never
reached hours ("75m 0s"). They now use the intl formatter ("1h 15m",
in the panel language), keeping each caller's placeholder. The cron
and deploy-console formats with tenths of a second stay as they are.
2026-10-10 22:27:17 -04:00
Juan Denis 0adb6bf752 Tie form labels to their fields
156 labels named no field: no htmlFor and no control inside, so
clicking one did not focus the input and screen readers announced the
input without a name. Each now points at its field through a useId()
id. The change is additive only (ids and htmlFor), checked by comparing
tokens with the previous version.
2026-10-10 22:25:37 -04:00
Juan Denis adb9ae8fbb Open clickable rows and cards from the keyboard
A row with onRowClick could only be opened with a mouse or the j/k
cursor: it took no Tab focus and ignored Enter. Clickable rows in the
tables, the grid and the card view are now focusable and open on Enter
or Space, with a focus ring. Keys on a button inside the row still
reach the button, and the cursor listener skips a key a row handled.
2026-10-10 22:17:18 -04:00
Juan Denis ca4bdb2236 Drop a needless exhaustive-deps disable
load is a useCallback with no deps, so listing it is exact.
2026-10-10 22:01:34 -04:00
Juan Denis 95a9e227fc Fix row deletes that read state from first render
The vault secret and variable-group columns were memoized without the
state their delete handlers read. Deleting a secret then refreshed a
null vault and reported failure; deleting a group variable sent group
null; deleting a group after a workspace switch reloaded the old
workspace's groups.
2026-10-10 22:01:16 -04:00
Juan Denis 48aed13d65 Delete four more frontend files nothing imports
An app-detail SettingsTab and its shared helper left behind when the
page merged into service detail, a re-export shim nobody imports, and
an unused utility; with their 13 strings in every locale.
2026-10-10 20:38:41 -04:00
Juan Denis 84ac8684e3 Guard the remaining Docker tabs against stale loads
Volumes, Networks and Compose had the same two bugs fixed earlier in
Containers and Images: a late answer from the previously selected
server replaced the current list, and a failed load showed an empty
list (or the old one) instead of the error.
2026-10-10 20:31:25 -04:00
Juan Denis a7413e34fa Move File Manager dialogs into components
Four inline dialogs kept their open flags, targets and input values as
ten separate useState calls on the page. One dialog state now says which
is open, and NamePromptModal / PermissionsModal own their inputs; the
browser test drives all three flows and passes on the old code too.
The create-file dialog had no way to open it and is removed with its
strings.
2026-10-10 20:28:55 -04:00
Juan Denis e94537d0a4 Cut docstrings that narrate refactor history
The worst cases the audit named: docstrings and header comments that
told how three old copies drifted, or quoted subprocess counts that
were stale the day they were written, instead of stating the contract.
Each now says what the function does and keeps one line of why. Also
drops the always-zero n_cloud counter that existed only to keep a boot
log line's or-chain intact.
2026-10-10 20:21:00 -04:00
Juan Denis 78201eeefc Format dates and numbers in the panel's language
107 direct toLocaleString/DateString/TimeString calls followed the
browser's locale (or a hard-coded en-US/en-GB) instead of the language
the panel is set to, and built a formatter per call. They now go
through utils/intl (formatDate/Time/DateTime/Number), keeping each
site's components via parts. A missing date now renders the fallback
dash instead of "Invalid Date". Ratchet ceiling 115 -> 4.
2026-10-10 20:19:32 -04:00
Juan Denis 8b4a19bc95 Delete ten frontend files nothing imports
Components, hooks and a workflow-builder rules file left behind when
their last consumer was replaced or retired, along with their 76
strings in every locale and a style block only they used. Three
unreferenced components that were never wired in (the DNS cutover
drawer, member actions card and account security nudge) are kept:
whether to mount or drop them is a product call.
2026-10-10 16:09:21 -04:00
Juan Denis 6296887bad Remove internal plan citations from app code
Comments, docstrings and a few strings cited plan numbers ("plan 29
#9", "plan 86 §B2") from a git-ignored folder, so they pointed at
nothing; one reached users in a bottleneck hint. Code and frontend
strings are unchanged apart from those citations (checked by AST and
token comparison). A vhost still carrying the old asset comment reads
as undrifted, and a guard test keeps new citations out.
2026-10-10 15:50:45 -04:00
Juan Denis db5f4df74a Show Docker list errors and ignore stale loads
The API now answers 503 when docker ps fails instead of an empty list,
so the Containers page no longer says "No containers" on a host
running many. The Containers and Images tabs show that error, and a
late answer from the previously selected server can no longer replace
the current server's list. Also fixes the local branch of the remote
Docker listing, which passed all= to a parameter named all_containers
and always failed.
2026-10-10 14:01:56 -04:00
Juan Denis 289a157199 Give every list one table and every page full width
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).
2026-10-09 00:21:04 -04:00
Juan Denis 7e0dca491c Translate split sentences as whole sentences
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.
2026-10-09 00:20:46 -04:00
Juan Denis 1677614654 Stop built-in views reading as unsaved
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.
2026-10-09 00:20:31 -04:00
Juan Denis 57142268b0 Fill the database studio and show engine names
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.
2026-10-09 00:20:25 -04:00
Juan Denis 73741eb197 Treat a missing email extension as no relay
The connections hub reads the relay from the Email extension's route.
Without the extension that 404 marked the whole hub as failed.
2026-10-09 00:20:23 -04:00
Juan Denis d8b3e130de Lift the sidebar off the canvas, drop view presets
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.
2026-10-09 00:20:21 -04:00
Juan Denis 6986f16b31 Translate the notification preferences saved message
It was a hard-coded English string the literal census missed.
2026-10-05 00:58:52 -04:00
Juan Denis 65fe44414f Drive the shared dropdown in browser tests
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.
2026-10-05 00:58:49 -04:00
Juan Denis e3cbb39bee Translate the log line-count options
"100 lines" was hard-coded English in every locale. Also carries the
translations for the taken-domain error.
2026-10-05 00:45:12 -04:00
Juan Denis 7e40248bf8 Show workspace member refresh errors over stale rows
DataTable only renders emptyState with no rows, so a failed refresh
after members loaded showed the old table with no error or retry.
2026-10-05 00:45:09 -04:00
Juan Denis 44ff9d419c Stop nesting the CatalogCard action in a button
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.
2026-10-05 00:45:07 -04:00
Juan Denis 0fec9e2216 Refuse existing domains when attaching one
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.
2026-10-05 00:45:04 -04:00
Juan Denis 31297577f0 Keep EnvEditor secrets masked when rows shift
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.
2026-10-05 00:45:01 -04:00
Juan Denis d2465d9817 Pass error details through and translate last labels
The API client handed translateServerError only the code and message,
so validation.required and conflict.exists could not name the field or
resource and fell back to server text; it now passes data.details.
Also translates the service-type subline, RepoConnectForm's default
labels and a ConnectionsHub fallback that the literal census skipped,
and refreshes the migration inventory counts.
2026-10-04 19:03:49 -04:00
Juan Denis 23d950b528 Translate inline copy in ds and shared modules
Grid menus, the filter drawer, KPI band and view pickers sit on every
list page, so their English sort, group and count labels showed in
every locale. Module-level helpers (operations, service types, monitor
scope, the AI context) now call t() at use time or carry labelKeys.
operations.test initialises i18next like errorMessage.test, since
operations.js now translates its labels.
2026-10-04 18:13:37 -04:00
Juan Denis 1a5137b685 Translate inline copy in shared components
Service, database, security, Docker, monitoring and deploy-console
components showed English busy labels, status arms and fallbacks in
every locale. They now go through t() in house style, with _one/_other
plurals for counts. Sentences that were split around an element
(". The statements are unlikely to apply cleanly.") become whole
keys, so a translator can reorder them. LogFileList and
ImportDumpModal resolve their declared keys instead of raw labels.
2026-10-04 18:12:33 -04:00
Juan Denis 3951221fd3 Translate inline copy in settings and setup
Busy-state ternaries, fallbacks and templated labels in the settings
tabs, the connections hub and the setup wizard rendered English in
every locale because the census could not see them until afe81eac.
They now go through t() in house style, with real plurals for counts.
ProviderCard and the sidebar presets also resolve their declared
labelKeys instead of showing the raw label.

es.json drops errors.* keys that en.json does not define yet; they
stay in the working tree for the typed error codes still in progress.
2026-10-04 18:10:48 -04:00
Juan Denis 586d9de205 Rewrite error copy as "Couldn't X." with plurals
Errors read five ways ("Failed to X", "X failed", "Could not X",
"Error loading X", "Couldn't X") and many dropped their period.
407 core strings now say "Couldn't {verb} {object}." with any
{{placeholder}} kept after it, and the generic common.error strings
say what to do next.

Interpolated English verbs could not be translated and produced
"Failed to restart container"-style grammar for every action, so 17
sites (container, compose, service, unit, process, alert and on/off
toasts, sort labels) now pick one string per action. 37 hand-rolled
plurals ("(s)", item{{value}}, fragment + `count === 1 ? '' : 's'`)
use i18next _one/_other keys: extract-i18n reads defaultValue_one /
defaultValue_other into k_one / k_other, and check-i18n accepts a
translator's extra forms (k_few, k_many) beside the English pair.

Validation says what is valid (IP or CIDR range, JSON payloads,
verification codes, sign-in links). The PRIMARY backup badge and the
upper-cased service type are plain text; container status labels go
to sentence case. Orphaned translations pruned; the i18n literal
ceiling drops to 840.
2026-10-04 17:46:37 -04:00
Juan Denis 708c73c5da Show the server's reason in every error toast
Error toasts said either what failed ("Failed to delete service")
with the reason thrown away, or only the server's reason ("Not
found") with no subject. utils/errorMessage.js is one door for both
halves: errorReason(err) reads data.error, data.message, then
err.message (a translated error code wins), and turns bare transport
failures (Failed to fetch, Request failed (500)) into a next step;
toastError(toast, message, err) renders "What failed. Why."

Moved onto it: 236 `err.message || t(...)` sites, 58 bare t() toasts
in catch blocks that dropped the error (51 gained a catch parameter),
and 63 toasts that showed only err.message, which now say what failed
under new keys. 56 "{{message}}" interpolations and ErrorState read
the reason through errorReason. The pages/ toast ratchet counts
toastError calls, since they are the same per-page presentation.
2026-10-04 17:39:07 -04:00
Juan Denis 16837621f6 Use one term per concept across the UI
Applies the copy glossary to about 370 strings and 64 call sites:
service (not app/application/site), sign in/out (not log in/out),
server and panel server (not box/machine/host), Delete destroys /
Remove detaches / Uninstall is for software, New opens a form and
Create submits it, settings, backup vs snapshot vs restore point,
extensions and Features, environment variables, compose project,
two-factor authentication. Confirm dialogs that destroy something now
use the same verb on the title and the button instead of "Confirm".

Backend messages that reach toasts verbatim ("Application not
found" x95 and others) are listed in plan 88 for the typed-error
work rather than edited here.
2026-10-04 04:48:33 -04:00
Juan Denis 611dcdc00c Normalize UI copy: sentence case, no em dashes or filler
1,250 strings across 223 files, edited at their inline English
defaults (en.json regenerated, keys unchanged):
- sentence case for buttons, titles, labels, tabs and nav (827),
  with an allowlist of product names and acronyms
- every em dash rewritten (184); "successfully", neutral "!" and
  short-toast periods dropped; helper sentences end with a period
- "Are you sure?" confirms now state the consequence; "Danger Zone",
  "no going back", "to get started" and "You're all set" gone
- straight quotes, "…", "time zone", unit spacing, "and" for "&"
- Confirm/Cancel and four other hardcoded labels wrapped in t()

scripts/copy-rewrite.mjs applies a {key: English} map at every call
site; the glossary pass reuses it.
2026-10-04 04:30:42 -04:00
Juan Denis bf3bcfa5e3 Show load errors on list pages instead of empty states
On about 30 pages a failed request rendered the empty state, often
with a call to action: SSL offered to install Certbot, Domains said
"No domains yet" (its error branch was unreachable), the catalog and
monitoring pages showed empty lists, Fleet printed "null%" and Server
Templates went blank. ResourceListPage now takes error/onRetry, and
every page separates loading, error (server reason + Try again) and
empty, keeping existing rows with a compact error when a refresh
fails. Settings tabs a user can't open redirect to Profile.
2026-10-04 02:48:47 -04:00
Juan Denis b571f01465 Show load errors in settings and security instead of empty states
A failed request in these tabs read as "nothing here": the firewall
tab offered to install a firewall, Cloud said not connected, About
invented version 1.0.0, and the security config, site settings and
2FA policy forms rendered with defaults, so saving after a failed
load would overwrite the real settings. Each tab now separates
loading, error (with the server's reason and Try again) and empty,
and keeps existing data with a compact error when a refresh fails.
2026-10-04 02:43:05 -04:00
Juan Denis 246eb605a7 Keep monospace for machine values only
Table columns, detail rows, the status bar, the sidebar and the
dashboard widgets showed dates, counts, enum words and labels in the
monospace face. They now use the UI face (sk-cell-dim for dim table
cells); monospace stays on hosts, IPs, ports, versions, paths, IDs,
hashes, code and logs. check-type-scale ratchets the number of
monospace rules so it only goes down.
2026-10-04 01:42:34 -04:00
Juan Denis ddd817f768 Put all text on one seven-step type scale
Core SCSS used about 30 font sizes (9, 9.5, 10.5, 11.5, 12.5px...)
and six weights, so the same kind of text looked different on every
page. It now uses -xs..2xl (12, 13, 14, 16, 18, 24, 32px) and
three weights, following the dense-product norm: 14px body and a
12px floor, as in Primer, Carbon and Atlassian, which raised its own
minimum from 11px to 12px for legibility. Small labels that were
9-11.5px grow to 12px.

The old $font-size-* names stay as aliases on the new scale so
extension stylesheets keep compiling; check-type-scale rejects them,
raw sizes and off-scale weights in core.
2026-10-04 01:24:25 -04:00
Juan Denis d1c310bb94 Replace the last native selects with the shared dropdown
Table footers, column menus, the filter drawer, the schedule picker,
the widget editor, the log viewer and the env edit row now use the
compact shared Select. Also drops CSS left dead by the field sweep.
2026-10-03 23:33:02 -04:00
Juan Denis 075989f922 Treat an open Select as an editable control
Keyboard shortcuts and DataTable row navigation only skipped native
<select> elements. With Radix Select, Escape closed the widget editor
and arrows moved the table cursor while a list was open.
2026-10-03 23:32:58 -04:00
Juan Denis f81345ea75 Let DomainField callers state their own DNS step
Tunnel hostnames are not an A record at this server: a Cloudflare
route creates its own record and a WireGuard tunnel needs the name
pointed at the edge. hint(name) replaces the default line there.
2026-10-03 23:22:03 -04:00
Juan Denis d194d5d2ab Add a compact size to the shared dropdown
Toolbars, table footers and inline rows kept native selects because the
Radix trigger is a full-width 40px form field. size="sm" gives them a
content-width 30px trigger, so they can use the same dropdown.
2026-10-03 23:20:34 -04:00
Juan Denis e5cd50227b Move pages onto the shared field components
Every page that picked a server now uses ServerPicker, so TargetPicker
and the templates-only picker are gone. Install commands and
connection strings are CopyFields, single-hostname inputs are
DomainFields, host ports are PortFields, and native selects are the
shared dropdown. The style guide shows each shared field.
2026-10-03 23:19:34 -04:00
Juan Denis 1451297317 Move shared widgets onto the shared field components
Log, journal, AI model and connection pickers, the capacity and
threshold panels and the deploy console toolbar use the shared
dropdown and ServerPicker; error cards and private URLs copy through
CopyButton/CopyField instead of calling the clipboard directly.
2026-10-03 23:19:28 -04:00
Juan Denis 1186594989 Move app, database and Docker panels onto shared fields
Run Container takes host and container ports as PortField rows and env
as an EnvEditor, database credentials and connection strings are
masked CopyFields, engine ports are checked live, and server, service
and database detail rows are InfoList. Native selects become the
shared dropdown.
2026-10-03 23:19:17 -04:00
Juan Denis eecb487d7a Move settings onto the shared field components
Secrets (API key, webhook secret, 2FA setup) are CopyFields that stay
masked until revealed, SMTP and firewall ports are PortFields, the
canonical and base domain inputs are DomainFields, detail rows are
InfoList, and every native select is the shared dropdown.
2026-10-03 23:19:08 -04:00
Juan Denis 3f772c57f6 Add ServerPicker, CopyField, PortField and EnvEditor
One component per job, so pages stop hand-building them:
- ServerPicker: which machine; local first, capacity, capability filter
- CopyField: any value meant to be copied; secrets stay masked
- PortField: host ports checked live against /ports/check, naming the
  app that holds a taken port and offering the next free one
- EnvEditor: KEY=value rows for forms; a pasted .env fills the rows
Call sites move onto them in the following commits.
2026-10-03 22:50:21 -04:00