Two gaps for existing installs:
- The tint ran only in install.sh, so `serverkit update` (which has its own
build path in scripts/update.sh) would rebuild with the DEFAULT color and a
fixed favicon.ico — existing installs would never get a unique mark.
- Re-tinting on every rebuild would also repaint the box on each update.
Fix: assign the color ONCE and persist it in /etc/serverkit/favicon-color
(survives updates), then re-apply the SAME color from both install and update.
update.sh applies it to the staged dist unconditionally (after the guarded
build), so it works whether dist was rebuilt (source mode) or shipped prebuilt
(release). An install that predates the feature picks up a color on its first
update and keeps it thereafter — a car keeps its paint.
Verified: bash -n on both scripts; persistence confirmed (re-run reuses the
stored color).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The install-time tint only recolored favicon.svg, but the page also shipped a
fixed raster favicon.ico + .png — and favicon-hash scanners hash /favicon.ico,
which was identical across every install. So the "car-paint" tint didn't change
what those tools cluster on.
- index.html links only the SVG now (drop the raster <link>s).
- Remove the unused favicon-16x16/32x32.png.
- install.sh: after tinting favicon.svg, copy it over favicon.ico too, so a
blind /favicon.ico fetch also returns the per-install color instead of a fixed
icon. (SVG bytes under an .ico name — browsers use the linked SVG; this only
affects what a scanner hashes at that URL.)
Still obscurity/defense-in-depth, not a real gate — but now it does what it says.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two issues from PR review:
- Login: the AuthLayout wrapper only reached the 2FA and login-link states —
the primary login form still had a hardcoded auth-container/auth-card, so the
`login_layout` setting didn't affect the page most users see. Wrap the main
form in AuthLayout too.
- apps.py: the legacy scalar-port fallback for a BYO-image app omitted `expose`,
which AppPortService defaults to `public` (binds 0.0.0.0), publishing the port
past nginx. Default it to `local` (loopback). (Workers set no port, so this
was latent, but the generic path is now safe.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The lockfile had drifted from package.json (which is why `tramo` — a declared
dep — was never installed and `vite build` failed to resolve `tramo/react`).
`npm install` reconciled node_modules; commit the resulting lockfile so `npm ci`
installs are reproducible and the frontend builds cleanly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A fixed list of N colors means two installs collide on the identical hex. Use
the "car-paint" model instead: a curated base hue (tasteful families, no muddy
yellows) + a few degrees of jitter + randomized saturation/lightness (rich,
mid-dark — never washed-out). Same family can recur, but the exact shade rarely
does, so favicon-hash comparison across installs almost always differs. Emitted
as an hsl() fill (SVG renders it natively; still a pure text replace).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reduce how identifiable a panel is from its unauthenticated surface, without
losing the brand once you're inside.
- Public vs interior title: new `public_title` setting (default "Control
Panel") drives the sign-in/register/setup headings and the browser tab
before login (and the static first-paint <title>); `panel_title`
(ServerKit) still names the tab once signed in. PageTitleUpdater picks by
route. Both are exposed pre-auth on /auth/setup-status and editable in the
admin Site Settings → Panel Appearance card.
- Per-install favicon tint (install.sh randomize_favicon): the favicon is a
plain-text SVG, so each install recolors the tile to one of a curated set of
legible colors via a string replace — same mark, different color, so a
favicon-hash comparison across installs differs. No image tooling required;
if the step can't run, the shipped default color stays. Runs on both fresh
and release installs; existing installs are untouched.
Defense-in-depth / obscurity only — real protection is access control + auth.
Verified: eslint 0 errors, backend py_compile ok, install.sh `bash -n` ok,
favicon sed recolor confirmed.
Co-Authored-By: Claude Opus 4.8 (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>
Restore the ServerKit name, favicon, manifest, auth-page headings, and
service-worker strings. Genericizing the panel's identity was the wrong
trade: hiding the title/favicon is security-by-obscurity that a real scanner
sees through, and it erased the product identity for almost no benefit. The
pages it touched (login/setup/register) require server or admin access to
reach anyway, so there is nothing to hide there.
The one durable, no-cost measure is kept: the `X-Robots-Tag: noindex`
header (from the prior commit) stays on both nginx server blocks so the
panel isn't indexed by search engines. Real protection lives in access
control + auth, not in the favicon.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
These raster favicons (favicon.ico/.png) were unreferenced dead files — the
app only ever served /favicon.* from public/. Removing them completes the
favicon cleanup from the de-fingerprint change; the served icon is the
generic public/favicon.svg.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reduce how easily the panel can be enumerated by scanners (Shodan/Censys)
and search engines:
- nginx: emit `X-Robots-Tag: noindex, nofollow, noarchive` on the panel
server blocks (both the TLS and insecure fallback configs).
- Generic page identity: title "Control Panel" + neutral meta description
in index.html; the runtime document.title setters (App.jsx, usePageTitle)
and PWA manifest name/short_name match — so the browser tab and manifest
no longer say "ServerKit".
- Drop the distinctive favicon (.ico/.png removed, generic favicon.svg) so a
favicon-hash no longer clusters instances; strip the product name from the
service worker (cache name, push title, header comment).
- Neutralize the visible auth/splash copy (Login/Register/Setup headings,
AppLoader label) to a generic name.
Search engines are the easy case; the header is defense-in-depth. Real
hiding still requires access control (IP allowlist / VPN / zero-trust) and
is deployment-specific.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- config: is_insecure_secret() rejects any dev/placeholder key (dev-*,
jwt-secret-key*, *change-in-production*, *change-this*, empty), not just the
two exact default strings — a variant like `dev-jwt-...` can no longer boot
in production. Used in both ProductionConfig.__init__ and init_app.
- systemd unit: bind gunicorn to 127.0.0.1 by default so the raw API port is
not world-reachable (host nginx already fronts :80/:443). Override with
SERVERKIT_BIND_HOST=0.0.0.0 for setups that front it differently.
- cli break-glass: mint the token with a string identity; flask-jwt-extended
rejects a non-string `sub` at decode time with a 422, which broke every CLI
call that authenticated against the panel API.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A docker app created with a `docker_image` but no repository (root_path /
compose_file NULL — e.g. an `image:` manifest service with no git source)
previously skipped the launch branch in start_app entirely and was just
marked `running`, so nothing ever started.
On first start, materialize a one-service compose from the image and its
typed ports under APPS_DIR, persist root_path/compose_file, and let the
normal compose_up overlay inject the app's effective env (including
resolved vault secrets) — so nothing sensitive is written to the base
compose. No-op when a source already exists, there is no image, or the
app targets a remote server.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Plan 47 Phase 6 (for shipped work). Sync the pre-bundled frontend plugin.json
copies for the four extracted builtins (entry_point/url_prefix + status
description) so builtin-extensions/ and frontend/src/plugins/ agree
(sync-builtin-frontends --check + test_extension_manifests green). Raise the
test-count ratchet 2588 -> 2623. Document the second-speed backend-extraction
recipe in EXTENSIONS.md, add CHANGELOG entries (wizard installs + opt-in
FTP/cloud/remote-access/status), and a README "lean by default" bullet.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Plan 47 Phase 3a. SECURITY_TABS' TabGroupLayout now passes groupId="security"
and mounts {groupRoutes.security}, so extracted security features (fail2ban,
clamav, …) can contribute tabs + group-nested routes via their manifests and
return as tabs in the exact place users know — the same mechanism the
files/servers/monitoring groups already use. Inert until a security extension
ships; no behavior change on its own.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Plan 47 Phase 2. The status-pages blueprint (public /status/<slug> + /badge +
authenticated management API) and its service move into
builtin-extensions/serverkit-status/backend/ (entry_point status_pages:
status_pages_bp, url_prefix unchanged). Core no longer registers /api/v1/status
(management) or imports the service; the unused /api/v1/observability/status-pages
alias is dropped. The StatusPage/StatusComponent/etc models stay core (G2). The
public status/badge pages work whenever the extension is installed.
The core WordPress health-check job (run_health_checks) now reaches
StatusPageService.sync_component_from_health via get_installed_extension_attr and
no-ops cleanly on a lean panel.
Tests: test_status_extraction.py (8) — extraction shape, install=routes,
health-job no-op, relocated status-page mutation + component-check authz.
Adjusted test_observability_namespace + test_api_authz_hardening accordingly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Plan 47 Phase 2. The tunnels blueprint + broker/publish/netutil services move
into builtin-extensions/serverkit-remote-access/backend/ (entry_point tunnels:
tunnels_bp, url_prefix unchanged). Core no longer registers /api/v1/tunnels or
imports the services; the Tunnel/ExposedService models stay core (G2).
New reusable core->extension seam get_installed_extension_attr(slug, module,
attr): the agent gateway reaches TunnelBrokerService.schedule_reconcile through
it, so tunnel self-heal still fires when the extension is installed and no-ops
cleanly otherwise. Backend re-acquisition on upgrade is covered by the Phase-2
migration.
Tests: test_tunnel_netutil.py + test_tunnel_reconcile.py re-homed into
test_remote_access_extraction.py (17), which installs the extension and runs the
helpers/reconcile against its modules.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Plan 47 Phase 2. The cloud-provider blueprint + service move out of core into
builtin-extensions/serverkit-cloud-provision/backend/ (entry_point, url_prefix
unchanged). Core no longer registers /api/v1/cloud or imports the service; the
CloudProvider/CloudServer models stay core (G2). The one-time legacy cloud-secret
encryption is dropped from core boot (already run on any panel reaching this
version; the extension encrypts on write).
Also adds run_backend_acquisition() — a one-shot migration that re-acquires the
now-extracted backend on upgraded panels that installed a converted builtin
frontend-only (whose API used to come from core). Without it, ftp/cloud/etc would
lose their API on upgrade. Wired into create_app after run_auto_install.
Tests: test_cloud_provision_extraction.py (7, incl. relocated secret-at-rest +
backend-acquisition migration); secret-encryption cloud block moved out of core.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Plan 47 Phase 2. The FTP blueprint + service move out of core into
builtin-extensions/serverkit-ftp/backend/ (entry_point ftp:ftp_bp, url_prefix
unchanged). Core no longer registers /api/v1/ftp or imports ftp_service — a
fresh panel that never touches FTP loads none of it. Already in
CONVERTED_BUILTIN_SLUGS, so upgrades re-acquire the backend automatically.
Acceptance (tests/test_ftp_extraction.py): uninstalled = no /api/v1/ftp routes +
app.services.ftp_service gone; installed = blueprint hot-loads and /ftp/status
responds; uninstall removes it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Plan 47 Phase 1. The onboarding "Recommended for you" step now installs real
extensions matched to the selected use cases instead of rendering decorative
strings:
- Backend use-case -> slug map + resolver (RECOMMENDED_EXTENSIONS_BY_USE_CASE,
recommend_extensions_for_use_cases) single-sourced and test-guarded against
dead slugs; exposed via GET /api/v1/plugins/recommendations.
- SetupStepSummary renders default-checked install checkboxes with each
extension's real display_name/description and installs source-aware on Finish
(per-item progress, fail-soft: an install failure never blocks setup).
- complete-onboarding persists onboarding_installed_extensions.
- WordPress flagship seeding is now gated on needs_setup(): fresh installs skip
it (the wizard installs it when the WordPress use case is picked); completed
-setup/upgraded installs keep it (zero regression). TESTING bypass keeps the
WordPress suite green; the real gate is covered by dedicated proving tests.
Proving tests: tests/test_onboarding_recommendations.py (12).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The release workflow builds linux/amd64,linux/arm64 via buildx + QEMU on
an amd64 runner, so the frontend-builder stage ran `npm ci`/`npm run
build` under emulation for arm64 -- 20-40 minutes when it did not hang
outright. Its output (frontend/dist) is static, arch-independent assets,
so pin the stage to $BUILDPLATFORM: buildx runs it natively, once, shared
by both platform images. Final images are unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Backstop for the updater-side skip: remove_retired_extensions() deletes a
retired extension''s copy-installed files and InstalledPlugin row on every
boot, BEFORE the plugin loader, so it is never loaded, never repaired
back, and never present for an older/offline updater to carry forward
into the next frontend build. Also drops serverkit-workflows from
CONVERTED_BUILTIN_SLUGS -- it no longer ships, so the auto-install
candidate was a permanent no-op.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 1.7.25 -> 1.7.33 update broke on panels that still had the retired
serverkit-workflows extension on disk: preserve_installed_plugins carried
it into the fresh tree, where its reactflow import (removed with plan 45)
sank the whole bundle build and blocked the update.
- RETIRED_PLUGIN_SLUGS: the carry-forward now skips slugs the product
deliberately deleted, and docker in-place mode strips their leftovers
after the hard reset (untracked dirs survive it).
- Quarantine-and-retry: when the bundle build fails and plugins were
carried forward, move them into /var/backups/serverkit/quarantined-
plugins-<ts> and retry once -- fail the plugin, never the update.
The updater self-refreshes from main before running, so this protects
every panel on its next `serverkit update` once merged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Track the in-flight plugin id/action so uninstall/update/enable/disable can't be double-fired and each row shows live progress; await loadExtensions() before clearing busy. Includes matching tramo editor and workflow_store tweaks (source + live copies) and tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the logo image at the top of the English and translated READMEs with the new docs/screenshots/poster.png banner, and change the tagline to "Deploy, manage, and monitor servers." (translated per language).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Updated both K8sPage copies to use the full-bleed `sk-tabgroup` page shell and content wrappers, including the loading state. This moves warning/empty/main tab content into `sk-tabgroup__content`/`__inner` for consistent spacing and tabbed-page structure, and removes the extra topbar meta text to match the new header pattern.
Regenerate frontend/src/plugins/<slug>/plugin.json from source via
scripts/sync-builtin-frontends.mjs. Resolves the drift check: k8s was a
missing trailing newline; tramo/wordpress/cloudflare-ops/gpu are the new
featured/feature_score fields normalized to canonical form.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduces the new `serverkit-k8s` backend plugin with full extension wiring: plugin metadata, model registration, lifecycle hooks, kubectl execution service, cluster CRUD/default handling, connectivity checks, normalized read endpoints (nodes/namespaces/workloads/pods/services/ingresses/logs/overview), and admin mutations (scale/restart/delete/apply). It also registers the plugin in the frontend plugins manifest (including plugin styles) so the Kubernetes page can be surfaced by the plugin system.
- Featured order: Faro (100), Automations (90), WordPress (80),
Cloudflare Zone Ops (70), GPU Monitor (60). Scores set in each bundled
extension's manifest (both copies); Faro's lives in the registry index.
- Add illustrated covers for Kubernetes and WordPress (extension-icons +
resolver rules) so they stop falling back to generic category glyphs.
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>
- registry_service: keep featured/feature_score through normalization so
registry (bundled:false) extensions like Faro actually float to the top
and show the Featured ribbon (they were being stripped).
- ExtensionIcons: give the Postfix/Dovecot "Email Server" the envelope and
the Stalwart "Mail Server" the server-rack icon so they stop looking like
duplicates ('email' matched before the bare 'mail' rule).
- Drop Faro's logo override so it uses its full-bleed illustrated cover
(92px) instead of the 44px brand-logo slot.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Match the published serverkit-faro v1.0.2 release (lean zip) in the
offline registry fallback.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add Faro illustrated cover (extension-icons/faro.png) + resolver rule so
serverkit-faro shows a 3D lighthouse instead of a flat glyph.
- Mark Cloudflare Zone Ops featured (score 95) in both plugin.json copies.
- Add a wordpress.detail.panel PluginSlot so the Faro (and future WP)
extensions can render on WordPress site pages.
- Sync the registry offline fallback with the Faro v1.0.1 entry (featured).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Node-based automation builder powered by the tramo engine, embedded as an
opt-in builtin plugin (Automations, /automations). Panel is system of record
for workflow docs; a managed @tramo/server Docker container on the host runs
them headless with run history, approvals, and inbound webhooks proxied
through the panel. Replaces the older Workflow Builder.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add an illustrated 3D cover icon for the Automations extension
(assets/extension-icons/automations.png, transparent bg, 256px) and map
tramo/automation to it in the extension icon resolver.
- Add manifest 'featured' + 'feature_score' (Automations = featured, score 100).
- Sort featured extensions to the top of the Marketplace by score (score is
hidden; used only for ordering) and show a gold 'Featured' ribbon on the
cover + a subtle featured border.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>