Commit Graph
2124 Commits
Author SHA1 Message Date
Juan Denis ee58075fae feat(operations): add generalized run stream 2026-08-21 10:09:02 -04:00
Juan Denis 0062a1a9f5 feat(remote-access): add paired resource pickers 2026-08-21 10:02:21 -04:00
Juan Denis 86c2ce5530 feat(templates): add capability-aware server picker 2026-08-21 09:58:52 -04:00
Juan Denis 988cb659d0 feat(new-service): add scoped resource selection 2026-08-21 09:54:56 -04:00
Juan Denis 652d1ab409 feat(frontend): adopt resource picker for targets 2026-08-21 09:51:50 -04:00
Juan Denis 29a95236fd feat(dashboard): add transactional editing session 2026-08-21 09:46:06 -04:00
Juan Denis 08315c99d7 feat(frontend): add editing controls and shortcut scope 2026-08-21 09:36:02 -04:00
Juan Denis c823c8dc15 feat(frontend): add editing session history 2026-08-21 09:28:20 -04:00
Juan Denis cfcc49adc2 feat(frontend): add resource options adapter 2026-08-21 09:24:29 -04:00
Juan Denis ee152eb1bd feat(search): add resource picker contract 2026-08-21 09:20:54 -04:00
Juan Denis 5fb29f14ef fix: preserve ErrorState SDK compatibility [skip version] 2026-08-21 01:35:07 -04:00
Juan Denis f1c7d1f44e refactor: consolidate error state component [skip version] 2026-08-21 01:30:58 -04:00
Juan Denis 5377a1f3e6 fix: prevent tab overflow route crashes 2026-08-21 01:20:43 -04:00
Juan Denis bf8633b239 fix: close alternate release entry points [skip version] 2026-08-21 00:28:54 -04:00
Juan Denis 4abb50efbb Merge main into dev and preserve locale parity 2026-08-21 00:22:07 -04:00
github-actions[bot] 291b44dd93 chore: bump version to 1.7.124 [skip ci] 2026-08-21 04:17:00 +00:00
Juan Denis f9103c8239 Merge pull request #110 from jhd3197/fix/release-promotion-guards
Make releases immutable and enforce dev-to-main promotions
2026-08-21 00:16:48 -04:00
Juan Denis 92e1862f32 fix: make releases immutable and gate main promotions 2026-08-21 00:15:52 -04:00
Juan Denis fd31f7f8ec Merge pull request #109 from jhd3197/feature/localization-bundles
Expand ServerKit with ten localized language bundles
2026-08-20 23:48:56 -04:00
Juan Denis 2d95e186aa fix: align backend locale order with manifest 2026-08-20 23:42:15 -04:00
github-actions[bot] 71d3a8da55 chore: bump version to 1.7.123 [skip ci] 2026-08-21 03:23:18 +00:00
Juan Denis 10307b4a4f Merge branch 'dev' of https://github.com/jhd3197/ServerKit into dev 2026-08-20 23:23:06 -04:00
Juan Denis 4a31f2a8a4 feat: add second-wave locale bundles 2026-08-20 23:22:32 -04:00
github-actions[bot] 1693a47759 chore: bump version to 1.7.122 [skip ci] 2026-08-21 03:02:12 +00:00
Juan Denis 2a25ff839d fix: restore appearance settings rendering 2026-08-20 23:01:37 -04:00
Juan Denis 240ce23765 feat: add next localization batch 2026-08-20 22:50:06 -04:00
Juan Denis 5481057f95 feat: add five localized language bundles 2026-08-20 22:34:34 -04:00
Juan Denis 13c0782fae Merge pull request #107 from jhd3197/dev
Key the last 8,034 literals and complete the Spanish locale
v1.7.121
2026-08-20 22:05:07 -04:00
github-actions[bot] cffa4cdcb8 chore: bump version to 1.7.121 [skip ci] 2026-08-21 02:01:00 +00:00
Juan Denis cc7178f994 fix(i18n): address Copilot review — render keyed labels, un-split sentences
- QuickCreate menu items carried labelKey but rendered item.label
- DataTable and the ColumnsMenu/GroupMenu/SortMenu/SortChipBar helpers
  rendered column.header while 66 files declare headerKey; all now
  resolve through translateLabel when the header is a string
- ExtensionBoundary failure card glued 'The extension' + slug + 'could
  not be loaded' from two fragments; now one Trans key with a component
  slot so translators can reorder
- LogFileList/IconReferenceTab empty states split 'No matches for "'
  around the query; now single keys with a {{query}} placeholder

