97 Commits
Author SHA1 Message Date
Juan Denis e49c627640 Let memoized file rows skip unrelated re-renders
FileRow and FileCard were wrapped in memo, but every handler passed to
them was re-created on each render, so a keystroke or a selection
re-rendered every row of a large directory. useStableHandler gives the
rows fixed handler identities that still run the latest code.
2026-10-10 22:38:21 -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 a636f2cdb1 Ratchet unassociated form labels at 46
The rest head a group of options, sit over read-only text, or label a
picker that does not forward an id yet. A new label must name its
field.
2026-10-10 22:25:44 -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 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 ada4d49ea4 Reload the file listing when the source changes
The listing effect watched only the path. Switching to a remote agent
that advertises no allowed paths keeps the path, so the panel host's
files stayed on screen under the agent's header, and edits went to
the agent against the wrong host's entries.
2026-10-10 22:01:06 -04:00
Juan Denis c98952b907 Load two stylesheets nothing imported; lint for orphans
The Coexistence page and the extension crash card were written with
their own stylesheets, but neither file was ever imported, so both have
always rendered without their layout. A new lint step,
check-orphan-files, fails on any src file nothing imports (SCSS
included) unless it is kept with a reason; it is what found these two.
2026-10-10 20:40:35 -04:00
Juan Denis c7ea877d88 Send the journal priority filter to the API
Picking a priority in the Terminal's journal view highlighted the level
but never sent it, so the list was unfiltered. The API already filtered
on ?priority=; the client now passes it, with the fresh value rather
than the previous render's.
2026-10-10 20:33:12 -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 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 dac348596a Persist File Manager prefs through one hook
Six view preferences each had a lazy localStorage read plus a copied
mirroring effect. usePersistentState does both and tolerates storage
that throws, so the page no longer crashes on it; the panel as a whole
still needs storage for its auth tokens.
2026-10-10 18:53:01 -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 fd8abdda47 Ignore stale folder listings in the File Manager
Opening a folder while the previous listing was still loading let the
slow answer land last and set the current path back, pulling the user
into the folder they had just left. Only the newest browse may now
write.
2026-10-10 14:20:41 -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 5e4ca2e8a1 Refetch logs with the values just chosen
Changing the line count, clearing a search or picking a journal unit
re-fetched through a closure from the previous render, so the request
used the old count, the old search pattern or the previous unit. The
loaders now take the new values explicitly. Also drops a stray brace
that kept the selected journal unit from ever highlighting.
2026-10-10 13:38:30 -04:00
Juan Denis c2229b6874 Strip comments and PIs from sanitized SVG icons
scrub() walked element children only, so comments, processing
instructions and CDATA passed through. They survive XML serialization
but close early when React re-parses the string as HTML (<!-->, <?x >),
so a registry or plugin icon could smuggle out an <img onerror> and run
script with the tokens in localStorage. Keep only allowed elements and
text; a browser regression runs eight bypass payloads in CI.
2026-10-10 06:36:41 -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 32cb19804a Match browser tests to the copy rewrite labels 2026-10-05 00:58:54 -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 718fc223df Update settings browser test for the 2FA rename
The glossary pass renamed the MFA column and filter to 2FA; the test
still looked for the old labels, which turned CI red.
2026-10-05 00:45:01 -04:00
Juan Denis a850c95005 Skip extensions with own catalog in i18n extract
An installed extension under src/plugins/ that ships locales/en.json
owns its keys. Collecting them into core's en.json made
extract-i18n --check fail only on machines with it installed.
2026-10-04 22:27:22 -04:00
Juan Denis 0cdb41caf3 Ratchet the i18n literal census to zero
Every remaining hit is now translated, so the ceiling drops to 0 and
any new untranslated string fails lint.

Three narrow rule changes keep the census honest. A number with a
bare unit symbol (`${ms} ms`, `${n}%`, GB) is a measurement, not copy.
A literal that IS the content of a <code>/<pre> (a log line,
`S3_BUCKET=...`) is a sample, though a fallback shown in its place
still counts. Arguments to reportClientError go to the server log, not
a screen. In the other direction, text starting ". " was being skipped
as a CSS selector; it is the tail of a sentence split around an
element, so it now counts.
2026-10-04 18:14:10 -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 878295dccf Translate inline copy on the pages
Page-level busy labels, status arms, empty-state fallbacks and
templated counts were English in every locale. They now go through
t() in house style: sentence case, "…", "Couldn't X." errors, and
_one/_other plurals where a count used to be hand-rolled. CronJobs
renders its state labels through their keys, and auth.signIn reads
"Sign in" to match the glossary.
2026-10-04 18:11:52 -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 afe81eac29 Count inline and templated copy in the i18n census
The literal census missed copy a user reads through an expression:
both arms of a JSX ternary ({saving ? 'Saving...' : 'Save'}), the
fallback of a || or ??, the right side of &&, template literals
(read as one sentence with holes as {{x}}), and the same shapes as
values of label/title/description keys with no matching *Key. It now
counts them, and reads only the arms of a ternary in copy attributes,
not its test.

