Replace the single long scroll of stacked cards with the core settings-layout:
a left sub-nav (Engine / Events & port / Pack secrets) and a content pane that
shows only the selected section. Reuses the global .settings-layout/.settings-nav
classes so it matches core Settings and WordPress. Drops the redundant per-card
Refresh (the topbar Refresh covers it).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Promote 'New from template' from a popup to its own Templates tab, built to grow
like a marketplace:
- Responsive card grid (icon, name, description, integration tags) with a
'Use template' action per card.
- Search box to filter.
- Per-card favorite star; favorites persist in localStorage and float to the
top (server-side store can replace this later without UI changes).
- 'Import template' action: paste a tramo workflow JSON or load a .json file to
create it as a new automation.
Removes the old modal picker and its topbar button.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the name-only New workflow modal with a two-column dialog: an explainer
panel (badge, mini flow diagram, use-case examples) on the left and, on the
right, the name field plus a 'How should it start?' trigger picker. Choices seed
a valid builtin trigger node (manual / cron / panel-event via webhook on
/sk/events / incoming webhook) so the editor opens with the trigger already
placed. Own copy/wording; collapses to a single column on narrow screens.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the three plain text rows with proper cards: a brand-tinted icon tile
(Telegram/GitHub/Discord/generic), bold name, muted description, integration
tags, and a hover chevron. Wider modal (size lg) with an intro line, and the
Close button moves to the modal footer slot.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 'New from template' picker renders inside a <Modal>, which portals to
<body> -- outside the .tramo-page subtree -- so the .tramo-template rules
nested under .tramo-page never matched it. The name/description spans fell back
to inline defaults and ran together. Move .tramo-template-list/.tramo-template
to top-level scope so the portaled modal picks them up (name bold on its own
line, muted description below).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the .sk-tabgroup__inner wrapper WordPress/Domains use inside
.sk-tabgroup__content so the content is padded + max-width centered under the
full-bleed header, instead of sitting edge-to-edge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The topbar was rendered inside a plain padded page-container, so it floated
inset with no header band. Switch the wrapper to the same classes TabGroupLayout
uses for WordPress/Domains -- 'page-container--full-bleed sk-tabgroup' with the
content in 'sk-tabgroup__content' -- so Automations gets the identical
full-width header strip (bordered band, icon+title+tabs+actions) over centered
content.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the '<icon> tramo . workflows . triggers . integrations' topbar subtext
and the redundant inner card-header on the Workflows/Runs tabs. Actions
(Deploy, New from template, New workflow, Refresh) now live in the PageTopbar
and are contextual to the active tab, so each tab shows a single clean header
over its content -- the same pattern as Domains and WordPress.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Navigating straight to an extension route whose extension isn't installed
(or any unknown URL) rendered a blank page -- the route was never registered
and there was no catch-all. Add a catch-all '*' route inside the dashboard
shell that renders a NotFound page (icon, the missing path, links to the
dashboard + Marketplace).
Contributed routes load async, so on a hard refresh onto an installed
extension route the catch-all can mount for a beat before the route
registers; NotFound keys off a new contributions __ready flag to show a
loading skeleton until the route set is known, avoiding a 'Not found' flash.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Automations extension (tramo) replaces the drag-and-drop builder.
- Delete builtin-extensions/serverkit-workflows + its mirror, pages/
WorkflowBuilder.jsx, and all of components/workflow/ (its @xyflow nodes,
edges, and config panels).
- KEEP @xyflow/react (vendor-flow chunk): pages/AppMap.jsx still uses it.
- Redirect /workflow -> /automations in App.jsx (retired-route pattern).
- Drop the pages/_workflow SCSS partial + its main.scss import.
- Refresh the stale serverkit-workflows comments in DashboardLayout +
sidebarItems to point at the Automations extension.
Build + lint clean; sync-builtin-frontends --check green (14 managed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The React-Flow Workflow Builder is replaced by the Automations extension
(tramo). Remove its core Python engine and port its four event emitters to
the panel event bus so tramo workflows can trigger on them via the events
bridge.
- event_service.py: new EVENT_CATALOG entries health.check_failed, git.push,
monitor.high_cpu, monitor.high_memory.
- docker_service.py: drop the app_stopped emit (app.stopped already fires via
the audit trail -- no double-emit).
- environment_health_service/git_service/monitoring_service: emit the new
panel events via EventService instead of WorkflowEventBus.
- Delete app/api/workflows.py, app/services/workflow_engine.py,
app/services/workflow_service.py; drop the blueprint + startup job
registration; neutralize the cron workflow-scheduler handler to a no-op.
- KEEP app/models/workflow.py + its tables (read-only legacy export lives in
the tramo blueprint; table drop is a future cleanup).
- Remove the two workflow-engine test classes from test_jobs.py.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three seed WorkflowDocs (backup-failed->Telegram, nightly health->GitHub
issue, panel event->Discord) served at GET /tramo/templates, plus
POST /tramo/workflows/from-template/<id> to instantiate one.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Opt-in builtin extension. Add a cluster by pasting its kubeconfig (stored
encrypted, one row per cluster); the engine shells out to kubectl against a
per-call temp kubeconfig to show cluster health, nodes, namespaces, workloads,
pods with live logs, services, and ingresses, plus scale/restart/delete and
apply-from-paste. Reads need viewer, mutations need admin. Not platform-gated
(kubectl runs anywhere); no cluster needed on the panel host.
Backend: models (ext_serverkit_k8s_clusters), kubectl_service (argv build +
temp-file materialize/cleanup), cluster_service (CRUD + normalizers), k8s
blueprint (/api/v1/k8s), lifecycle. Frontend: K8sPage (7 tabs) mirrored to the
baked plugin tree via sync-builtin-frontends. 30 extension tests green;
manifest/platform suites green; frontend builds; sync-drift clean.
Plan: docs/plans/44_KUBERNETES_EXTENSION_PLAN.md
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Live test on the dev box caught 'serverkit list-apps --json' printing
the table: cmd_list_apps' hand-rolled flag mapping dropped everything
but --all, and cmd_list_users/cmd_list_servers dropped all args. All
three now forward "$@" (with -a kept as the legacy --all alias), and
test_cli.sh T20 drives the real executed-mode dispatch through a fake
venv with a recording python stub so any wrapper verb that eats flags
fails the suite.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
serverkit migrate-db forwarded the wrapper's own name to cli.py, which
only defines db-migrate — the command has failed with 'No such command'
since the rename. Fix the forward and stop the drift class entirely:
- cmd_panel_cli forwards Click-only verbs verbatim (manifest, services,
apps, repair, login-url, support-bundle, db-migrate/status/history;
panel-status/panel-doctor where a native bash name collides)
- serverkit completion prints a bash completion script; install.sh and
update.sh drop it into /etc/bash_completion.d (best-effort), the
uninstall lib removes it
- test_cli.sh T17-T19: migrate-db forwarding, dispatch reachability,
and a two-way parity guard (cli.py verbs <-> dispatch <-> completion);
test_update.sh T29 covers refresh_bash_completion
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
status, doctor, list-users, list-apps, list-servers, db-status,
deployment-status, services list, apps list, and manifest plan gain a
--json flag emitting machine-readable output, so the CLI can feed
scripts, cron checks, and external monitors. Table-only extras refuse
the combination loudly (--all, --repair).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Email Server builtin extension's backend package was present on disk (and
exercised by the green test suite) but never tracked. Adds it alongside the
already-tracked serverkit-gpu / serverkit-gui backend plugins: the Flask
blueprint (email.py) plus the Postfix/Dovecot/DKIM/SpamAssassin/Roundcube
service modules. __pycache__ stays gitignored.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Clears the frontend Dependabot security alerts, minus the vite chain:
- react-router-dom ^6.22.3 -> ^7.18.1 (direct; fixes the // open-redirect,
alert #17). v7 keeps the react-router-dom API surface — lint + build green.
- overrides pin the transitive fixes: lodash 4.18.1 (prototype pollution +
_.template code injection), js-yaml 4.3.0 (merge-key DoS), postcss 8.5.16
(stringify XSS), @babel/core 7.29.7 (sourceMappingURL file read), ws 8.21.0
(memory-exhaustion DoS + uninitialized memory), engine.io-client 6.6.6.
- npm audit fix cleared the remaining non-breaking transitive alerts
(flatted, minimatch, brace-expansion, ajv).
Down from 8 to 2 npm-audit findings; the remaining two (esbuild + rollup via
vite, both dev-server-only) need the vite 5->8 major upgrade, tracked
separately as #68.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GET /api/v1/search?q= (JWT, 2-char minimum) fans a term out across the core
entity types — services, servers, domains, databases, WordPress sites, cron
jobs (admin-only), installed extensions, and vaults (names only, never secret
values) — reusing each domain's existing access helpers so a member only sees
what their grants allow. Per-type cap of 5; each entity block is isolated so one
failing type degrades gracefully instead of 500-ing the search.
Wires the frontend api.search() async provider (200ms debounce) into the
palette's entity group, replacing the fetch-everything-on-open prefetch. Adds
tests/test_search.py covering shape, min-length, cap, member-vs-admin filtering,
cron admin-only, and the vaults names-only rule.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mechanical wrap-the-card pass: each remaining settings tab component now spreads
useSettingFocus's register(id, class) onto its identifiable cards so a palette
settings result scrolls to and flashes the exact card. Covers Profile,
Appearance, Sidebar, White Label, About, Users, Activity, Site, Modules,
Migrations, SSO, API, AI, System, and Connections. Cards whose roots live in
non-editable subcomponents (login links, invitations, provider cards) register
at the nearest reachable section or fall back to landing on the tab.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
useSettingFocus rides the useFocusParam rail with a new `setting` kind: a
palette result navigates to /settings/<tab>?focus=setting:<id>, and the target
card scrolls into view and flashes a token-based highlight for ~2s. Wires the
headline Security (2FA card + admin require-2FA policy, now mounted) and
Notifications (email/SMTP, channels, severity, categories, quiet hours) cards.
Adds scripts/check-settings-index.mjs (dependency-free) to the lint step so
every settings tab must carry at least one index entry — a new tab can't ship
invisible to search.
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>
Connecting GitHub required hand-registering an OAuth app and pasting a client
id + secret — friction, and an open-source panel can't ship a secret anyway.
Now an admin sets it up in one click.
- backend: build_github_app_manifest / complete_github_app_manifest — hand
GitHub an app manifest, convert the returned code into credentials stored
locally (client id/secret in settings; PEM + webhook secret encrypted). No
shared secret in the repo or build.
- connect flow: when an app slug exists, generate_github_authorize_url points at
the app's install screen (grants repos + authorizes in one hop); repositories
are listed per-installation since GitHub Apps don't use /user/repos.
- get_github_config surfaces provider_kind/app_slug/app_name/install_url.
- routes: GET/POST /source-connections/admin/github/app-manifest[/complete].
- frontend: Settings -> Connections leads with "Set up in one click" (auto-POSTs
the manifest form to github.com); a new /connections/github-app/callback page
finishes setup and chains into connect. Manual OAuth app moves under
"Advanced". 7 proving tests (manifest shape, conversion+storage, install-url
routing, app-mode repo listing); ratchet 2498->2505.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Feedback: the template step showed only AgentSite in a stacked card. Make it a
real gallery of the whole catalog.
- pull the FULL template catalog into the picker (repo + one-click compose), not
just repo-kind; icon + title tiles ~4 across
- collapsed to two rows; "Show all N" expands into a scrollable grid with a
simple search box
- curated ordering floats AgentSite + popular apps first; AgentSite badged
"Featured"
- picking a repo template continues in the wizard; picking a one-click compose
template routes to its install flow (its real deploy path)
- keep the inline "Bring your own manifest" escape hatch
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Feedback on the template step: it read as sparse (one bare row), the "Browse all
templates" button punted to another page, and the stepper hugged the left.
- center the Source/Connect/Review stepper
- render templates as featured cards: icon (with fallback), name, a "Featured"
badge, version, description, category chips, and the repo URL — a real preview
instead of a one-line row
- add an inline "Bring your own manifest" card: deploy any Git repo shipping a
serverkit.yml / Docker Compose / Dockerfile by switching to the Git-remote
input in place, no page jump
- demote "Browse all templates" to a subtle text link
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>
Templates was two pages stacked: a one-row "Deploy templates" section above the
real 106-template grid, plus a category chip wall with a "More +95" spillover.
Now it is one browsing surface.
- repo-kind templates render in the main grid as normal cards with a kind badge
(Badge info "Git repo" vs outline "One-click")
- topbar SearchField + FilterButton/FilterDrawer (category, kind, sort) with a
single row of quick category chips; the chip wall and spillover are gone
- repo cards open the same detail shell with a repo-specific body (repo URL,
detected manifests, env preview from the /manifest endpoint), primary action
"Deploy" -> /services/new?template=<id>
- `#deploy-templates` anchor redirects to the ?kind=repo view; kind joins
category/search/sort in the URL params
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deploy templates were a hardcoded frontend constant (AgentSite, with a
hand-fabricated manifest and an EXAMPLE badge). They now live in the backend
catalog as `kind: repo` YAML entries that blend with the 105 compose templates.
- template schema gains `kind: compose | repo` (absent = compose, so every
existing template is untouched); repo templates carry a `repo` block with a
`url` instead of a compose stack
- backend/templates/agentsite.yaml (kind: repo) replaces the JS constant
- list + detail payloads serve `kind`/`repo`; category derivation picks up
repo templates automatically
- GET /api/v1/templates/<id>/manifest inspects a PUBLIC repo via
RepositoryManifestService.analyze_public_repo (raw host, no clone, no OAuth)
and falls back to the template's declared hints, labeled `template-hints`
- index.json + catalog-cert test recognize repo-kind; ratchet floor 2475->2498
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A v1 manifest service gains a third image source next to buildpacks and
BYO image: — `dockerfilePath: services/api/Dockerfile`. This is the
monorepo path: one repository, several services, each built from its own
Dockerfile with the repo root as the shared build context.
- spec: new service key (camel + snake alias), validated relative-only
(no `..`/absolute), mutually exclusive with `image` and `containers`,
rejected on db services; schema docs copy kept in sync.
- resolve/plan: the path rides the create_app payload (with autoDeploy)
and shows in the step description; a changed path plans as an app
update; drift_pair tracks it against the stored build config.
- apply: create clones the project's repository into the managed apps
dir (idempotent — an existing checkout is reused) and writes the same
git-deployment + build config the import wizard does, so the existing
deploy pipeline (push webhook, per-service autoDeploy) takes over
unchanged. Source resolves from the stored manifest's provenance, else
a sibling app's git deployment; neither is a plan-time blocker
(dockerfile_no_source). Remote targets refuse like the other appliance
features ('a Dockerfile build' joins _service_features).
- scaffold: a dockerfile-built app round-trips its build config instead
of leaking the built image as a BYO `image:` declaration.
- import wizard: a v1 manifest with dockerfilePath seeds
build_method=dockerfile + the path.
Tests: 10 new (spec exclusivity/traversal + plan/apply/blocker wiring);
manifest suite 96/96, full backend suite green except 3 pre-existing
test_server_time_timezone failures also present on a clean tree.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
build-release.sh excluded *.png/*.jpg/*.jpeg globally — fine when the only
images were screenshots, fatal once ExtensionIcons.js (8bd9c1a) started
importing frontend/src/assets/extension-icons/*.png as build inputs: the
/tmp build tree lost them and rollup failed to resolve. The blanket rule
was also silently stripping frontend/public/favicon-*.png from every
release build.
Replace with targeted excludes: docs/screenshots, frontend/dist,
.codex-screenshots, template-icons, and (via --no-wildcards-match-slash)
root-level ./*.png|jpg|jpeg strays + dev/*.png. Verified by listing the
tar: 14 extension icons + 4 favicons included, zero stray images, *.log/
*.tmp/*.pyc still excluded; shellcheck clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The plan-39 em-dash purge hand-wrote six baked plugin.json copies without
the trailing newline that sync-builtin-frontends.mjs emits, tripping the
CI drift gate (--check). Regenerated via the script; content unchanged.
Guard suite test_extension_manifests.py: 28 passed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Introduce serverkit-gpu extension: backend plugin with GPU API and service (nvidia-smi based) providing per-GPU metrics and best-effort container resolution for GPU processes. Files added: backend/app/plugins/serverkit-gpu/{__init__.py,gpu.py,gpu_service.py,plugin.json}. Also update several frontend plugin.json files (including serverkit-gpu) to match manifest formatting (trailing newline adjustments). The GPU service degrades gracefully when nvidia-smi or Docker info is unavailable.
Install the guard the data loss taught us to want: code and its proving tests
died together, so the suite stayed green (2572->2209 passing) while whole
features silently vanished — nobody diffs a green number.
- backend/tests/BASELINE_COUNT (2475) + check_test_count.py: compares pytest's
collected count to the floor; CI (backend-ci.yml new step) fails if it drops.
Lowering the floor requires editing BASELINE_COUNT in the same commit, so a
silent test loss surfaces as a red X instead of a still-green suite. Verified:
passes at baseline, exits 1 on a simulated drop.
- CHANGELOG: document the recovery-parity restore (Cloudflare R2 engine,
email-bounce, DNS-cutover staging, survey routes, cron CSS, 18 test suites)
and the ratchet.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>