es.json synced: orphans pruned, changed keys retranslated.
2026-08-20 22:00:55 -04:00
github-actions[bot] 4f77884129 chore: bump version to 1.7.120 [skip ci] 2026-08-21 01:42:32 +00:00
Juan Denis be130be5c4 chore(i18n): ratchet literal ceiling from 8034 to 0
All user-visible literals are now keyed; the ratchet prevents regressions.
2026-08-20 20:23:42 -04:00
Juan Denis 6424fec1d2 feat(i18n): complete Spanish locale — 7,969 keys
Full informal-tú Latin American Spanish translation of every en.json
key, merged over the existing 183-key locale (existing entries kept).
Mechanically verified: exact key-set parity, {{placeholder}} and <N>
tag parity, no empty values. Adversarial 3-reviewer QA pass applied
(28 fixes: glossary consistency, SDK gate/severity/rate-limit terms,
{{action}} quoting, register and punctuation).

check-i18n: no orphans, no placeholder drift, 0 untranslated.
2026-08-20 20:23:41 -04:00
Juan Denis 549a02f73b feat(i18n): regenerate en.json with 7,969 extracted keys
Generated by scripts/extract-i18n.mjs from the inline t() defaults —
was 183 keys before the mass conversion.
2026-08-20 20:23:41 -04:00
Juan Denis cc9c3fb70d feat(i18n): convert all remaining user-visible literals to translation keys
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.
2026-08-20 20:23:29 -04:00
Juan Denis f7d421797a fix(settings): use path-based settings routes in AI drawer and docker gate
DrawerHeader navigated to /settings?tab=ai and RequiresDocker linked to
/settings?tab=system, but settings routes are path-based — the query-tab
URLs landed on the wrong screen. Now /settings/ai and /settings/system.
Both files also converted to i18n keys as part of the mass conversion.
2026-08-20 20:23:19 -04:00
Juan Denis 555be9d7f8 Merge pull request #106 from jhd3197/dev
Localize the panel and give extensions the same i18n
v1.7.119
2026-08-20 19:11:10 -04:00
github-actions[bot] 4510143386 chore: bump version to 1.7.119 [skip ci] 2026-08-20 23:04:37 +00:00
Juan Denis 2825c536a8 fix(i18n): address review — reset locale readiness, typed error for bad language, correct parity test reference
- LocaleContext: reset ready on each language change so non-default
  locale bundles trigger a rerender once active
- auth PUT /me: raise typed ValidationError (auth.unsupported_language)
  instead of a hand-shaped error body; error-shape ceiling 1145 -> 1144
- i18n.py docstring: point at the real parity test
  (test_user_language_preference.py)
2026-08-20 19:04:09 -04:00
github-actions[bot] 7af31c3331 chore: bump version to 1.7.118 [skip ci] 2026-08-20 22:50:28 +00:00
Juan Denis ae95ac4478 docs: regenerate MIGRATION_INVENTORY to match measured state 2026-08-20 18:41:14 -04:00
github-actions[bot] a38cdc07b4 chore: bump version to 1.7.117 [skip ci] 2026-08-20 21:31:34 +00:00
Juan DenisandClaude Fable 5 1cb09b9987 fix(i18n): verified es fixes — 2FA term, cron nav, scheduling ambiguity, display probe, RAE comma
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 11:02:36 -04:00
Juan DenisandClaude Opus 5 821eccdb3b feat(i18n): run the locale check against any extension repo
`--locales <dir>` points the four assertions at another tree's bundles, so an
extension repo verifies its own translations with the panel's check instead of
copying it -- copying is how the vendor shims drifted. The languages.json
cross-check is skipped there, since an extension ships bundles rather than a
language list.

It also now reports keys present in en.json but absent from a translation.
Those render English rather than failing, so nothing was catching a bundle that
was quietly half-finished -- which is exactly the state a translation pass
starts in and needs to be able to see.

Used to verify all 13 extension Spanish bundles (2,073 keys) at full parity.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 10:47:13 -04:00
Juan DenisandClaude Opus 5 7de4d10eda feat(i18n): codemod for the long tail; census fixes; Intl component options
Built to convert the WordPress extension (1,099 strings across 44 files) and
kept because the same job remains for 12 more extension repos and the panel's
own 8,034-literal tail.

i18n-codemod.mjs keys the same four shapes the census counts, using the same
isCopy rule, so what it converts is exactly what the ratchet measures. It
REFUSES rather than guesses: a hit outside a component needs `t` threaded
through a helper's arguments, which is a design decision, not a rewrite.
Five bugs it had, all caught by building the result rather than by reading it:

- multi-line JSX text became a string literal containing raw newlines. JSX
  collapses that whitespace at render, so the default must too.
- a literal already containing \n (an SSH key placeholder) was emitted raw and
  produced an unterminated string.
- `confirm({ title: 'x' })` matched BOTH the data-object rule and the copy-call
  rule; applying both corrupted the file. Innermost edit wins -- for a call,
  t() is right because nothing resolves a `titleKey` there.
- copy hides in `err.message || 'Failed…'` and `x ? 'A' : 'B'` as often as it
  sits directly in a call, so argument recursion matches the census.
- that recursion then had to stop at nested JSX, or it "translated" a
  `rel="noopener noreferrer"`.

Two census corrections, both of which were inflating the numbers:

- check-i18n-literals recursed into nested JSX inside a copy prop and counted
  the nested element's own attributes. `rel` is not copy; the ratchet was
  unreachable on correct code.
- check-intl-door walked dist/, so an extension repo's bundle double-counted
  every call in it. WordPress reported 56 where the source had 28.

Both censuses and the extractor now take --root (and --out) so an extension
repo runs the panel's own checks instead of copying them -- copying is how the
vendor shims drifted. --update refuses a foreign root: those ceilings are the
panel's.

The format door takes `parts`, passing Intl.DateTimeFormat component options
through for what a named style cannot express -- chart ticks, month labels.
That was the actual reason callers reached for toLocaleDateString([], {...}),
which silently follows the browser; now they keep the format and the locale.

Ceilings re-seeded to the corrected numbers: literals 8034, intl door 123.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 08:50:13 -04:00
Juan DenisandClaude Opus 5 9c0ae9e957 feat(extensions): enforce the SDK gate at install; localize the GUI builtin
Follow-up audit of the extension surface, across all of them rather than one.

THE COMPATIBILITY HOLE. Two places documented an install-time `sdk_version`
check as existing -- app/utils/sdk.py's docstring ("checked at install (manifest
lint) and at load") and the runtime loader's comment, which named
`_assert_manifest_sdk_compatible` -- but `sdk_version_satisfies` had ZERO
production callers; only its own unit test. Consequences split by extension
kind: a runtime ESM extension installed cleanly and failed later at the load
gate, while a builtin/in-repo extension never passes that gate at all, so its
range went unchecked everywhere. The function now exists and runs at install,
beside the min/max_panel_version check, and refuses with what the operator
needs to act: what the extension wants, what this panel offers. Fail-open on a
missing range, matching the load gate's 'grace'. Proved both directions.

This matters now because SDK 1.3.0 added the localization surface and made
i18next/react-i18next shared singletons: an extension that externalizes them
needs a panel at 1.3.0+. Pinning `^1.3.0` is what turns a white screen on an
older panel into a readable message. The bump is additive, so anything pinned
to ^1.2.0 keeps installing and loading. Documented as a version table.

THE AUDIT. Measured, not assumed -- both censuses now take `--root` so they can
run against any extension repo (and refuse `--update` there, since the ceilings
belong to the panel):

  2,105 untranslated literals across 13 extension repos (WordPress alone 1,099
  in 44 files), plus 78 direct toLocale*/Intl calls that follow the BROWSER's
  locale (WordPress 56). None of it was visible to any check before, because
  every census stopped at frontend/src.

serverkit-gui -- the one builtin with real UI -- is converted as the worked
example, importing useTranslation/useFormat from 'serverkit-sdk' exactly as a
third-party extension would. That also closes its own toLocaleTimeString.
Ceiling 8055 -> 8034, intl door 124 -> 123.

The probe now asserts the claim the SDK surface rests on: an extension
resolving i18next through the shared vendor gets the HOST's INITIALISED
instance, and an extension-style t() returns Spanish. A second instance would
be uninitialised and every t() in that extension would render English forever
with nothing reporting it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:31:42 -04:00
Juan DenisandClaude Opus 5 cff35cc651 feat(sdk): share i18next with extensions; generate the vendor shims
SDK 1.2.0 -> 1.3.0 (additive).

Auditing the extension surface for plan 79 found the localization gap I
expected and two pre-existing bugs I did not.