Untracked extension copies under src/plugins are skipped: they are
censused in their own repos, and counting them made the host number
depend on what a machine has installed.

The ceiling moves from 0 to the honest count, 844.
2026-10-04 17:37:10 -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 89c59b055b Size every control, table, card and badge from the geometry tokens
Second geometry pass, over every stylesheet: control heights are 28,
32 or 40; tables have a 32px header and 40px rows (the databases grid
stays compact at 32); cards use one radius and 16px padding; about 30
page-specific pill classes follow the 20px badge spec; and shadows are
only on things that float (menus raised, modals and drawers overlay),
with focus as one ring. Glows, hover lifts and card shadows are gone,
and page overrides of the shared primitives are removed.

Sign-in and setup screens use the 40px size. Fixed chrome (status bar,
icons, avatars, chart boxes) is marked // geometry: and exempt.
Measured: distinct button heights 25 -> 13, table headers 8 -> 3,
badges 7 -> 3. check-geometry ceilings are now zero for raw shadows
and control heights.
2026-10-04 04:10:02 -04:00
Juan Denis 5e6c49c1b6 Put spacing, radii and controls on one geometry scale
Measured across the app: buttons rendered at 25 heights, 452 of them
at the browser's default 13.33px font, with 19 radii and 22 shadows.

- Controls: 28/32/40 tokens; buttons, inputs and selects default to
  32px (the dense-product norm), 28 small, 40 large. Buttons and form
  fields inherit the page font.
- Radii: four theme-driven tokens (badge 4, control 6, card 8,
  overlay 12). Theme radius now reaches every corner, not ~10%.
- Spacing: every padding/margin/gap is a $space-* token on the 4px
  grid, with 2px and 6px half steps; odd values snapped.
- Flat primary button and status dots: no gradient, glow or inner
  highlight. Badges are 20px with a 4px radius.

check-geometry rejects the legacy names and ratchets the raw shadows
and control heights that still need a judgment pass.
2026-10-04 03:29:16 -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 a5bb7d3a7c Extract i18n keys for the last select sweep
Also lowers the logical-property ceiling to 427.
2026-10-03 23:33:07 -04:00
Juan Denis b32a90b36d Ratchet one field per job in lint
check-shared-fields fails on a native <select>, a second server
picker, or a page calling the base-domain or port-check APIs instead
of DomainField and PortField. A new exception needs a reason in the
allowlist.
2026-10-03 23:33:06 -04:00
Juan Denis fdaeddef3a Extract i18n keys for the shared-field sweep
Also lowers the logical-property ceiling to what the sweep left.
2026-10-03 23:19:50 -04:00
Juan Denis 3e7aedbf17 Move extension, engine and server-template cards onto CatalogCard
The three remaining catalogs each had their own card: a 140px cover
banner with source, category, first-party and trust badges on
Extensions, a legacy SharedCard on Server Templates, and a bespoke
button card in the engine drawer. All three now use the shared
CatalogCard: one tag, author and trust as plain text, an outline
action, and a flat surface. Server-template facts also get singular
forms ("1 package", not "1 packages").
2026-10-03 21:12:32 -04:00
Juan Denis e691a0842e Lower the logical-property ceiling to 438 2026-10-03 21:06:05 -04:00
Juan Denis 2b1028f29a Put the Databases header on the shared top bar
The explorer drew its own toolbar with an h1 titled differently from
the sidebar entry and a hand-rolled New menu. It now uses PageLayout
like the other workbench pages, with the shared button and dropdown
primitives; the tree and workspace below are unchanged.
2026-10-03 21:04:15 -04:00
Juan Denis c7ea5ef3db Drop the servers hex-colour baseline to zero 2026-10-03 20:54:35 -04:00
Juan Denis 2fd6f3c97c test(settings): await desktop footer actions before measuring [skip version] 2026-09-10 22:11:35 -04:00
Juan Denis 4dc281ce58 test(settings): await both mobile footer actions [skip version] 2026-09-08 09:40:06 -04:00
Juan Denis 8468ab79f2 test(ai): verify management UI and document completed integration 2026-09-08 04:10:21 -04:00
Juan Denis 891666786c test(settings): cover AI controls and responsive settings surfaces in browsers 2026-09-08 03:03:43 -04:00
Juan Denis 1ace946ab9 Keep service metrics stable across metadata refreshes [skip version] 2026-09-07 23:34:57 -04:00
Juan Denis 3c925f10fe fix: clear stale service metrics across runtime changes [skip version] 2026-09-05 15:24:58 -04:00
Juan Denis d67e5d07d0 fix: complete audit follow-ups and add regression coverage
Centralize backup schedule calculations and shared request/form flows, encode query values, and move repository finalization into its service. Add browser CI coverage, repair stale test fixtures, and provide opt-in request and build measurement tools.
2026-09-05 14:41:15 -04:00