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.
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.
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.
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.
Shows where the panel host's disk goes, when the rollback install is
removed, and the retention windows that bound ServerKit's own share,
with cleanup handed to the existing reclaim modal.
Panel side of the managed profile: the panel receives a presentation
profile from Cloud over the existing command channel, stores it,
acknowledges it, and hides or simplifies fleet-owner-only surfaces
until the profile is lifted or the link is revoked.
- ui.managed_profile command handler with managed-profile.json
storage, audit entries, and ack-by-output; profile is cleared on
disconnect and on consent revoke
- GET /api/v1/connect/managed-profile for the local UI
- ManagedProfileContext with capability-driven filtering of sidebar
items, command palette actions, and sidebar settings; ManagedCard
explains the managed state
- In-page collapses for Templates deploy, About self-update, and
Backup schedules/storage (restore stays available)
- Settings gains a ServerKit Cloud tab showing link and profile state
Local verification: 153 backend tests pass, 198 frontend node tests
pass, lint clean, build ok. No live policy has been pushed to a real
panel yet.
The palette's empty state now leads with a Create tile grid (shared list in
data/createItems.js) plus a '+ n more' shelf that folds in the less-common
targets and any extension-contributed 'New ...' commands, then a Recipes
group (picking one starts it and opens the recipes console). Create and
recipe entries also score into regular search results.
The sidebar '+' stops carrying its own little dropdown and opens this
palette instead — one door for every new-thing flow, like the prototype.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
en.json drops from 8107 to 6624 keys: repeated UI copy (Cancel x95,
Status x67, Refresh x54, ...) now lives under common.actions /
common.labels / common.loading, and extractor-era suffix duplicates
(refresh2, status3) within a component collapse onto one key.
Trailing ... defaults are normalized to the ellipsis character.
Translations are preserved: all 16 locales were re-keyed, carrying
existing per-component translations onto their new common.* keys
(majority vote where sources disagreed), and stale keys were pruned.
Verified: npm run lint (0 errors), node --test (140 pass), vite build.
edit
Mass conversion of ~8,000 English literals across 320+ files to
t('app.<file>.<key>', 'English default') calls, plus hand-conversion of
the 43 literals the codemod refused (concatenated toasts, module-level
helpers, class components) using bare t() from i18n/t or restructured
{{placeholder}} keys. Marketplace unsigned-release dialog now translates
the Install/Update verb inside the placeholder.
ESLint: 0 errors. Literal checker: 0 remaining.
Plan 79 B3 + acceptance 2/3/7.
The selector lives in Settings -> Appearance beside the theme picker, because
it is the same shape of choice, and is registered in the settings search index
so it is findable. Built on ui/select rather than a hand-rolled dropdown.
Proof is a Playwright probe (.claude/skills/screenshots/probe-locale.mjs --
that tree is git-ignored, like docs/plans). A screenshot in one language
proves nothing: the copy could be hardcoded English that happens to look
right. The probe loads the shell under two stored locales in a browser whose
own language is French, so following the browser cannot pass it, then drives
the REAL selector and asserts what the plan committed to:
1. the shell renders per-locale and <html lang>/<dir> follow the panel;
2. selecting Spanish re-renders the sidebar with no navigation, and caches
the choice for the next pre-auth paint;
3. with every API call aborted, the pre-auth screen still renders in
Spanish -- which is the whole reason locales are bundled, not fetched.
It immediately earned itself by catching two bugs that lint, build and reading
the diff all missed:
- `typeof import.meta.glob === 'function' ? import.meta.glob(...) : {}` -- a
guard added so the module could be imported by node tooling. Vite replaces
the CALL at build time; there is no such function at runtime, so the guard
was always false and discarded every locale bundle. Every language silently
fell back to English and nothing failed.
- Sidebar category headings stayed English in every locale: the keys lived in
CATEGORY_LABEL_KEYS and the English in CATEGORY_LABELS, two parallel maps.
Not sibling properties, so the extractor could not pair them and the keys
never reached en.json. One object per category now holds both.
Both are the failure this plan exists to prevent: copy that looks translated
and is not, with nothing to say otherwise.
es.json covers all 164 keys.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
One list of everything the panel has soft-deleted, with Restore on every row
and a quiet purge for admins. Type-agnostic: `kind` comes from the server's
registry, so a newly restorable model appears here with no change to this
component.
Says out loud what restore does NOT do -- it returns the record, not the
configuration torn down when it was deleted -- because a Recycle Bin that
implies otherwise is worse than none.
The retention countdown per row ("purges in 3d") turns amber in the last
stretch: that is the only warning before a record is gone for good. Purge is
admin-only, sits behind a confirm, and stays a dim icon next to a full-width
Restore -- the safety net should make the safe action the loud one.
Registered as a settings tab with search-index entries, so the command palette
finds "restore", "undelete", "trash".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ported from the Domains design mock. A CSS grid, not a <table>: header and
rows share one --sk-grid-tpl track list, which is what makes show/hide and
move-left/right a pure data change with no <colgroup> to keep in sync.
fields.js field model + filter-rule engine (text/enum/num/bool/date),
sort, group, CSV/JSON export
DataGrid grid, sticky head, groups, selection, density, sub-detail
ColumnMenu per-column popover: sort / group / filter / move / hide.
Flat, state-model-agnostic props so <DataTable> can drive
it from sorts[]+hiddenKeys and <DataGrid> from one cfg.
useGridConfig the whole chrome as ONE serialisable object, so a saved
view is a snapshot of it and "dirty" is a deep-equal away
useGridRows filter -> sort -> page -> group, memoised
GridViewPicker saved views rendered as the page heading
GridFilterDrawer Filters + Fields panes
GridChips / GridBulkBar / GridFooter / GridToolsMenu / GridFilterButton
Does not replace ds/DataTable -- column descriptors are a superset, so a
list can move up by adding `type` and `width`.
Adds the --surface-0 token (the recessed step below --surface) that group
headers, drawer tab strips and drawer footers sit on.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The group's information architecture had drifted. The Events tab pointed at
/telemetry, which was nested inside the monitoring TabGroupLayout but ALSO
rendered its own PageTopbar — two stacked headers. Jobs sat as a top-level
sidebar entry while being the same class of thing as Events, so the two read as
rival pages.
- MONITOR_TABS is now Overview / Monitors / Incidents / Rules / Capacity /
Events / Jobs / Doctor (+ Status Pages from the extension). Alerts is gone as
a tab; /monitoring/alerts and /monitoring/fleet-alerts redirect to Incidents,
and FleetAlertsPanel moved there with its own ack/resolve lifecycle intact.
- Jobs moves to /monitoring/jobs and loses its sidebar entry; /jobs redirects.
Activity/Scheduled becomes an in-page SegControl since the group's bar already
owns the tab row. JOBS_TABS is deleted.
- Events (Telemetry) rebuilt on the standard pattern: no PageTopbar of its own,
KpiBand + MetricCard severity filters instead of bespoke telemetry-stat divs,
topbar SearchField + FilterDrawer instead of an inline collapsible panel,
DataTable + DS Drawer. Every capability is kept — correlation drill-down, all
filters, test-event emit, cleanup, pagination.
- The Overview leads with a monitors summary (availability, live check feed,
monitor list); host resources keep everything they had, one section down.
- Status pages can now publish a monitor that already exists, and detach it
again without destroying it — deleting a component now deletes a real monitor.
FilterDrawer grows `children` + `onClear` so a host can add non-list filters
(free text, date bounds) and still have "Clear all" reach them; without onClear
it would silently reset only the chip groups.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two sidebar surfaces that shouldn't have been top-level pages. Both
touch App.jsx + sidebarItems.js, so they land together.
Test Sandbox: the distro-matrix console shipped visible and routable
on every production install. Add a shared useDevMode() hook
(module-scope cache + listeners, same shape as useModules) resolving
`import.meta.env.DEV || (isAdmin && dev_mode)`. The sidebar item gates
on requiresCondition: 'devMode' and the route on the same hook, so a
visible nav entry can never redirect to the dashboard. The guard waits
on `resolved` first, or an admin deep link would bounce before the
dev_mode fetch lands. Settings' toggle calls refreshDevMode() so the
sidebar reacts without a reload.
Webhooks: the inbound receive/verify/forward console is server
configuration, not a daily ops surface, so it becomes a Settings ->
Admin tab next to the outbound subscriptions it complements. /webhooks
and /secrets/webhooks redirect there; the sidebar item is gone, which
empties ADVANCED_ITEM_IDS down to the queue console. The command
palette entry drops its navId (a Settings tab has no nav item for the
workspace permission gate to consult) and settingsIndex.js gains the
three entries check-settings-index.mjs requires for a new tab. The
native confirm() on delete becomes the shared useConfirm() dialog.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fleet Monitor and /monitoring were the same questions at two scopes, so they
were two pages restating each other: both grew an alerts surface and both
grew thresholds. They are now one page with a scope, not two pages with a
gap between them.
The top bar carries a server picker, URL-backed as ?server= so the choice
survives a tab change, a refresh and a pasted link. Overview, Alerts and
Capacity all follow it. The fourth KPI tile is whichever reading the current
scope actually has — load average on the panel host, container count on an
agent — rather than a blank where the other one would be.
The overview is ported from the design mock: KPI band with trends and
sparklines, two trend charts, and a Host health strip below. That strip is
what the standalone heatmap became, except each card is a button that
re-scopes everything above it instead of a cell you can only read.
There is deliberately no second tab strip inside the page. Overview / Alerts
/ Rules / Capacity / Doctor live in the group's own top bar, the way the mock
puts them — a nav under a nav reads as two competing headers.
Also: the sidebar said "Observability" for a page, a route and a title that
all said "Monitoring", and the delivery channels rendered Discord, Slack and
a raw webhook with the same generic glyph. Both fixed.
/fleet-monitor redirects; the Servers tab strip loses the entry.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Make the panel's identity admin-configurable instead of hardcoded, and let
operators pick a login-page layout — so instances can be branded/varied from
the panel rather than by editing source.
- Settings: new `panel_title` (default "ServerKit") and `login_layout`
(default "centered") keys via SettingsService (key/value — no migration).
- Exposed pre-auth on the existing GET /auth/setup-status so the login page
reads them before any authenticated call.
- Frontend threads panelTitle + loginLayout through AuthContext; document.title
(App.jsx PageTitleUpdater + usePageTitle) and the auth-page headings use the
configured title, with import.meta.env.VITE_PANEL_TITLE as the build-time
default for the first paint.
- Login page refactored to an AuthLayout selector with three variants —
Centered (current), Split-hero, Minimal — chosen by `login_layout`.
- Admin UI: a "Panel Appearance" card (title field + layout dropdown) in the
admin-only Site Settings tab; saves via the generic settings API and calls
refreshSetupStatus so the running SPA updates live. Searchable via a new
settingsIndex entry.
Register/Setup adopt the configured title on their headings. manifest.json
stays static (documented limitation). Verified: eslint clean on all touched
files (0 errors), settings-index guard passes. (The repo's full `vite build`
currently fails on an unrelated pre-existing plugin import, serverkit-tramo →
`tramo/react`, independent of this change.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
"Marketplace" reads as a store; the page is where you browse and install
extensions, so call it Extensions.
- Nav label + page/tab title + route /marketplace -> /extensions
(first tab drives the topbar title via TabGroupLayout).
- /marketplace and /marketplace/installed now redirect to /extensions.
- Update command palette, page-context map, NotFound, empty-state copy.
- Keep the internal nav id, CSS classes, component/file names, and the
backend /api/v1/marketplace/registry endpoints unchanged; palette keeps
"marketplace" as a search keyword so the old term still finds it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rebuild CommandPalette around scored providers (pages, settings, actions,
plugin contributions, docs, async entities) with a single subsequence fuzzy
scorer and shouldFilter={false} so cmdk no longer double-filters. Adds:
- data/palettePages.js, data/settingsIndex.js (84 cards), data/commandActions.js
- utils/paletteScore.js (word-start bonus + category weight) and
utils/paletteFrecency.js (localStorage frecency: count + time decay)
- hooks/usePaletteAuthz.js — mirrors the sidebar's admin + workspace-nav gate
so a member's palette never surfaces admin surface
- F1 and Ctrl/Cmd+Shift+P open aliases alongside Ctrl/Cmd+K
- prefix modes (`>` actions, `?` docs), Recently used empty state, kbd footer
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
/services/new rendered everything at once regardless of state — a "Ready to
Import" rail of placeholder rows, a decorative Connect->Detect->Deploy strip, a
numbered guide, and two boilerplate note boxes. It is now a slim stepper over
one centered column.
- three explicit steps: Source (radio-cards) -> Connect (source-specific input
+ API-driven template picker) -> Review (manifest detection, buildpack, env
list, core fields; nothing renders without real data)
- decomposed the 1,327-line monolith into pages/new-service/: a
useNewServiceForm hook (step state + submit) with presentational SourceStep /
ConnectStep / ReviewStep components
- ?template=/?source= deep links land preloaded on the Connect step; deploy
templates load from the backend catalog (kind: repo) instead of a JS constant
- submit payloads are byte-identical to before (flow re-skin, not a backend
change); _new-service.scss rewritten for the stepper layout
- deletes the hardcoded frontend catalog (data/deployTemplates.js) now that its
last consumer is gone, and the old single-file NewService.jsx
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Per-source explainer strip (SOURCE_GUIDES): the selected source card renders
what it does, what to have ready, and the 2-3 next steps.
- Extract the hardcoded SERVICE_TEMPLATES into frontend/src/data/deployTemplates.js;
the Templates tab gains a 'Deploy templates' section (badged 'Example' for the
demo) with a 'Use template' deep-link (/services/new?template=<id>) that
preselects it in the wizard. One list, two entry points; Template Library
button now targets #deploy-templates.
- New docsLinks.js map + <DocsLink> (book icon, opens serverkit.ai docs in a new
tab, hidden under White Label — the single place that rule lives). Wired into
the New Service source pane (deploy-sources / manifest) and the Marketplace
manual-install modal (extensions/installing).
Lint 0 errors; build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>