The gap: extensions had no way to translate anything. The SDK now re-exports
the HOST's translation surface -- useTranslation, Trans, t, useLocale,
useLabel, useFormat and the intl door -- and i18next/react-i18next join the
shared vendor singletons. That sharing is the load-bearing part: an extension
bundling its own i18next would get a separate, uninitialised instance, so
every t() in it would render its English default forever and nothing would
report it. Exactly the "two React copies" hazard vendorShare already documents,
and exactly the failure mode this plan hit twice in core. They also join
dedupe/optimizeDeps for the same reason React is there.

The bugs, both caused by the same duplication:

- The import map listed `react` and `react-router-dom`, but no shim existed
  for either. A runtime extension importing bare `react` 404'd at load.
- The serverkit-sdk shim re-exported 26 of the SDK's 71 exports. An extension
  importing Button, Modal, EmptyState, formatBytes or timeAgo from the SDK got
  `undefined`. The shim's own comment claimed parity was "asserted by the
  frontend build + backend test_sdk_contract"; nothing asserted it.

Three files independently restated the same module list -- the import map, the
namespace capture, and one hand-written shim per module -- so they drifted.
Now vendorManifest.js is the list, generate-vendor-shims.mjs derives every
shim's exports (from the real package's namespace, or from the SDK entry's own
export statements -- never typed by hand), the import map is built from the
same manifest, and `--check` fails the build on drift. 26 -> 85 exports on the
SDK shim, 6 -> 8 shims, all present.

Manifest contributions gain an optional `labelKey` beside `label` for nav, tabs
and command-palette entries, resolved at render by the same translateLabel core
uses -- data cannot call t(), and a label resolved at import would translate
once and never follow a switch. Omitting it renders `label` verbatim, which is
what every existing extension does, so nothing breaks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:16:04 -04:00
Juan DenisandClaude Opus 5 33410950e8 chore(i18n): RTL plumbing ratchet; keep the bare t off the Vite-only path
Plan 79 §H + ratchet 7.

<html dir> already follows the language manifest and the locale probe asserts
it. What was missing is a stylesheet that could follow it: 468
direction-anchored declarations (margin/padding/border-left|right, bare
left:/right:, text-align and float) against 5 logical properties. An RTL
locale today would render a mirrored language inside an unmirrored layout, so
the plan ships plumbing and a ratchet, not a locale. The count is 468 rather
than the plan's grepped 412 because float and text-align are equally anchored.

Also splits `src/i18n/t.js` out of the i18n barrel. The API client's error
mapper imports the bare `t`, and the client is reachable from a node test --
which then loaded i18n/index.js and its `import.meta.glob`, a Vite
compile-time transform that throws under plain node. The obvious fix was the
runtime guard that had just been removed for silently discarding every locale,
so the module is split instead: t.js touches only the i18next singleton, and
index.js keeps initialisation and the glob for browser-only callers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 00:04:29 -04:00
Juan DenisandClaude Opus 5 fe3c0d77bc feat(i18n): language selector, and fix two silent locale failures
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>
2026-08-19 23:59:25 -04:00
Juan DenisandClaude Opus 5 965ea5c27f feat(i18n): translate onboarding and auth; map server error codes to keys
Plan 79 §E wave 2 + F1.

Login, Register and Setup go to zero untranslated literals. These screens run
before anyone is signed in, so they are what proves the precedence chain: with
no user row to read, the locale comes from localStorage, then the panel
default from /auth/setup-status, then the browser.

F1 turned out smaller than the plan assumed, and in a good way. The plan
proposed inventing an error-code contract; the repo already has one -- plan 76
milestone B's typed-error door emits { error, status, code, request_id }. So
the work was to USE it: fourteen auth returns become typed raises carrying
stable codes, and the API client maps the code to a translation key with the
server's own English as the default. The 257 `toast.error(err.message)` sites
keep working untouched and gain translations as codes land. Error-shape
ratchet 1159 -> 1145.

The first cut mapped `t('errors.' + code)`, which is wrong in a way worth
recording: a computed key is invisible to the extractor, so those strings
would never reach en.json and a translator would have no way to learn they
exist. The registry is an explicit literal table instead -- it doubles as the
reviewable list of which server errors are actually translated, and anything
absent falls through to English, which is today's behaviour.

Only auth carries codes so far. Plan 76 §C measured 23 sites that choose an
HTTP status by grepping their own error prose; translating that prose would
turn those 403s into 400s, so the rest waits on milestone C rather than
shipping a silent status regression.

test_login_links asserted the bare {'error': ...} body by equality. Updated to
assert the typed contract including `code` -- asserted rather than tolerated,
since dropping the code would silently make the message untranslatable again.

Ceiling 8087 -> 8055.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:49:32 -04:00