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.
Stock nginx (1.18-1.24 on Debian/Ubuntu) has no `.mjs` entry in mime.types,
so the runtime-extension vendor shims the import map points at came back as
application/octet-stream. Browsers refuse to execute an ES module with that
type, so every runtime-loaded extension (WordPress, Git, Mail, Fail2ban, ...)
failed on fresh installs with "Failed to fetch dynamically imported module:
blob:..." (Chrome) or "'application/octet-stream' is not a valid JavaScript
MIME type" (Safari).
Add a location block to the shipped vhosts (and the container frontend
config) that forces text/javascript for /serverkit-vendor/*.mjs. The regex is
anchored to that folder: a bare `\.mjs$` would also capture the
/api/v1/plugins/<slug>/assets/dist/index.mjs route and 404 every bundle.
update.sh already re-copies these vhosts, so existing installs pick it up on
their next update.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NWc84WDsWR7M55Mn7qtpie
Updates the service detail status chip layout for small screens from wrapped columns to a single horizontal, non-wrapping scroll row. It flattens group wrappers, hides group labels on phones, and prevents chip/ring clipping with small spacing and padding adjustments so pills remain readable and tappable.
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.
Expose MFA and passkey enrollment, use shared confirmations and locale formatting, and guard pending user and invitation actions. Add Chromium regressions for both themes and failure states to frontend CI.
- The scroll-fade masks added in #134 used `#000`, which the hex-literal
ratchet in check-frontend-boundaries.mjs counts (design-system 2→6,
databases 5→9). Masks only need an opaque colour, so use the keyword.
- intl.test.mjs compared "60 days ago" in en-US vs es; on any date whose
month equals its day-of-month (7 July) the two short dates coincide and the
test fails. Pin the date instead.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVRnRXqNMZagqQ3RTqG5pz
* fix(navigation): stabilize overflow tab measurement
* fix(databases): prevent mobile toolbar overlap
* fix(navigation): keep mobile actions accessible
* fix(ui): refine mobile scroll affordance
* fix(layout): align status bar with expanded rail
---------
Co-authored-by: Juan Denis <jhd3197@gmail.com>
Generated files the connect work moved and nobody re-ran:
- docs/API_SURFACE.md gains GET /connect/status
- docs/MIGRATION_INVENTORY.md: the hand-shaped error bodies count is 1145
(ceiling 1150) for the 409 the backups PUT now answers when Cloud owns
the destination
- en.json gains app.backups.storageManagedByCloud, so the sentence the
read-only backup form shows is translatable like every other string
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018q56AfqpgKjio4n9KYubvZ
The panel can now run the signed commands Cloud sends, and the first four
of them are the Storage Hub's. A destination assigned in Cloud is tested
here before it is saved, written as `s3.path_prefix` (which every upload,
list and verify path already honoured), and reported back on a `storage`
stream. Unassigning hands it back and deletes nothing in the bucket.
- backend/app/services/connect_commands.py — verify a command JWT against
ServerKit Cloud's JWKS (signature, audience, exp, nonce, consent scope), run the
handler on its own thread, report running/succeeded/failed. An action
this version does not know fails with a sentence saying so.
- backend/app/services/connect_storage.py — storage.assign/unassign/
test/report, and the snapshot the storage stream carries.
- backend/app/services/connect_client.py — handle `command` opens, fetch
the JWKS per connection, publish a storage snapshot on connect and
every five minutes.
- backend/app/api/backups.py — GET /storage says when Cloud owns the
destination; PUT answers 409 rather than letting two writers race.
- frontend/src/pages/Backups.jsx — the local form is read-only while
Cloud manages it, with the sentence that says how to take it back.
- backend/tests/test_connect_storage.py — 23 tests, no network.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018q56AfqpgKjio4n9KYubvZ
The Walkthrough Studio page mounted globally on top of the dashboard on
panels where the extension is not installed. Its module is baked into the
bundle (eager glob), it carried a default export, and PluginLoader's
legacy auto-render only excluded plugins whose contributions appear in
the RUNTIME envelope - which an uninstalled extension never populates.
Three guards:
- drop the studio's default export (routes resolve the named
WalkthroughStudioPage export; both the baked copy and the
builtin-extensions source)
- PluginLoader now also excludes any plugin whose BAKED manifest
declares contributions (declaresBakedContributions)
- regression test pinning both guards for every baked plugin
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`is-blue` resolved to var(--cyan), making it pixel-identical to `is-cyan`
while Walkthrough Studio offers both in its tone dropdown. The theme defines
no --blue token, so point it at --accent, the one remaining distinct hue
among the tones on offer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
addStep() appended through a functional setDraft but then selected with
setStepIndex(draft.steps.length), reading the closed-over render snapshot.
The two can disagree under fast interaction, selecting the wrong step or an
index past the end of the list.
Build the list and derive the index from the same snapshot so they cannot
drift apart. Applied to the builtin-extensions source and the bundled copy
together; sync-builtin-frontends --check keeps them identical.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The signal handler searched the whole step list for a matching signal and
completed whichever step it found. These signals are ordinary product events
-- running a monitor check, creating a service -- so a later step's signal
fires readily while an earlier step is still current, marking it done out of
order and skipping every step in between.
Scope the match to currentStep, which is what the route effect directly above
already does. Normalization flattens an explicit `completion` block onto the
step (normalized.signal), so this covers both spellings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Duplicate page-scoped keys (Title, Duration, Publish, Live preview,
Move up/down) and Walkthrough Studio strings now reuse common.labels/
common.actions instead of re-declaring identical English text.
en.json regenerated: 6430 -> 6420 keys.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Visual authoring tool for walkthroughs: create and reorder steps, pick
highlighted UI targets, choose completion modes (route, signal, manual,
check), validate against the shared schema, preview in a live dock,
publish to the server library, and import/export JSON or copy an
extension contribution block. The Studio dogfoods the new system by
declaring its own getting-started walkthrough in its manifest.
WalkthroughContext now merges built-in walkthroughs with extension
contributions and server-persisted custom guides through a shared
registry, so enabling or disabling an extension adds or removes its
guides at runtime. Adds the walkthrough contribution type to the plugin
SDK (with TypeScript types and vendor shim), Studio API methods, and
i18n strings.
5bfb0f67 dropped the tab-local header but did not regenerate en.json,
so the lint gate failed on catalog drift. Regenerate en.json and delete
the three now-unused recycleBinTab keys from every locale bundle.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The tab-local title, retention blurb and standalone Refresh button
duplicated chrome the grid already provides (Refresh data lives in the
grid tools menu; per-row 'purges in Xd' carries the retention signal).
The page now leads with the view picker and table like other grids.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The self-update feature commit added the 10 new app.aboutTab keys to en
and es only; every other locale fell back to English in an otherwise
fully translated About tab. Add ar, bn, de, fr, id, it, ko, pl, pt, ru,
th, tr, vi, zh-Hans and zh-Hant, matching each file's existing register
(Sie/vous/formal).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The About tab could only detect a new release and link to it; applying the
update meant SSH and scripts/update.sh. Wire that script to the UI:
- panel_update_service launches update.sh via systemd-run in its own
transient unit — a plain subprocess child would be killed when the
updater restarts the panel's own systemd unit. Status is stateless
(transient-unit state + the updater's own update-*.log) so it survives
the mid-update backend restart.
- Capability gates keep the button honest: root systemd installs only.
Docker installs get the docker compose pull instructions; dev servers
and hosts without systemd-run get a reason instead of a button.
- GET/POST /api/v1/system/update (admin_required, confirm-gated,
audited). API surface doc regenerated.
- About tab gains Update Now with inline confirm, live log tail while
the update runs, a restarting phase that tolerates the panel being
briefly unreachable, and success/rollback/timeout outcomes. New
aboutTab locale keys in en + es.
The heavy lifting (blue/green switch, migrations, health check,
automatic rollback) stays in update.sh, which is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
30b780f4 corrected the serverkit-status description in two of its three
copies. The extension's manifest also exists at
frontend/src/plugins/serverkit-status/plugin.json, which is generated from
the builtin-extensions/ source by scripts/sync-builtin-frontends.mjs --
so Extensions CI went red on the builtin-frontend drift check, and
bump-version failed downstream of it rather than on its own.
Regenerated with the sync script rather than hand-edited; the diff is the
one description line.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The deploy drawer posted only url/branch/app_type/build_method/port, so a
repo template's `build_command`, `start_command` and `dockerfile_path`
were decorative — declared in the YAML, read by nobody. The endpoint has
always accepted custom_build_cmd / custom_start_cmd / dockerfile_path;
only the caller was dropping them.
The visible cost was nixpacks builds: with no start command it falls back
to `npm start`, so any repo whose package.json has no `start` script
produced an image that built fine and could not boot.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
localkit, email, cloudflare-ops and git are now published, signed and in
the registry index, so the panel no longer needs in-tree copies of them.
This removes ~29k lines that existed in two places at once and flips the
bundled registry so they install from the catalog like any other
extension.
Unblocked by the operator publishing all nine repos (2026-08-30); the two
registry-mirror tests that plan 83 expected to stay red now pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
# builtin-extensions/serverkit-cloudflare-ops/frontend/components/StoragePanel.jsx
# frontend/src/plugins/serverkit-cloudflare-ops/components/StoragePanel.jsx
PR #121 review follow-ups. The setup summary de-duplicated the install
queue with recommendations winning, so a security-posture extension
that also appeared as an unchecked recommendation was silently dropped
— the posture half now wins regardless of checkbox state. The
Cloudflare storage panel templated English around a translated title
(`No ${title.toLowerCase()} yet.`); each section now passes a fully
translated emptyHint instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The plan 52 Phase 6 git split updated the source manifest under
builtin-extensions/ (shell permission, entry_point, url_prefix,
min_panel_version) without running sync-builtin-frontends.mjs, so the
Extensions CI drift check went red on the next PR.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The API client throws on any non-2xx, so a 400 from validate-install
never reached the `!validation.valid` branch — and the catch only read
`err.data.error` (singular), while this endpoint returns an `errors`
array. The operator saw "Request failed (400)" instead of the actual
reason (e.g. the app name being reserved by the Recycle Bin).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>